react hooks

更新于 阅读 1

react hooks是函数式组件的一个特性,每次render时会调用renderWithHooks,所以挂载和更新阶段时hooks都会执行,react使用一个链表来维护hooks,这样更新时可以根据顺序找到对应的hook,也就是hook不要放在条件和循环中的原因,具体为什么放在后面。

组件在挂载阶段和更新所属的ReactCurrentDispatcher.current是不同的,挂在使用HooksDispatcherOnMount,更新使用HooksDispatcherOnUpdate

export function renderWithHooks<Props, SecondArg>( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ): any { // 设置dispatcher,hooks执行时使用 ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; // 执行函数组件 let children = Component(props, secondArg); ReactCurrentDispatcher.current = ContextOnlyDispatcher; const didRenderTooFewHooks = currentHook !== null && currentHook.next !== null; if (didRenderTooFewHooks) { throw new Error( 'Rendered fewer hooks than expected. This may be caused by an accidental ' + 'early return statement.', ); } return children; }

组件执行完之后ReactCurrentDispatcher.current会被设置成ContextOnlyDispatcher,如果hooks嵌套使用就会报错,例如useEffect, 可以看到代码直接抛出了错误。

export const ContextOnlyDispatcher: Dispatcher = { // ... useEffect: throwInvalidHookError, // ... }; function throwInvalidHookError() { throw new Error( 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + ' one of the following reasons:\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + '2. You might be breaking the Rules of Hooks\n' + '3. You might have more than one copy of React in the same app\n' + 'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.', ); }

HooksDispatcherOnMount和HooksDispatcherOnUpdate的代码如下,可以看到useEffect是调用的不同的函数,内部的实现也不相同

const HooksDispatcherOnMount: Dispatcher = { useCallback: mountCallback, useContext: readContext, useEffect: mountEffect, // ... }; const HooksDispatcherOnUpdate: Dispatcher = { useCallback: updateCallback, useContext: readContext, useEffect: updateEffect, //... };

hooks链表

每次函数执行都会生成新的hook节点,

export type Hook = { memoizedState: any, baseState: any, // baseQueue: Update<any, any> | null, queue: any, // 更新队列 next: Hook | null, // 下一个hook节点 |};

产生的hook链表放在currentlyRenderingFiber.memoizedState上,使用next指向下一个节点。

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 // 第一次hook节点 currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { // Append to the end of the list // workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }

由于hook是按照编写的顺讯执行的,所以从前往后就会形成一个链接,workInProgressHook始终指向最新的hook,比如如下组件,

function Article() { // hook A const [hookA, setHookA] = useState("A"); // hook B useEffect(() => { console.log("B"); }, []); return <div>{hookA}</div> }

上面组件执行的hook链接为

currentlyRenderingFiber.memoizedState = hookA ---> hookB; ^ workInProgressHook

组件挂载

组件第一次执行的,hook执行时链表不存在,所以会创建hook然后根据顺序形成hook链表,就是调用的上面的 mountWorkInProgressHook 方法,第一个hook创建时赋值给fiber的memoizedState属性,后面的hook通过next依次连接起来,同时更新workInProgressHook。 以 useState 和 useEffect 举例。

function mountState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { // 创建hook,并形成链表 const hook = mountWorkInProgressHook(); // ... // 返回 return [hook.memoizedState, dispatch]; } // mountEffect调用mountEffectImpl函数 function mountEffectImpl(fiberFlags, hookFlags, create, deps): void { const hook = mountWorkInProgressHook(); // ... }

组件更新

React组件在更新时由于fiber链表有两条current、workInProgress,所以对应的hook链表也有两条 currentHook 、workInProgressHook。currentHook指向上一次更新时的hook,workInProgressHook 是根据 currentHook的数据来构建的。

current tree
           current.memoizedState = hookA -> hookB -> hookC
                                              ^
                                         currentHook 
                                              |
workInProgress tree                           |
    workInProgress.memoizedState = hookA -> hookB -> hookC    
                                              ^
                                     workInProgressHook
function updateWorkInProgressHook(): Hook { let nextCurrentHook: null | Hook; if (currentHook === null) { const current = currentlyRenderingFiber.alternate; if (current !== null) { // 组件刚开始渲染时从current上获取第一个hook节点 nextCurrentHook = current.memoizedState; } else { nextCurrentHook = null; } } else { // currentHook 不是null的时候指向下一个hook nextCurrentHook = currentHook.next; } let nextWorkInProgressHook: null | Hook; if (workInProgressHook === null) { // 第一次渲染的时候 nextWorkInProgressHook 指向fiber的memoizedState nextWorkInProgressHook = currentlyRenderingFiber.memoizedState; } else { // 这里表示链表已经存在了,nextWorkInProgressHook指向链表的下一个元素 nextWorkInProgressHook = workInProgressHook.next; } if (nextWorkInProgressHook !== null) { // There's already a work-in-progress. Reuse it. workInProgressHook = nextWorkInProgressHook; nextWorkInProgressHook = workInProgressHook.next; currentHook = nextCurrentHook; } else { if (nextCurrentHook === null) { // PS: 表示要构建的hook在以前的hook链表上找不到对应的元素,也就是说组件本次渲染的hook比以前多, 所以不要讲hook写在判断、循环里面 throw new Error('Rendered more hooks than during the previous render.'); } // currentHook指向心的hook在上一次渲染时的链表中对应的元素 currentHook = nextCurrentHook; // 构建新的hook const newHook: Hook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, baseQueue: currentHook.baseQueue, queue: currentHook.queue, next: null, }; // 将workInProgressHook指向新的hook元素 if (workInProgressHook === null) { // 第一次渲染时将newHook赋值给fiber.memoizedState currentlyRenderingFiber.memoizedState = workInProgressHook = newHook; } else { workInProgressHook = workInProgressHook.next = newHook; } } return workInProgressHook; }