阅读 146

proxy-memoize代替reselect

引言

在像 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: [     { num: 1, text: 'hello' },     { num: 2, text: 'world' },   ], }) // 受影响的是 "arr[0].num", "arr[1].num" and "arr.length" const result2 = memoizedFn({   arr: [     { num: 1, text: 'hello' },     { num: 2, text: 'proxy' },   ],   extraProp: [1, 2, 3], }) // 受影响的对象num的值并没有改变,于是: console.log('result1 === result2 =>',result1 === result2) //true 复制代码

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

你可以通过proxy-memoize了解到其中使用跟踪和影响的比较是通过内部库proxy-compare完成的。

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

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

工作原理:

const state = { a: 1, b: 2 }; const affected = new WeakMap(); const proxy = createDeepProxy(state, affected); proxy.a // touch a property isDeepChanged(state, { a: 1, b: 22 }, affected) // is false isDeepChanged(state, { a: 11, b: 2 }, affected) // is true 复制代码

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

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

Usage with React Context

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

官方实例Sandbox:codesandbox.io/s/proxy-mem…

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.

他两都是解决这个问题的:可以创建可记忆的(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({ a: 1, b: 2 })))//{sum: 3, diff: -1} console.log("fn =>",(fn({ a: 1, b: 2 ,c:3}) === fn({ a: 1, b: 2 ,c:1})))//true console.log("fn1=>",(fn1({ a: 1, b: 2}) === fn1({ a: 1, b: 2})))//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({ a: 1, b: 2}) === fn1({ a: 1, b: 2})))//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 中的一个更改时,才会重新计算这个值。

总结

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


作者:HearLing
链接:https://juejin.cn/post/7021438027792646174


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐