引言
在像 React 这样的前端框架中,对象不变性非常重要。但其实它本身并不支持强制不变性。那这个库利用了 Proxy
和 WeakMap
,并提供了记忆功能。仅当参数(对象)的使用部分发生变化时,记忆函数才会重新计算原始函数。
通过引言我们已经知道了它的优点,那么你可能会好奇他是如何实现的,那么你可以看看下面这个介绍,如果你只关心它是如何使用你也可以跳过这一小节:
如何工作
当它(重新)计算一个函数时,它将用代理(递归地,根据需要)包装一个输入对象并调用该函数。当它完成时,它将检查什么是受影响的。这个受影响其实是在函数调用期间访问的输入对象的路径列表。
当它下一次接收到一个新的输入对象时,它将检查受影响路径中的值是否被更改。如果是被更改,那么它将重新计算函数。否则,它将返回一个缓存结果。默认缓存大小为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' }, ], })
const result2 = memoizedFn({ arr: [ { num: 1, text: 'hello' }, { num: 2, text: 'proxy' }, ], extraProp: [1, 2, 3], })
console.log('result1 === result2 =>',result1 === result2)
|
这个神奇的效果是如何实现的呢?
你可以通过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 isDeepChanged(state, { a: 1, b: 22 }, affected) isDeepChanged(state, { a: 11, b: 2 }, affected)
|
状态可以是嵌套对象,只有当触及某个属性时,才会创建新的代理。当然如果你想深究createDeepProxy和isDeepChanged是如何实现的,你可以去看proxy-compare源码,我这里就不过多介绍了。
接下来介绍它配合React Context和React Redux这两个主要场景的使用,我这里放的是自己写的例子,当然你也可以看官网给出的例子都行。
Usage with React Context
如果将proxy-memoize
与 useMemo 一起使用,我们将能够获得类似 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.
他两都是解决这个问题的:可以创建可记忆的(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 }))) console.log("fn =>",(fn({ a: 1, b: 2 ,c:3}) === fn({ a: 1, b: 2 ,c:1}))) console.log("fn1=>",(fn1({ a: 1, b: 2}) === fn1({ a: 1, b: 2})))
|
当然我发现如果扩展成这样也是可以的(偶然的发现,可能确实是因为这个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})))
|
那么久来个稍微复杂一点的例子吧
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"}]})) console.log("fn1 =>",fn1({count:1 ,text: '1', users: [{firstName:"hh",lastName:"ll"}]}) === fn({count:1 ,text: '', users: [{firstName:"hh",lastName:"ll"}]}))
|
可以发现,我们要取的值是在一个数组里,并且我们只要数组里的firstName这个属性,按reselect来的话我们要先拿到数组再去遍历拿到里面的值,所以检测变化就是检测这个数组变化咯。这时你就能发现memoize的简洁和优化
memoize((state) => state.users.map((user) => user.firstName))
|
它不会每次都创建,只有在用户长度更改或 firstName 中的一个更改时,才会重新计算这个值。
总结
这个其实是我工作中调研的一个库,这个知识无偿分享给大家,也不知道大家喜不喜欢这种硬核一点的知识分享哈,那如果你觉得写的还不错的话,点个赞再走吧💖