工作半年期间调研的一个库,很有价值
共 5620字,需浏览 12分钟
·
2021-10-24 02:15
大家好,我是 HearLing。不知道大家还记得不记得我呀,许久未发文章,一直都在忙工作,这段时间就没有输出文章了,经过几个月的修炼,还是沉淀了许多知识。今后和 Chocolate 会多多更新文章,大家可以多多关注呀。
这篇文章是我工作中调研的一个库,使用 proxy-memoize 代替 reselect。可能许多小伙伴听到还没了解过这两个库,我还问了下 Chocolate,他也不知道哈哈哈。
在工作中我还调研了许多的库,在后期都会总结一下,我觉得沉淀这些知识很有帮助,今天分享给大家,以后说不定大家能用到呢,赶快收藏一波~
引言
在像 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' },
],
})
// 受影响的是 "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
(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 = { 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-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:
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({ 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 中的一个更改时,才会重新计算这个值。
总结
这个其实是我工作中调研的一个库,这个知识无偿分享给大家,也不知道大家喜不喜欢这种硬核一点的知识分享哈,那如果你觉得写的还不错的话,点个赞再走吧💖