前言
aHooks 是阿里巴巴开源的一个React Hooks库,其中有很多hooks实现得很巧妙,一起来看看吧,本文的主角是usePersistFn。
这里是usePersistFn文档
usePersistFn 解决了什么问题?
在某些场景中,你可能会需要用 useCallback 记住一个回调,但由于内部函数必须经常重新创建,记忆效果不是很好,导致子组件重复 render。对于超级复杂的子组件,重新渲染会对性能造成影响。通过 usePersistFn,可以保证函数地址永远不会变化。
写个具体Demo描述下上述场景
function Child(props) {
console.log("child render");
return <button onClick={props.showCount}>showCount</button>;
}
const ChildMemo = memo(Child);
function App() {
const [count, setCount] = useState(0);
const showCount = useCallback(() => {
console.log("showCount");
}, []);
return (
<div className="App">
<button onClick={() => setCount((val) => val + 1)}>触发父组件渲染</button>
<h2>count:{count}</h2>
<ChildMemo showCount={showCount} />
</div>
);
}
为了减少Child组件的渲染,我们使用memo配合useCallback,同时useCallback第二个参数传空数组,所以showCount只会被创建一次,这样当父组件重渲染时,ChildMemo不会重新渲染,达到了减少不必要渲染次数的目的。
但是当我们在showCount中想访问count变量时
const showCount = useCallback(() => {
+ console.log(count);
}, []);
我们会发现count的值不会更新,其实这是 react hooks 底层实现机制决定的,可以简单描述为闭包陷阱,具体就不展开讲了,解决办法是将count加到依赖项内。
const showCount = useCallback(() => {
+ console.log(count);
}, [count]);
问题解决了,此时可以访问到最新的count,但每次count变化时,都会重新创建showCount产生一个新的函数,从而导致memo失效。
针对这个问题,其实react官方给了个临时解决方案,那就是使用useRef 。
function Child(props) {
console.log("child render");
return <button onClick={props.showCount}>showCount</button>;
}
const ChildMemo = memo(Child);
function App() {
const [count, setCount] = useState(0);
const countRef = useRef();
useEffect(() => {
countRef.current = count;
}, [count]);
const showCount = useCallback(() => {
console.log(countRef.current);
}, [countRef]);
return (
<div className="App">
<button onClick={() => setCount((val) => val + 1)}>触发父组件渲染</button>
<h2>count:{count}</h2>
<ChildMemo showCount={showCount} />
</div>
);
}
我们可以抽个自定义hooks,这也是react官方使用useRef的方案
function usePersistFn(fn, deps) {
const fnRef = useRef();
useEffect(() => {
fnRef.current = fn;
}, [fn, ...deps]);
return useCallback(() => {
return fnRef.current();
}, [fnRef]);
}
这样使用
function App() {
const [count, setCount] = useState(0);
const showCountWithPersist = usePersistFn(() => {
console.log(count);
}, [count]);
return (
<div className="App">
<button onClick={() => setCount((val) => val + 1)}>触发父组件渲染</button>
<h2>count:{count}</h2>
<ChildMemo showCount={showCountWithPersist} />
</div>
);
}
本节代码 codesandbox.io/s/rough-daw…
但是每次使用都需要传递依赖项,比较麻烦,我们可以优化下,不需要传入依赖项。
我们传入依赖项的根本原因是因为希望依赖变化了,需要重新将 fn 赋值给fnRef.current
useEffect(() => {
fnRef.current = fn;
}, [fn, ...deps]);
那我们只要不检查依赖是否变化,在每次函数执行时,无论依赖是否变化,我们都重新将 fn 赋值给fnRef.current,那就不需要使用者传递依赖了
function usePersistFn(fn) {
const fnRef = useRef();
+ fnRef.current = fn; // 重点是这一行,去掉了useEffect
return useCallback(() => {
return fnRef.current();
}, [fnRef]);
}
本节代码 codesandbox.io/s/unruffled…
其实阿里开源的 react hooks 工具库 ahooks中的usePersistFn(github.com/alibaba/hoo…) 就是这种思路实现不需要传递依赖项的。
唯一不同的就是,它通过使用useRef代替了useCallback,但是最终达到的效果都是一样的,即都保证了返回的引用在usePersistFn被多次调用时都是相同的。
- ahooks (ahooks.js.org/zh-CN/hooks…)
function usePersistFn(fn) {
const fnRef = useRef(fn);
fnRef.current = fn;
const persistFn = useRef();
if (!persistFn.current) {
persistFn.current = function (...args) {
return fnRef.current.apply(this, args);
};
}
return persistFn.crrent;
}
首先创建persistFn的ref,然后第一次渲染时,!persistFn.current 会返回true,则会将匿名函数赋值给persistFn.current 。
至于return fnRef.current.apply(this, args); 使用apply只是为了保证this指向,匿名函数也可以换成箭头函数,就不需要apply,比如改成这样:
persistFn.current = (...args) => fnRef.current(args);
来自React官方的建议
我们建议 在 context 中传递
dispatch,而不是在 props(属性) 中单独回调。为了完整起见,并作为escape hatch(逃生舱),此处仅提及以下方法。还要注意,此模式可能会在 并发模式 中导致问题。我们计划在将来提供更符合人们习惯的替代方案,但是目前最安全的解决方案是,如果某个值依赖于更改,则总是使回调无效。
主要是以下几点
- 当面临props需要在
子/孙组件一层层传递时,请使用Context配合dispatch
但问题是现在直接使用Context会有大量无用渲染的问题,要想减少无用渲染,需要注意的点有点多,比如需要做到以下几点
-
拆分不同粒度的
Contextconst App = () => { // ... return ( <ContextA.Provider value={valueA}> <ContextB.Provider value={valueB}> <ContextC.Provider value={valueC}> ... </ContextC.Provider> </ContextB.Provider> </ContextA.Provider> ); }; -
关注
Context的顺序,让不变的放在在外层,多变的在内层。
总结
- 谨慎使用
useRef的方式来达到闭包穿透的效果,在React18的并发模式(Concurrent Mode)可能会出现非预期的结果。 - 不建议使用
Context,若非要使用,使用时需要注意避免产生额外渲染行为,需要保持以下原则- 拆分
Context - 关注
Context的顺序,让不变的放在在外层,多变的在内层。 - 在当前
React Context缺乏context selectors这种机制的情况下,建议使用状态管理库代替Context,毕竟大部分状态管理库都会带有selectors机制来优化性能。
- 拆分