详解react中useCallback内部是如何实现的
作者:AKclown
示例demo与debug
新建了一个react项目,将APP.tsx改写成如下代码
import { useCallback, useState } from 'react'; function App() { const [num, updateNum] = useState(0); const TestCallback = useCallback(() =>{ console.log('num: ', num); },[]); return ( <div className="App"> <p onClick={() => { updateNum(num => num + 1); updateNum(num => num + 1); updateNum(num => num + 1); }}>{num}</p> <p onClick={TestCallback}>打印</p> </div> ); } export default App;
在浏览器的source
设置断点,熟悉一遍useCallback的调用流程。(由于.gif过大,这里就不上git了,自行调试)
源码解析
useCallback的整体流程框架
在react中mount阶段和update阶段进入到同一个useCallback方法里。但resolveDispatcher找到的dispatch对象mount
和update
会不同,最终导致在mount阶段调用mountCallback
而update阶段调用的是updateCallback
。
下面为调用useCallback
方法触发的行为
function useCallback(callback, deps) { var dispatcher = resolveDispatcher(); return dispatcher.useCallback(callback, deps); }
下面来看看resolveDispatcher
是如何获取到dispatch的
function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current; ... return ((dispatcher: any): Dispatcher); }
ReactCurrentDispatcher.current
会在renderWithHooks
方法中进行所处阶段判断并且赋值
。如果current === null || current.memoizedState === null
为true表示在mount阶段
反正为update阶段
function renderWithHooks<Props, SecondArg>(...) { ... ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; } // mount阶段调用的dispatch const HooksDispatcherOnMount: Dispatcher = { ... useCallback: mountCallback, }; // update阶段调用的dispatch const HooksDispatcherOnUpdate: Dispatcher = { ... useCallback: updateCallback, };
从上面的代码分析可以知道在mounted
阶段调用的是mountCallback
在update
阶段调用updateCallback
Hook
一个函数式组件链路: fiber(FunctionComponent) => Hook(保存数据状态) => Queue(更新的队列结构) => update(更新的数据)
在后续需要使用到Hook这个结构,那么先来看一下Hook是数据结构是怎么样的,以及属性的作用是什么?
- memoizedState 存放的是Hook对应的state
- next链接到下一个Hook,从而形成一个
无环单向链表
- queue存储同一个hook更新的多个update对象,数据结构为
环状单向链表
// 组件对应的fiber对象 const fiber = { // 保存该FunctionComponent对应的Hooks链表 memoizedState: hook, ... }; const hook: Hook = { // 1. memoizedState 存放的是Hook对应的state memoizedState: null, // 2. next链接到下一个Hook,从而形成一个`无环单向链表` queue: null, // 3. queue存储同一个hook更新的多个update对象,数据结构为`环状单向链表` next: null, ... };
fiber与Hooks的关系(懒得画图了,引用了Understanding the Closure Trap of React Hooks
)
mount阶段
分析mountCallback
的实现
- 通过
mountWorkInProgressHook
获取到对应的Hook对象 - 判断条件deps是否为undefined
- 将
回调函数
和判断条件
存入到hook.memoizedState
- 返回传入的回调函数
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; hook.memoizedState = [callback, nextDeps]; return callback; }
mountWorkInProgressHook
的实现,创建初始化Hook
对象,并且将该Hook对象
保存在workInProgressHook
链路中. workInProgressHook
表示正在执行的hook
function mountWorkInProgressHook(): Hook { const hook: Hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null, }; if (workInProgressHook === null) { // This is the first hook in the list currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { // Append to the end of the list workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
在组件render
时,每当遇到下一个Hook
,通过移动workInProgressHook
的指针来获取到对应的Hook
PS: 只要每次组件render
时useState
的调用顺序及数量保持一致,那么始终可以通过workInProgressHook
找到当前useState
对应的hook
对象
// fiber.memoizedState标识第一个Hook workInProgressHook = fiber.memoizedState; // 在组件`render`时,遇到下一个hook时 workInProgressHook = workInProgressHook.next; ....
update阶段
分析updateCallback
的实现
- 通过
updateWorkInProgressHook
获取到当前的Hook对象 hook.memoizedState
获取到上一次缓存的state
。假设这是第一次update
那么其值就是mount阶段
保存的[callback, nextDeps]
数据- 如果依赖条件不为空,使用
areHookInputsEqual
判断依赖项是否更改。只会遍历数组第一层数据比较
不会做深层比较。如果依赖项没变化,返回原本缓存的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; }
依赖比较areHookInputsEqual
的方法实现
function areHookInputsEqual( nextDeps: Array<mixed>, prevDeps: Array<mixed> | null, ): boolean { ... // $FlowFixMe[incompatible-use] found when upgrading Flow for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { // $FlowFixMe[incompatible-use] found when upgrading Flow if (is(nextDeps[i], prevDeps[i])) { continue; } return false; } return true; }
总结
在React中会使用闭包机制
来处理上文的callback
回调函数。当包含useCallback
组件被渲染时,React 会为该特定渲染周期创建一个闭包。闭包是一个封装的作用域,其中包含渲染时位于作用域内的变量、函数和其他引用
。
因此deps我们传入的是空数组,其回调函数callback一直引用的状态始终是初始状态,无法获取最新状态
。缓存的回调函数可以访问最初调用时范围内的状态和道具
插件推荐
阅读源码可以通过使用Bookmarks
快速标记代码位置,实现快速条件
到此这篇关于详解react中useCallback内部是如何实现的的文章就介绍到这了,更多相关react useCallback内部实现内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!