前言
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
会有大量无用渲染的问题,要想减少无用渲染,需要注意的点有点多,比如需要做到以下几点
-
拆分不同粒度的
Context
const 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
机制来优化性能。
- 拆分