前言 这篇主要还是笔记性质,一边探索一边记录。
因为Fiber链表性质,Update被重新实现,这里需要重新分析一下。
细节 这一节主要是对源码的分析。
先预设置一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class App extends React .Component { state = { text: 'Text' } changeText = () => { this .setState({ text: 'Hello World' }) } render () { return ( <div className="App" > <header className="App-header" > <div>{this .state.text}</div> <button onClick={this .changeText}>change Text</button> </header> </div> ) } }
我们忽略事件相关的东西,专注setState。
始于setState 基于v15的理解,不管是props更新,还是state更新,实质上归根结底,还是setState触发的更新。
v16的props更新呢,它会不遵循这个路线吗?思前想后的结果是:不会。所以这里就直接分析setState了。
这里寻找这个定义挺容易的,直接命令行输入 grep -rn 'prototype.setState' ./packages
就可以查出来。当然,断点更容易出来。
1 2 3 Component.prototype.setState = function (partialState, callback ) { this .updater.enqueueSetState(this , partialState, callback, 'setState' ); };
这里涉及到了this.updater
。不妨全局查一下:grep -rn '\.updater =' ./packages
。出来的结果只有./packages/react-reconciler/src/ReactFiberClassComponent.js:497
,也就是adoptClassInstance
函数可能是调用,观察这个赋值的目标classComponentUpdater
,也能基本证明这个猜测。
所以这里this.updater.enqueueSetState
实质上就是classComponentUpdater.enqueueSetState
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 enqueueSetState (inst, payload, callback ) { const fiber = getInstance(inst); const currentTime = requestCurrentTime(); const expirationTime = computeExpirationForFiber(currentTime, fiber); const update = createUpdate(expirationTime); update.payload = payload; if (callback !== undefined && callback !== null ) { update.callback = callback; } flushPassiveEffects(); enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); },
这里关系到update的实质上就是enqueueUpdate(fiber, update)
,说白了,这里update主要还是设置一个expirationTime, fiber节点上的更新队列才是实质核心。
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 export function enqueueUpdate <State >(fiber: Fiber, update: Update<State> ) { const alternate = fiber.alternate; let queue1; let queue2; if (alternate === null ) { queue1 = fiber.updateQueue; queue2 = null ; if (queue1 === null ) { queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); } } else { } if (queue2 === null || queue1 === queue2) { appendUpdateToQueue(queue1, update); } else { } }
这里enqueueUpdate主要是调用appendUpdateToQueue。这个函数基本可以理解为向update数组push一个元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function appendUpdateToQueue (queue, update ) { if (queue.lastUpdate === null ) { queue.firstUpdate = queue.lastUpdate = update; } else { queue.lastUpdate.next = update; queue.lastUpdate = update; } }
走完这一步时候,fiber<APP>.updateQueue
链表加入了update,updateQueue上firstEffect指向了这个update。
然后enqueueSetState
开始执行scheduleWork(fiber, expirationTime)
。
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 function scheduleWork (fiber: Fiber, expirationTime: ExpirationTime ) { const root = scheduleWorkToRoot(fiber, expirationTime); if (root === null ) { return ; } if ( !isWorking && nextRenderExpirationTime !== NoWork && expirationTime > nextRenderExpirationTime ) { interruptedBy = fiber; resetStack(); } markPendingPriorityLevel(root, expirationTime); if ( !isWorking || isCommitting || nextRoot !== root ) { const rootExpirationTime = root.expirationTime; requestWork(root, rootExpirationTime); } }
这个函数主要是从Fiber开始往上遍历,更新对应节点的childExpirationTime属性,然后返回FiberRoot节点。childExpirationTime用来判定是否要更新child,这里不做细表。然后接下来会执行requestWork(root, rootExpirationTime)
。
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 function requestWork (root: FiberRoot, expirationTime: ExpirationTime ) { addRootToSchedule(root, expirationTime); if (isRendering) { return ; } if (isBatchingUpdates) { if (isUnbatchingUpdates) { nextFlushedRoot = root; nextFlushedExpirationTime = Sync; performWorkOnRoot(root, Sync, false ); } return ; } if (expirationTime === Sync) { performSyncWork(); } else { scheduleCallbackWithExpirationTime(root, expirationTime); } }
此时入注释中所标,这个函数几乎不会执行任何东西,除了开头那一句——addRootToSchedule(root, expirationTime)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function addRootToSchedule (root, expirationTime ) { if (root.nextScheduledRoot === null ) { root.expirationTime = expirationTime; if (lastScheduledRoot === null ) { firstScheduledRoot = lastScheduledRoot = root; root.nextScheduledRoot = root; } else { } } else { } }
作为一个链表,lastScheduledRoot代表的是下一个操作、读取的目标。所以这里线索基本上可以锁定到读取了lastScheduledRoot变量的函数。这里能列入候选的函数只有两个
findHighestPriorityRoot
addRootToSchedule
但是满足场景的目标只有findHighestPriorityRoot。进一步反推,findHighestPriorityRoot调用者只有performWork——所以呢,不管什么,这个场景下,最后引起DOM更新的,必定、也必须是performWork。
Tips: 这里之所以说lastScheduledRoot代表的是下一个操作,是因为这里没有对应的nextScheduledRoot变量,这个nextScheduledRoot直接挂到root节点上了,所以lastScheduledRoot就是下一个,也是最后一个。
Tips: 关于findHighestPriorityRoot可以后面看看Reconciler部分分析,会有详细分析。这里仅仅做脉络推导。
Tips: lastScheduledRoot实际上在React-DOM里面就一个指向fiberRoot或者干脆为null,addRootToSchedule实质上只是更新了fiberRoot.expirationTime。
略过的Event 不管怎样,v16更新后更新逻辑因为基础数据结构变化,出了一些必要的变化,总之这里更新后DOM确确实实不再是setState直接引起的了。它被耦合进了事件这一块。当更新队列处理完毕之后,React只是不动声色lastScheduledRoot赋值给了fiberRoot,然后由事件机制处理了后续。
但是这里不打算调过头去研究新的Event了。所以还是通过断点来过去调用栈。这里产生的调用栈是:
1 2 3 4 5 6 dispatchInteractiveEvent ->interactiveUpdates -->dispatchEvent --->performSyncWork ---->performWork ----->performWorkOnRoot -> renderRoot & completeRoot
这里暂时不管是如何调用下来的,但是这里能确认的是当断点走过performWorkOnRoot函数。Text在DOM上就完成了从Text到HelloWorld的过程。
这个函数其实在render篇已经提到过了。不过这里重点是要把Update部分单独拎出来讲,侧重点有所不同。
这里基础的路径还是:
1 2 3 4 5 6 performWorkOnRoot ->beginWork -->updateClassComponent ---->updateClassInstance ----->processUpdateQueue ------>getStateFromUpdate
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 function getStateFromUpdate <State >( workInProgress: Fiber, queue: UpdateQueue<State>, update: Update<State>, prevState: State, nextProps: any, instance: any, ): any { switch (update.tag) { case ReplaceState: { } case CaptureUpdate: { } case UpdateState: { const payload = update.payload; let partialState; if (typeof payload === 'function' ) { partialState = payload.call(instance, prevState, nextProps); } else { partialState = payload; } if (partialState === null || partialState === undefined ) { return prevState; } return Object .assign({}, prevState, partialState); } case ForceUpdate: { hasForceUpdate = true ; return prevState; } } return prevState; }
这里场景下的核心是Object.assign({}, prevState, partialState)
。很好理解。
processUpdateQueue这个函数在这里需要关注点的是,更新了workInProgress.memoizedState。但是这是App这个fiber节点的事情。不妨回顾有关ChildReconciler的分析。当我们把文首的例子拆成Fiber,有几个节点呢(这里由FunctionComponent->ClassComponent了)?
这里答案是6个。我们添加一个button。
1 2 3 4 5 6 1. FiberRoot tag = 3 2. fiberNode{elementType = App} tag = 1 3. fiberNode{elementType = 'div' } tag = 5 4. fiberNode{elementType = 'header' } tag = 5 5. fiberNode{elementType = 'div' } tag = 5 6. fiberNode{elementType = 'button' } tag = 5
这里完全可以做一个小结,在这个更新的的render环节,主要是两个Fiber节点变了。
第二个节点memoizedState变化为{text: ‘Hello World’}
第五个节点里面memoizedProps和pendingProps节点里面分别保存了新旧不同的children。
以v15的Diff算法。会针对第五个节点执行创建新节点对旧节点进行替换、插入第六个节点。我们后面再看看V16里面是如何实现Diff的。
总之,这是completeRoot环节的问题。切略过不提。
Hook的实现 原本想过如何去理解Hook,但是最后决定把它作为Update的一个小结来分析。
由很多人说Hook其实可以作为Redux的替代,但是Redux本身是借助setState实现,所以这里看看Hook是如何处理的。
这里需要一个新的例子。这里改造一下。
1 2 3 4 5 6 7 8 9 10 11 12 import React, { useState } from 'react' ;function App ( ) { const [text, setText] = useState('Text' ); return ( <div className="App" > <header className="App-header" > <div>{text}</div> <button onClick={() => { setText('Hello World' ) }}>change Text</button> </header> </div> ) }
看看useState的定义:
1 2 3 4 5 6 7 8 9 10 11 export function useState <S >(initialState: (() => S) | S ) { const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); } function resolveDispatcher ( ) { const dispatcher = ReactCurrentDispatcher.current; return dispatcher; } const ReactCurrentDispatcher = { current: (null : null | Dispatcher), }
这个逻辑埋得有点深。断点在useState('Text')
之前,可以发现它是ƒ bound dispatchAction()
,最终是对dispatchAction的处理。
再看看调试工具里面的调用栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 App (App.js:5 ) renderWithHooks (react-dom.development.js:13449 ) updateFunctionComponent (react-dom.development.js:15199 ) beginWork (react-dom.development.js:16252 ) performUnitOfWork (react-dom.development.js:20279 ) workLoop (react-dom.development.js:20320 ) renderRoot (react-dom.development.js:20400 ) performWorkOnRoot (react-dom.development.js:21357 ) performWork (react-dom.development.js:21267 ) performSyncWork (react-dom.development.js:21241 ) interactiveUpdates$1 (react-dom.development.js:21526 ) interactiveUpdates (react-dom.development.js:2268 ) dispatchInteractiveEvent (react-dom.development.js:5085 )
根据renderWithHooks函数,可以做出的论断是ReactCurrentDispatcher.current 可能的值是 HooksDispatcherOnUpdate && HooksDispatcherOnMount。初始渲染阶段,它是HooksDispatcherOnMount,之后它是HooksDispatcherOnUpdate。
这里得看看HooksDispatcherOnMount,然后才是HooksDispatcherOnUpdate。
为什么是这个顺序?因为setText是一个函数,后面在update环节会调用。而它里面有很多变量,必须在这里形成闭包缓存起来以备后面使用。
如果无法理解这个mount & update。这里做个简要分析:
当我们初次渲染渲染时候,App函数会运行,useState会运行第一次。这是一个初始化
当App里面onClick触发setText时候,useState里面会有第二次运行。但是我们的useState依然会运行第二次。
这里问题来了: 这两次useState运行过程中,又应当是怎样的实现的数据变更和变量传递呢?
Mount && Dispatch 第一阶段是Mount,这是初始化渲染环节里面的处理方式。
HooksDispatcherOnMount.useState:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function mountState <S >( initialState: (() => S) | S, ): [S , Dispatch <BasicStateAction <S >>] { const hook = mountWorkInProgressHook(); if (typeof initialState === 'function' ) { initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; const queue = (hook.queue = { last: null , dispatch: null , lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), }); const dispatch: Dispatch< BasicStateAction<S>, > = (queue.dispatch = (dispatchAction.bind( null , ((currentlyRenderingFiber: any): Fiber), queue, ): any)); return [hook.memoizedState, dispatch]; }
mountWorkInProgressHook构建了一个空的Hook数据结构,它和Fiber很像,或者说,它是fiber的一个子集。
1 2 3 4 5 6 7 export type Hook = { memoizedState: any, baseState: any, baseUpdate: Update<any, any> | null , queue: UpdateQueue<any, any> | null , next: Hook | null , };
Tips: Hook也被储存在链表结构中。它们使用以下变量进行储存,next连接所有Hook:
1 2 3 4 5 let currentHook: Hook | null = null; let nextCurrentHook: Hook | null = null; let firstWorkInProgressHook: Hook | null = null; let workInProgressHook: Hook | null = null; let nextWorkInProgressHook: Hook | null = null;
这里初始化渲染是将firstWorkInProgressHook,workInProgressHook都设为了这个新建的hook。
而currentlyRenderingFiber变量在renderWithHooks函数里面有定义,它是当前渲染的Fiber节点。
1 currentlyRenderingFiber = workInProgress;
所以说,就这个setText函数来说,未看其内容,已经可以知道,它可以获取queue(尤其是内部的memoizedState值,这里场景是text变量),同时也可以访问到对应的Fiber节点,它形成一个闭包。接下来看看dispatchAction函数。
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 function dispatchAction <S , A >( fiber: Fiber, queue: UpdateQueue<S, A>, action: A, ) { const alternate = fiber.alternate; if ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber) ) { } else { flushPassiveEffects(); const currentTime = requestCurrentTime(); const expirationTime = computeExpirationForFiber(currentTime, fiber); const update: Update<S, A> = { expirationTime, action, eagerReducer: null , eagerState: null , next: null , }; const last = queue.last; if (last === null ) { update.next = update; } else { const first = last.next; if (first !== null ) { update.next = first; } last.next = update; } queue.last = update; if ( fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork) ) { const lastRenderedReducer = queue.lastRenderedReducer; if (lastRenderedReducer !== null ) { let prevDispatcher; try { const currentState: S = (queue.lastRenderedState: any); const eagerState = lastRenderedReducer(currentState, action); update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (is(eagerState, currentState)) { return ; } } catch (error) { } finally { } } } scheduleWork(fiber, expirationTime); } }
虽然里面逻辑很多,但是它核心的地方却只有几个:
根据action参数(Hello World)创建了一个update
update被加入到了queue链表上
执行scheduleWork
此时因为scheduleWork在batchedUpdates函数下游,isBatchingUpdates(这个变量在batchedUpdates更改并后续requestWork中引用)被赋值为true,所以scheduleWork并不会引发后面的commit phase阶段。
而是由事件系统触发了。调用栈其实和上面Event提到的一致。
到了这里,最后的疑问可能就是commit阶段里,后续它究竟是如何获取queue链表了,这里还是call by share相关知识了,这里不再提及,主要还是对hook变量上的queue做了修改,此时hook.queue被添加了一个update到尾部上。当setText被导出,这个hook就会因为闭包被缓存再mountState的作用域里面不会被GC。
由于这个hook每次运行都会重新生成新的hook,所以多个FunctionComponent里面相同的setText使用不会读取到旧的值。
而且因为hook没有被导出过,renderWithHook也由相关render阶段执行,所以也无法在React组件之外访问到它。
以下是重点 。前面我们提到了对hook的创建,操作,链表结构,以及firstWorkInProgressHook变量。但是它们都没有做导出。这里hook是链表,它和WorkInProgress是相同的性质,它将可以类似全局性质的获取、变更。
回头仔细观察renderWithHooks函数。其中两句显得尤为关键。
1 2 3 4 let children = Component(props, refOrContext);const renderedWork: Fiber = (currentlyRenderingFiber: any);renderedWork.memoizedState = firstWorkInProgressHook;
综上,可以知道renderedWork.memoizedState
变量被赋值未新建的那个Hook。为什么在末尾重点提到它呢?因为它不但承上,而且启下,是整个hook和fiber结构的联结点。
现在已知memoizedState不但会保存常规的memoizedState值,还会保存ReactElement和Hook。
Update 然后就是我们想知道的更新方面的环节。当renderWithHook再度调起App(),此时HooksDispatcherOnUpdate.useState就有了用武之地,它实质指向updateState。
1 2 3 4 5 6 7 8 function updateState <S >( initialState: (() => S) | S, ): [S , Dispatch <BasicStateAction <S >>] { return updateReducer(basicStateReducer, (initialState: any)); } function basicStateReducer <S >(state: S, action: BasicStateAction<S> ): S { return typeof action === 'function' ? action(state) : action; }
而updateReducer里面有这样的返回:
1 2 3 4 5 6 7 8 9 function updateReducer <S , I , A >( reducer: (S, A) => S, initialArg: I, init?: I => S, ): [S , Dispatch <A >] { const hook = updateWorkInProgressHook(); const queue = hook.queue; const dispatch: Dispatch<A> = (queue.dispatch: any); return [hook.memoizedState, dispatch]; }
这里hook执行后的返回值大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 { baseState: "Text" baseUpdate: null memoizedState: "Text" next: null queue: { dispatch: ƒ () last: {expirationTime : 1073741823 , action : "Hello World" , eagerReducer : ƒ, eagerState : "Hello World" , next : {…}} lastRenderedReducer: ƒ basicStateReducer(state, action) lastRenderedState: "Text" } }
参见updateWorkInProgressHook
,它返回的主要是nextCurrentHook的一个副本。
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 updateWorkInProgressHook ( ): Hook { if (nextWorkInProgressHook !== null ) { } else { currentHook = nextCurrentHook; const newHook: Hook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, queue: currentHook.queue, baseUpdate: currentHook.baseUpdate, next: null , }; if (workInProgressHook === null ) { workInProgressHook = firstWorkInProgressHook = newHook; } else { workInProgressHook = workInProgressHook.next = newHook; } nextCurrentHook = currentHook.next; } return workInProgressHook; }
这里关键是nextCurrentHook
变量,再回头看看renderWithHooks,里面有一句:
nextCurrentHook = current !== null ? current.memoizedState : null
。
所以一切都顺理成章了。它们都指向了之前创建的hook。
到这里我们就可以明白,这个遍历是如何从dispatch传递到update环节的。在Mount环节,我们初始化了一个Hook,然后再dispatch我们更新了这个Hook,并将他赋值到了当前fiberNode的memoizedState属性。最后我们更新环节则更换了一个useState函数,它在里面获取了dispatch变更后的Hook,然后执行了后续渲染。
在之前的ChildReconciler篇里面其实有提到renderWithHooks。但是那时候只是专注于它的结果,它返回的是一颗展开完毕的VDOM树。
这里我们仍然不做接下来的细节分析,但是,对于最简单的Hook更新,我们已经对其数据流变化一清二楚了。
Commit 这里还是走的Event这块。
1 2 3 4 5 6 dispatchInteractiveEvent ->interactiveUpdates -->dispatchEvent --->performSyncWork ---->performWork ----->performWorkOnRoot -> renderRoot & completeRoot
但是为了能更好代入Hook这块,我们做一些更细致的工作:
1 2 3 4 5 6 7 8 9 10 11 dispatchInteractiveEvent ->interactiveUpdates -->dispatchEvent --->performSyncWork ---->performWork ----->performWorkOnRoot -> renderRoot() { workLoop ->performUnitOfWork -->beginWork ---->updateFunctionComponent ->renderWithHooks } & completeRoot
这样,就能将Event这块和Hook这块衔接上了。