最近在研究React
的源码,然后,我就悟了!
💡推荐阅读
💎 开门见山
请看👇的代码:你觉得可以按预期运行吗?
import { useMemo, useState } from 'react' function App() { const [count, setCount] = useState(0) const onClick = useMemo(() => { return () => setCount((count) => count + 1) }, []) useMemo(() => console.log(count), [count]) return ( <div className="App"> <button onClick={onClick}> count is {count} </button> </div> ); } export default App;
答案是完全可以!
💎 分析
🚗 用法分析
他们都接收两个参数,useXxx(callback, [...deps])
- 👉第一个参数
callback
是回调函数 - 👉第二个参数
deps
是依赖项
不同的是当依赖项发生改变时
- 🚆
useCallback
会重新创建回调函数,以保证每次调用都是最新值。并缓存这个函数 - 🚆
useEffect
回调函数会重新执行 - 🚆
useMemo
回调函数会重新执行,并缓存返回值。
根据useMemo
返回值的不同,可以模拟出不同的效果:
- 👉当返回值是个函数时,它
useCallback
和是完全等效的。 - 👉当没有返回值或者不管返回值时,它
useEffect
和部分功能是等效的
❝这是因为,它不会像
❞useEffect
一样,对返回值做处理。也就是说,它无法模拟unMounted
生命周期函数。
就是这么简单的原因,上面的代码会执行成功。
🚗 源码分析
这部分是选读,如果你对源码感兴趣,可以阅读这块。
🚆 useMemo
源码逻辑
- 👉注册
hook
状态 - 👉此时是
mounted
阶段,调用mountMemo
, - 👉将注册的
callback
和deps
拿出来 - 👉执行
callback
,并将执行结果和deps
缓存在当前hook
的状态上 - 👉
deps
发生改变,进入update
阶段,调用updateMemo
- 👉取出当前的
hook
状态,拿到callback
和deps
,再从当前hook
拿到上次的deps
- 👉比较前后两次的
deps
,如果一致,直接返回当前的状态值 - 👉否则重新执行
callback
,保持返回值,并将该值最为最新的状态值和deps
一起保存起来
function mountMemo<T>( nextCreate: () => T, deps: Array<mixed> | void | null, ): T { // 拿到当前的hook状态 const hook = mountWorkInProgressHook(); // 拿到当前的hook依赖项 const nextDeps = deps === undefined ? null : deps; if (shouldDoubleInvokeUserFnsInHooksDEV) { nextCreate(); } // 执行回调函数 const nextValue = nextCreate(); // 缓存回调函数返回值和依赖 hook.memoizedState = [nextValue, nextDeps]; // 返回返回值 return nextValue; } function updateMemo<T>( nextCreate: () => T, deps: Array<mixed> | void | null, ): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; // Assume these are defined. If they're not, areHookInputsEqual will warn. if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { // 如果前后依赖相同时,直接返回当前值 return prevState[0]; } } if (shouldDoubleInvokeUserFnsInHooksDEV) { nextCreate(); } // 否则重新计算赋值,并返回最新值 const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
🚆 useCallback
源码逻辑
- 👉注册
hook
状态 - 👉此时是
mounted
阶段,调用mountCallback
, - 👉将注册的
callback
和deps
拿出来 - 👉将
callback
和deps
缓存在当前hook
的状态上 - 👉
deps
发生改变,进入update
阶段,调用updateCallback
- 👉取出当前的
hook
状态,拿到callback
和deps
,再从当前hook
拿到上次的deps
- 👉比较前后两次的
deps
,如果一致,直接返回当前的状态值 - 👉否则重新将
callback
做为最新的状态值和deps
一起保存起来
function mountCallback<T>( callback: T, deps: Array<mixed> | void | null ): T { // 获取当前hook状态 const hook = mountWorkInProgressHook(); // 获取当前hook依赖项 const nextDeps = deps === undefined ? null : deps; // 缓存回调函数和依赖 hook.memoizedState = [callback, nextDeps]; // 返回回调函数 return callback; } function updateCallback<T>( callback: T, deps: Array<mixed> | void | null ): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { // 如果依赖项相同时,直接返回当前值 return prevState[0]; } } // 否则重新赋值,并返回最新值 hook.memoizedState = [callback, nextDeps]; return callback; }
从源码上看,useCallback
和useMemo
的实现十分类似,唯一的不同之处是:useMemo
在依赖项发生变化时会缓存回调函数的返回值。
💎 总结
useCallback
和useMemo
都是缓存中间状态,
不同的是useMemo
可以缓存任何类型的值,useCallback
仅仅缓存函数。所以开头的例子可以按预期运行。
好了,今天的分享比较简单,但是希望可以帮你理解地更深一点。
下篇我们继续聊hook
。