代码以React v19.1.0为例,https://github.com/facebook/react/tree/v19.1.0
上一篇我们讲了React构建fiber树的整体流程,即循环调用performUnitOfWork,里面穿插着beginWork和completeWork的调用,并且最后给出了简洁的伪代码实现。
那么这一篇呢,我们先看一下beginWork前半部分的逻辑。因为beginWork它的主要工作是渲染组件(此处的渲染即,我们平时所说的渲染,对于函数组件来说就是执行),并且根据返回的jsx,生成子节点。说到了渲染组件,我觉得这肯定是很多人最感兴趣的了。对于这么重量级的部分,我认为有必要牺牲一下学习的连贯性,把他吃透,这样反而更有利于加深我们对React的理解。
这一篇篇幅会很长,我会从beginWork入手,讲到React的hooks实现,并且以useReducer为例,讲一下React Hooks实现的原理,讲完了之后,下一篇再回来继续看beginWork的部分。
beginWork里面的逻辑 beginWork 先看beginWork代码,依旧是去掉开发环境下的代码,和Profiler相关的代码。然后它里面有个switch case的逻辑,分支太多了,我就保留几个意思意思吧,想了解更多可以自己去看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 function beginWork ( current: Fiber | null , workInProgress: Fiber, renderLanes: Lanes, ): Fiber | null { if (current !== null ) { const oldProps = current.memoizedProps ; const newProps = workInProgress.pendingProps ; if ( oldProps !== newProps || hasLegacyContextChanged () || (__DEV__ ? workInProgress.type !== current.type : false ) ) { didReceiveUpdate = true ; } else { const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext ( current, renderLanes, ); if ( !hasScheduledUpdateOrContext && (workInProgress.flags & DidCapture ) === NoFlags ) { didReceiveUpdate = false ; return attemptEarlyBailoutIfNoScheduledUpdate ( current, workInProgress, renderLanes, ); } if ((current.flags & ForceUpdateForLegacySuspense ) !== NoFlags ) { didReceiveUpdate = true ; } else { didReceiveUpdate = false ; } } } else { didReceiveUpdate = false ; if (getIsHydrating () && isForkedChild (workInProgress)) { const slotIndex = workInProgress.index ; const numberOfForks = getForksAtLevel (workInProgress); pushTreeId (workInProgress, numberOfForks, slotIndex); } } workInProgress.lanes = NoLanes ; switch (workInProgress.tag ) { case LazyComponent : { const elementType = workInProgress.elementType ; return mountLazyComponent ( current, workInProgress, elementType, renderLanes, ); } case FunctionComponent : { const Component = workInProgress.type ; const unresolvedProps = workInProgress.pendingProps ; const resolvedProps = disableDefaultPropsExceptForClasses || workInProgress.elementType === Component ? unresolvedProps : resolveDefaultPropsOnNonClassComponent (Component , unresolvedProps); return updateFunctionComponent ( current, workInProgress, Component , resolvedProps, renderLanes, ); } case ClassComponent : { const Component = workInProgress.type ; const unresolvedProps = workInProgress.pendingProps ; const resolvedProps = resolveClassComponentProps ( Component , unresolvedProps, workInProgress.elementType === Component , ); return updateClassComponent ( current, workInProgress, Component , resolvedProps, renderLanes, ); } case HostRoot : return updateHostRoot (current, workInProgress, renderLanes); case HostComponent : return updateHostComponent (current, workInProgress, renderLanes); case HostText : return updateHostText (current, workInProgress); case Throw : { throw workInProgress.pendingProps ; } }
删减了很多,但是依旧是很长。前面一大段不重要啊,就是判断这个组件是否需要重新渲染的。如果不需要重新渲染,就跳过了。值得提一嘴的就是前面那个current!==null
,就是判断是初次渲染(mount),还是重新渲染(update)。因为初次渲染是没有current节点的,这一点前面我们讲current tree和workInProgress tree关系的时候,没有详细说,所以这次特别提一下。
直接看下面switch case那一段,就是根据fiber node的各种tag,去执行不同的操作。比如,对于函数组件,就去执行updateFunctionComponent这个方法。我们给他传入的入参,分别是current节点,workInProgress节点,函数组件本身(没错,对于函数组件,fiber节点的type字段就是这个函数本身,你可以随便本地起个React项目打断点看一下),组件的props,和这个组件的渲染优先级。
updateFunctionComponent 看看对于函数组件干了什么,依旧是精简后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 function updateFunctionComponent ( current: null | Fiber, workInProgress: Fiber, Component: any, nextProps: any, renderLanes: Lanes, ) { let context; if (!disableLegacyContext && !disableLegacyContextForFunctionComponents) { const unmaskedContext = getUnmaskedContext (workInProgress, Component , true ); context = getMaskedContext (workInProgress, unmaskedContext); } let nextChildren; let hasId; prepareToReadContext (workInProgress, renderLanes); nextChildren = renderWithHooks ( current, workInProgress, Component , nextProps, context, renderLanes, ); hasId = checkDidRenderIdHook (); if (current !== null && !didReceiveUpdate) { bailoutHooks (current, workInProgress, renderLanes); return bailoutOnAlreadyFinishedWork (current, workInProgress, renderLanes); } if (getIsHydrating () && hasId) { pushMaterializedTreeId (workInProgress); } workInProgress.flags |= PerformedWork ; reconcileChildren (current, workInProgress, nextChildren, renderLanes); return workInProgress.child ; }
前面几行依旧不重要,是处理context 相关的。其实这个方法里最重要的就两个函数调用,一个是renderWithHooks,一个是reconcileChildren。它们分别完成了渲染组件,和生成子节点的任务。关于reconcileChildren,我们要很久之后再见了。接下来很大的篇幅,包括后面几篇,我都会讲renderWithHooks,和hooks的具体实现相关的内容。
renderWithHooks renderWithHooks的简要逻辑如下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 export function renderWithHooks<Props , SecondArg >( current : Fiber | null , workInProgress : Fiber , Component : (p: Props, arg: SecondArg ) => any, props : Props , secondArg : SecondArg , nextRenderLanes : Lanes , ): any { renderLanes = nextRenderLanes; currentlyRenderingFiber = workInProgress; workInProgress.memoizedState = null ; workInProgress.updateQueue = null ; workInProgress.lanes = NoLanes ; ReactSharedInternals .H = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate ; let children = Component (props, secondArg); if (didScheduleRenderPhaseUpdateDuringThisPass) { children = renderWithHooksAgain ( workInProgress, Component , props, secondArg, ); } finishRenderingHooks (current, workInProgress, Component ); return children; }
简单总结,就是首先把这个fiber节点的一些字段都清空,然后根据是mount还是update,分别给所有的hooks赋值上相应的实现。
然后就去执行这个函数,这个其实我们之前平时写React就知道,函数组件的渲染就是执行它本身嘛。你在函数组件里面写的代码,全会在此时被执行。
然后如果组件渲染过程中,又产生了新的更新,比如在函数体里面setState(虽然这是不对的),那么react会再重新render一遍,直到组件稳定了,不会在渲染过程中产生新的更新,或是重复调用太多次而报错。
最后,还原一下上下文,等着下一个组件来渲染。
我们可以先看一下给hooks添加上实现的那一段,也就是
1 2 3 4 ReactSharedInternals .H = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate ;
我们直接去packages/react目录下搜索一下hooks的实现,比如useReducer,能看到它的实现其实是resolveDispatcher().useReducer(reducer, initialArg, init)
。而这个resolveDispatcher
,其实就是直接返回了ReactSharedInternals.H
。这个变量是定义在shared包里的,供React各个子包一起使用的。
而HooksDispatcherOnMount
和HooksDispatcherOnUpdate
,分别就是mount过程中和update过程中,全部hooks的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 const HooksDispatcherOnMount : Dispatcher = { readContext, use, useCallback : mountCallback, useContext : readContext, useEffect : mountEffect, useImperativeHandle : mountImperativeHandle, useLayoutEffect : mountLayoutEffect, useInsertionEffect : mountInsertionEffect, useMemo : mountMemo, useReducer : mountReducer, useRef : mountRef, useState : mountState, useDebugValue : mountDebugValue, useDeferredValue : mountDeferredValue, useTransition : mountTransition, useSyncExternalStore : mountSyncExternalStore, useId : mountId, useHostTransitionStatus : useHostTransitionStatus, useFormState : mountActionState, useActionState : mountActionState, useOptimistic : mountOptimistic, useMemoCache, useCacheRefresh : mountRefresh, };const HooksDispatcherOnUpdate : Dispatcher = { readContext, use, useCallback : updateCallback, useContext : readContext, useEffect : updateEffect, useImperativeHandle : updateImperativeHandle, useInsertionEffect : updateInsertionEffect, useLayoutEffect : updateLayoutEffect, useMemo : updateMemo, useReducer : updateReducer, useRef : updateRef, useState : updateState, useDebugValue : updateDebugValue, useDeferredValue : updateDeferredValue, useTransition : updateTransition, useSyncExternalStore : updateSyncExternalStore, useId : updateId, useHostTransitionStatus : useHostTransitionStatus, useFormState : updateActionState, useActionState : updateActionState, useOptimistic : updateOptimistic, useMemoCache, useCacheRefresh : updateRefresh, };
Hooks的具体实现 我们在上面renderWithHooks里面看到了let children = Component(props, secondArg);
这行代码,它会渲染我们的函数组件,即执行函数本身。执行的时候,·你在函数组件里面写的各种hooks,就被替换成上面hooks的具体实现来执行。
那么我们这一篇,就以useReducer为例,来看一看Hooks的原理和具体实现,看看它在组件渲染的过程中做了什么。选useReducer是因为,最常用的Hooks是useState,而useState是useReducer的一种特殊场景,相信看到这里的也都熟悉useReducer。所以我们选择更具通用性的useReducer来研究(而且useState完全是通过调用useReducer来实现的)。
mountReducer 当函数组件首次渲染,即mount的时候,对于组件里的useReducer hook,执行的就是mountReducer。简单来讲,就是把hook挂到fiber上面
我们来看看mountReducer具体是怎么实现的
1 2 3 4 5 6 7 8 9 function mountReducer<S, I, A>( reducer : (S, A ) => S, initialArg : I, init?: I => S, ): [S, Dispatch <A>] { const hook = mountWorkInProgressHook (); return [hook.memoizedState , dispatch]; }
首先第一行就是一个看名字就很重要的函数调用,mountWorkInProgressHook
。那我们不得不再去看看它的实现了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function mountWorkInProgressHook ( ): Hook { const hook : Hook = { memoizedState : null , baseState : null , baseQueue : null , queue : null , next : null , }; if (workInProgressHook === null ) { currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
还好它并不长,他只是定义了一个新的hook对象,每个字段的含义我都在注释里写了。其中baseState和baseQueue如果看不懂的话可以先略过,这个和React的并发更新有关,后面会细讲,不用现在死磕。
然后workInProgressHook是一个全局对象,是一个链表。它会在合适的时候被清空。如果它是null,那就代表当前这个Fiber节点上还没有hook,那么我们就要把hook挂到Fiber节点的memoizedState
字段上。
注意,这里的memoizedState和上面hook对象里的memoizedState,并没有关系,只是同名而已。一个代表Fiber的hooks链表,一个代表hook的state。
我们以一个很简单的react组件举例子,它用了两个useReducer。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import React , { useReducer } from 'react' ;function countReducer (state, action ) { switch (action.type ) { case 'inc' : return state + 1 ; case 'dec' : return state - 1 ; default : return state; } }function toggleReducer (state ) { return !state; }export default function DoubleReducerDemo ( ) { const [count, dispatchCount] = useReducer (countReducer, 0 ); const [show, toggleShow] = useReducer (toggleReducer, true ); return ( <div > <button onClick ={() => toggleShow()}>Toggle Counter</button > {show && ( <div > <p > Count: {count}</p > <button onClick ={() => dispatchCount({ type: 'inc' })}>+1</button > <button onClick ={() => dispatchCount({ type: 'dec' })}>-1</button > </div > )} </div > ); }
那么当它插入第一个hook的时候,结构是这样的
如果workInProgressHook不是null,则说明这个Fiber节点上面已经有hooks了。workInProgressHook = workInProgressHook.next = hook;
相当于workInProgressHook.next = hook;workInProgressHook = workInProgressHook.next;
这样两句话,意思就是把刚创建好的hook插入到链表的尾部,并且更新链表的尾节点,便于下一次插入。
也就是说,fiber里面的hooks的结构是这样的:
最后,这个方法返回了刚才新创建好的这个hook
然后再回去看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function mountReducer<S, I, A>( reducer : (S, A ) => S, initialArg : I, init?: I => S, ): [S, Dispatch <A>] { const hook = mountWorkInProgressHook (); let initialState; if (init !== undefined ) { initialState = init (initialArg); if (shouldDoubleInvokeUserFnsInHooksDEV) { setIsStrictModeForDevtools (true ); try { init (initialArg); } finally { setIsStrictModeForDevtools (false ); } } } else { initialState = ((initialArg : any): S); } hook.memoizedState = hook.baseState = initialState; const queue : UpdateQueue <S, A> = { pending : null , lanes : NoLanes , dispatch : null , lastRenderedReducer : reducer, lastRenderedState : (initialState : any), }; hook.queue = queue; const dispatch : Dispatch <A> = (queue.dispatch = (dispatchReducerAction.bind ( null , currentlyRenderingFiber, queue, ): any)); return [hook.memoizedState , dispatch]; }
其实就比较贴合我们对useReducer的认知了,根据初始值和初始函数,算出一个initialState。然后把这个值,同时赋给hook.memoizedState
和hook.baseState
。接下来,再给hook.queue
和dispatch
赋值,然后return [hook.memoizedState, dispatch];
,就结束了。
我们先来看一下给queue赋值的地方,我依旧是用注释,说明一下各个字段的含义
1 2 3 4 5 6 7 const queue : UpdateQueue <S, A> = { pending : null , lanes : NoLanes , dispatch : null , lastRenderedReducer : reducer, lastRenderedState : (initialState : any), };
最下面两个字段的含义顾名思义,但是我看useReducer里面好像没看到哪里用到它,所以不清楚实际作用是什么,暂时略过吧。
然后我们看下面的dispatch方法,是给dispatchReducerAction
方法绑死了currentlyRenderingFiber和queue参数。我们可以看看他的具体实现,略过开发环境的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function dispatchReducerAction<S, A>( fiber : Fiber , queue : UpdateQueue <S, A>, action : A, ): void { const lane = requestUpdateLane (fiber); const update : Update <S, A> = { lane, revertLane : NoLane , action, hasEagerState : false , eagerState : null , next : (null : any), }; if (isRenderPhaseUpdate (fiber)) { enqueueRenderPhaseUpdate (queue, update); } else { const root = enqueueConcurrentHookUpdate (fiber, queue, update, lane); if (root !== null ) { startUpdateTimerByLane (lane); scheduleUpdateOnFiber (root, fiber, lane); entangleTransitionUpdate (root, queue, lane); } } }
这个方法其实就是reducer的第二个参数,常见的dispatch,我们调用它去改变状态。他所做的事情,就是创建一个update对象,然后把这个update推到queue里面。具体逻辑我们暂不深究,等后面讲到相关的部分再来细讲。
也就是说,hook里面的主要结构是这样的
updateReducer 当函数组件重新渲染的时候,再执行到这个hook的时候,就会执行updateReducer。我们看看updateReducer执行了什么。
1 2 3 4 5 6 7 8 function updateReducer<S, I, A>( reducer : (S, A ) => S, initialArg : I, init?: I => S, ): [S, Dispatch <A>] { const hook = updateWorkInProgressHook (); return updateReducerImpl (hook, ((currentHook : any): Hook ), reducer); }
对比mountReducer,一开始从mountWorkInProgressHook
,变成了updateWorkInProgressHook
。mountWorkInProgressHook
是在组件mount的时候,把每一个hook都挂到fiber上面。而updateReducer,则是从current fiber上,把所有的hook都copy到workInProgress fiber上面。
为了节省篇幅,具体代码就不看了。代码也不复杂,抛去各种容错的代码,其实就是遍历一个老链表,把每个节点都加到新链表的末尾(每个hook节点都是新创建的字面量,但是这个节点的各个字段,例如memoizedState,和queue都和老节点值相等)
下面的updateReducerImpl
是重头戏,它包含了useReducer最核心,也是最复杂的代码。里面为了处理可中断渲染,并发更新的逻辑,写得很复杂。我们可能关注核心逻辑,对于较为复杂的逻辑,我们简单讲解,但不花太多口舌。等到后面讲到React 18开始的新API的时候,涉及到并发渲染,我们再来详细讲吧。
对于大篇幅的代码,我们分段来看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function updateReducerImpl<S, A>( hook : Hook , current : Hook , reducer : (S, A ) => S, ): [S, Dispatch <A>] { const queue = hook.queue ; let baseQueue = hook.baseQueue ; const pendingQueue = queue.pending ; if (pendingQueue !== null ) { if (baseQueue !== null ) { const baseFirst = baseQueue.next ; const pendingFirst = pendingQueue.next ; baseQueue.next = pendingFirst; pendingQueue.next = baseFirst; } current.baseQueue = baseQueue = pendingQueue; queue.pending = null ; } }
我们首先拿到了hook.queue.pending
,记作pengingQueue
,里面的内容是这一次新增的update。然后拿到了hook.baseQueue
记作baseQueue
,里面的内容是上一次渲染的时候因为优先级不够,而没有被执行的update。
简单提一下,React18开始,部分API可以使得update的优先级变低,从而使得主线程优先完成高优先级的渲染,之后有时间了再来完成低优先级的更新。所以低优先级的更新会被暂存在baseQueue里。
pendingQueue
和baseQueue
的结构,都是一个环形链表,并且这两个变量都指向环形链表的尾节点。也就是说,pendingQueue/baseQueue
是环形链表的尾,pendingQueue.next/baseQueue.next
是环形链表的头。
这里之所以用这样的数据结构来记录update,是因为这里是一个经常需要在尾部插入的场景。如果用普通的单链表,记录头节点的话,那么在尾部插入的时间复杂度是O(n)。而如果使用环形链表,并且记录尾节点,则在尾部插入的时间复杂度则是O(1)。同时,由于它是个环,那么从尾部找到它的头,并且从头开始遍历,也很简单,几乎和普通单链表没有差距。如下图
而上面那一大段代码,就是把baseQueue和pendingQueue做一个合并。把这次新增的,加到上一次遗留后面,并且保证遍历的时候,先遍历到上一次遗留的。这很合理,先来后到。下面的图演示了两个环形链表如何合并成一个新的环形链表。且永远用环形链表的尾指针来表示这个链表。
我们看下面这张图,来理解一下上面合并两个环形链表的代码
合并完毕后,将current.baseQueue
,baseQueue
,pendingQueue
都指向pendingQueue,得到一个新的环形链表。这个环形链表的头节点,是原来的baseFirst。这样遍历这个链表的时候,就会先遍历到原来baseQueue的内容,然后再遍历到pendingQueue上面的内容,保证了先来后到。
我们继续看下面的代码。两个环形链表已经拼接完了,就该遍历链表,逐个取出update去更新了。这里代码实在太多,我不逐行讲解了,而是在关键位置加上注释。在后面只做简短的讲解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 function updateReducerImpl<S, A>( hook : Hook , current : Hook , reducer : (S, A ) => S, ): [S, Dispatch <A>] { const baseState = hook.baseState ; if (baseQueue === null ) { hook.memoizedState = baseState; } else { const first = baseQueue.next ; let newState = baseState; let newBaseState = null ; let newBaseQueueFirst = null ; let newBaseQueueLast : Update <S, A> | null = null ; let update = first; let didReadFromEntangledAsyncAction = false ; do { const updateLane = removeLanes (update.lane , OffscreenLane ); const isHiddenUpdate = updateLane !== update.lane ; const shouldSkipUpdate = isHiddenUpdate ? !isSubsetOfLanes (getWorkInProgressRootRenderLanes (), updateLane) : !isSubsetOfLanes (renderLanes, updateLane); if (shouldSkipUpdate) { const clone : Update <S, A> = { lane : updateLane, revertLane : update.revertLane , action : update.action , hasEagerState : update.hasEagerState , eagerState : update.eagerState , next : (null : any), }; if (newBaseQueueLast === null ) { newBaseQueueFirst = newBaseQueueLast = clone; newBaseState = newState; } else { newBaseQueueLast = newBaseQueueLast.next = clone; } currentlyRenderingFiber.lanes = mergeLanes ( currentlyRenderingFiber.lanes , updateLane, ); markSkippedUpdateLanes (updateLane); } else { const revertLane = update.revertLane ; if (revertLane === NoLane ) { if (newBaseQueueLast !== null ) { const clone : Update <S, A> = { lane : NoLane , revertLane : NoLane , action : update.action , hasEagerState : update.hasEagerState , eagerState : update.eagerState , next : (null : any), }; newBaseQueueLast = newBaseQueueLast.next = clone; } if (updateLane === peekEntangledActionLane ()) { didReadFromEntangledAsyncAction = true ; } } else { } const action = update.action ; if (shouldDoubleInvokeUserFnsInHooksDEV) { reducer (newState, action); } if (update.hasEagerState ) { newState = ((update.eagerState : any): S); } else { newState = reducer (newState, action); } } update = update.next ; } while (update !== null && update !== first); if (newBaseQueueLast === null ) { newBaseState = newState; } else { newBaseQueueLast.next = (newBaseQueueFirst : any); } if (!is (newState, hook.memoizedState )) { markWorkInProgressReceivedUpdate (); if (didReadFromEntangledAsyncAction) { const entangledActionThenable = peekEntangledActionThenable (); if (entangledActionThenable !== null ) { throw entangledActionThenable; } } } hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; queue.lastRenderedState = newState; }
我们看他这里的逻辑,主要就是遍历baseQueue
,逐个取出里面的update。每个update有它自己的lane(优先级),我们前面已经看过了。这里的lane是用二进制位来表示的一个数字。用二进制来表示,就是为了能直观的看出位运算的结果。判断update是否要在这次渲染执行,就是看update的lane是否属于这次renderLanes的子集(即updateLane & renderLanes === updateLane?)。
如果优先级不属于这次渲染,那么就放到newBaseQueue
里。我们上一part看到的baseQueue,说了含义是上一次渲染遗留的update,其实就是这么来的。
然后有个比较难懂的逻辑,就是虽然这个update优先级是要在这次执行的,但是当newBaseQueue
不为空的时候,也要把这个update加进去。这个就和React的并发渲染有关。React的更新是有优先级的,比如如果你普通的useReducer
,那么它就是高优先级。如果你用startTransition
包裹了useReducer
,那么它就是低优先级。
如果你像下面这样写,点击一次按钮的时候,会有两次更新。只不过一次是低优先级,一次是高优先级。
1 2 3 4 5 6 7 onClick = ()=> { dispatchNum (n => n+2 ) startTransition (()=> { dispatchNum (3 ) }) dispatchNum (n => n+1 ) }
假设num初始值是0。我们只想最终结果,num应该是4。因为startTransition对于最终结果是不应该产生影响的。如果你为了性能优化,而影响了结果,谁还敢用呢? 那么第一次渲染,是高优先级的渲染,会先执行一次+2,在执行一次+1,此时n是3。然后第二次渲染,终于要执行低优先级的了。可这个低优先级的更新,是将num设为3,那这样是不是不太对,凑不成4?
所以他的真正逻辑是这样的。第一次渲染,先执行+2。然后等遍历到低优先级update的时候,发现这个update优先级不够了,于是把他存入newBaseQueue
。同时,把当前的结果记作newBaseState,作为基准。意思就是,在此之前,所有的update都没有被跳过,执行完他们的状态是newBaseState。等以后在遍历的时候,不用管前面的update了,从第一个被跳过的update开始遍历,把newBaseState当做初始值就可以了。接着遍历第三个update,执行它,并且将它也推入到newBaseQueue
。
那么第一遍高优先级渲染执行完,state是3(0+2+1),让用户临时看到了高优先级的结果。并且newBaseState是2,newBaseQueue里分别是“设为3”和“+1”两个更新。第二遍执行的时候,会以newBaseState作为基准,然后把newBaseQueue
里面的所有更新都重新执行一遍。这里面会有更新被重复执行。最终第二遍渲染执行完之后,state就是最终结果4了(2=>3+1)
那么以上,就是useReducer的逻辑