工作半年期间调研的一个库,很有价值

小狮子前端

共 5620字,需浏览 12分钟

 ·

2021-10-24 02:15

大家好,我是 HearLing。不知道大家还记得不记得我呀,许久未发文章,一直都在忙工作,这段时间就没有输出文章了,经过几个月的修炼,还是沉淀了许多知识。今后和 Chocolate 会多多更新文章,大家可以多多关注呀。

这篇文章是我工作中调研的一个库,使用 proxy-memoize 代替 reselect。可能许多小伙伴听到还没了解过这两个库,我还问了下 Chocolate,他也不知道哈哈哈。

在工作中我还调研了许多的库,在后期都会总结一下,我觉得沉淀这些知识很有帮助,今天分享给大家,以后说不定大家能用到呢,赶快收藏一波~

引言

在像 React 这样的前端框架中,对象不变性非常重要。但其实它本身并不支持强制不变性。那这个库利用了 ProxyWeakMap,并提供了记忆功能。仅当参数(对象)的使用部分发生变化时,记忆函数才会重新计算原始函数。

通过引言我们已经知道了它的优点,那么你可能会好奇他是如何实现的,那么你可以看看下面这个介绍,如果你只关心它是如何使用你也可以跳过这一小节:

如何工作

当它(重新)计算一个函数时,它将用代理(递归地,根据需要)包装一个输入对象并调用该函数。当它完成时,它将检查什么是受影响的。这个受影响其实是在函数调用期间访问的输入对象的路径列表

当它下一次接收到一个新的输入对象时,它将检查受影响路径中的值是否被更改。如果是被更改,那么它将重新计算函数。否则,它将返回一个缓存结果。默认缓存大小为1,可配置。

一个个说吧,首先要包装成对象:显然这里需要注意:一个要被记忆的函数必须是一个只接受一个对象作为参数的函数。

//要为对象
const fn = (x) => ({ foo: x.foo });
const memoizedFn = memoize(fn);
//不支持
const unsupportedFn1 = (number) => number * 2;
const unsupportedFn2 = (obj1, obj2) => [obj1.foo, obj2.foo];

再来说它是如何检查受影响的。下面这个例子是一个实例不是解释哈,我们先理解表层,再来更深一层的理解如何实现:

const fn = (obj) => obj.arr.map((x) => x.num);
const memoizedFn = memoize(fn);

const result1 = memoizedFn({
  arr: [
    { num1text'hello' },
    { num2text'world' },
  ],
})

// 受影响的是 "arr[0].num", "arr[1].num" and "arr.length"

const result2 = memoizedFn({
  arr: [
    { num1text'hello' },
    { num2text'proxy' },
  ],
  extraProp: [123],
})

// 受影响的对象num的值并没有改变,于是:
console.log('result1 === result2 =>',result1 === result2) //true

这个神奇的效果是如何实现的呢?

你可以通过proxy-memoize(https://github.com/dai-shi/proxy-memoize)了解到其中使用跟踪和影响的比较是通过内部库proxy-compare(https://github.com/dai-shi/proxy-compare)完成的。

简单介绍一下 proxy-compare :这是一个从 react-tracked 中提取的库,只提供与代理的比较特性。(实际上,react-tracked v2将使用这个库作为依赖项。)

该库导出了两个主要功能: createDeepProxy 和 isDeepChanged

工作原理:

const state = { a1b2 };
const affected = new WeakMap();
const proxy = createDeepProxy(state, affected);
proxy.a // touch a property
isDeepChanged(state, { a1b22 }, affected) // is false
isDeepChanged(state, { a11b2 }, affected) // is true

状态可以是嵌套对象,只有当触及某个属性时,才会创建新的代理。当然如果你想深究createDeepProxy和isDeepChanged是如何实现的,你可以去看proxy-compare源码,我这里就不过多介绍了。

接下来介绍它配合React Context和React Redux这两个主要场景的使用,我这里放的是自己写的例子,当然你也可以看官网给出的例子都行。

Usage with React Context

如果将proxy-memoizeuseMemo 一起使用,我们将能够获得类似 react-tracked 的好处。

官方实例Sandbox:https://codesandbox.io/s/proxy-memoize-demo-vrnze

import memoize from 'proxy-memoize';

const MyContext = createContext();

const Component = () => {
  const [state, dispatch] = useContext(MyContext);
  const render = useMemo(() => memoize(({ firstName, lastName }) => (
    <div>
      First Name: {firstName}
      <input
        value={firstName}
        onChange={(event) =>
 {
          dispatch({ type: 'setFirstName', firstName: event.target.value });
        }}
      (Last Name: {lastName})
      />
    div>

  )), [dispatch]);
  return render(state);
};

const App = ({ children }) => (
  <MyContext.Provider value={useReducer(reducer, initialState)}>
    {children}
  MyContext.Provider>

);

当上下文发生变化时,组件将re-render。怎样才不会每次re-render呢,在这个例子中我们可以发现除非 firstName 没有改变,否则它返回memoized的react 元素树,re-render 将不会发生。这种行为不同于react-tracked,但还是有优化的。

Usage with React Context 实际上使用可能没有那么广泛,但是如果你们项目中有使用了许多 ReactContext 确实是可以用这个来优化。

接下来要说的我觉得是最广泛的应用场景(当然我是说的大部分项目)

Usage with React Redux

Instead of reselect:  https://github.com/reduxjs/reselect.

他两都是解决这个问题的:可以创建可记忆的(Memoized)、可组合的 selector 函数、可以用来高效地计算 Redux store 里的衍生数据。

如果你没用过proxy-memoize,你大概率是使用的reselect来编写选择器 selector 函数 ,这里我们来对比两个库,我这里举一个简单的例子,但是往往state结构是没有这么简单的,这里只是个演示。

其实在对比中你就可以知道memoize如何使用以及他的优化好处了。

为啥说代替reselect

相信看了下面的例子你能明白:

const fn = memoize((x:State) => ({ sum: x.a + x.b, diff: x.a - x.b }));

const fn1 = createSelector(
    [(state:State)=>state],
    (state) => {
        return {
            sum :state.a+state.b,
            diff:state.a-state.b
        }
    }
)

console.log("fn=>",(fn({ a1b2 })))//{sum: 3, diff: -1}
console.log("fn =>",(fn({ a1b2 ,c:3}) === fn({ a1b2 ,c:1})))//true
console.log("fn1=>",(fn1({ a1b2}) === fn1({ a1b2})))//false

当然我发现如果扩展成这样也是可以的(偶然的发现,可能确实是因为这个state太简单了吧),但是写起来就更复杂(尤其是层级深需要的值多的时候,并且当需要的是数组中属性值时,这就实现不了)

const selectA = (state:State)=>state.a
const selectB = (state:State)=>state.b
const selectSub = createSelector(
    selectA,
    selectB,
    (a,b) => {
        return {
            sum :a+b,
            diff:a-b
        }
    }
)
console.log("fn1=>",(fn1({ a1b2}) === fn1({ a1b2})))//true

那么久来个稍微复杂一点的例子吧

import { useDispatch, useSelector } from 'react-redux';
import memoize from 'proxy-memoize';

const Component = ({ id }) => {
  const dispatch = useDispatch();
  const selector = useMemo(() => memoize((state) => ({
    firstName: state.users[id].firstName,
    lastName: state.users[id].lastName,
  })), [id]);
  const { firstName, lastName } = useSelector(selector);
  return (
    <div>
      First Name: {firstName}
      <input
        value={firstName}
        onChange={(event) =>
 {
          dispatch({ type: 'setFirstName', firstName: event.target.value });
        }}
      />
      (Last Name: {lastName})
    div>

  );
};

同理我们也来对比一下:

/**
* 对比
*/

const fn = memoize((state:State) => state.users.map((user) => user.firstName))
const fn1 = createSelector(
[(state:State)=>state.users],
(users) => {
    return users.map((user)=>user.firstName)
})
console.log("fn =>",fn({count:1 ,text''users: [{firstName:"hh",lastName:"ll"}]}) === fn({count:1 ,text''users: [{firstName:"hh",lastName:"lllll"}]}))//true
console.log("fn1 =>",fn1({count:1 ,text'1'users: [{firstName:"hh",lastName:"ll"}]}) === fn({count:1 ,text''users: [{firstName:"hh",lastName:"ll"}]}))//false
  

可以发现,我们要取的值是在一个数组里,并且我们只要数组里的firstName这个属性,按reselect来的话我们要先拿到数组再去遍历拿到里面的值,所以检测变化就是检测这个数组变化咯。这时你就能发现memoize的简洁和优化

memoize((state) => state.users.map((user) => user.firstName))

它不会每次都创建,只有在用户长度更改或 firstName 中的一个更改时,才会重新计算这个值。

总结

这个其实是我工作中调研的一个库,这个知识无偿分享给大家,也不知道大家喜不喜欢这种硬核一点的知识分享哈,那如果你觉得写的还不错的话,点个赞再走吧💖

浏览 55
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报