初步整理 这里还需要回顾一下,FiberNode遍历DOM树的数据结构和遍历基础。
遍历理论在上一篇v16-render里面有简略伪代码可以供参考。它依托FiberNode得三个属性:child,return,sibling
来实现。如果对这个理论不了解必须回上一篇看看理论。
链表构建 这里链表的构建主要就是child,return,sibling
三个属性的构建。
这个构建过程在reconcileChildren
函数及其调用函数中。
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 export function reconcileChildren ( current: Fiber | null , workInProgress: Fiber, nextChildren: any, renderExpirationTime: ExpirationTime, ) { if (current === null ) { workInProgress.child = mountChildFibers( workInProgress, null , nextChildren, renderExpirationTime, ); } else { workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderExpirationTime, ); } } export const reconcileChildFibers = ChildReconciler(true );export const mountChildFibers = ChildReconciler(false );
可以看到不管如何,最终都会调用到ChildReconciler。两者区别是初始更新和Update的区别。
接下来是ChildReconciler函数代码:
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 function ChildReconciler (shouldTrackSideEffects ) { function reconcileChildFibers ( returnFiber: Fiber, currentFirstChild: Fiber | null , newChild: any, expirationTime: ExpirationTime, ): Fiber | null { const isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null ; if (isUnkeyedTopLevelFragment) { newChild = newChild.props.children; } const isObject = typeof newChild === 'object' && newChild !== null ; if (isObject) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild( reconcileSingleElement( returnFiber, currentFirstChild, newChild, expirationTime, ), ); case REACT_PORTAL_TYPE: return placeSingleChild( reconcileSinglePortal( returnFiber, currentFirstChild, newChild, expirationTime, ), ); } } if (typeof newChild === 'string' || typeof newChild === 'number' ) { } if (isArray(newChild)) { return reconcileChildrenArray( returnFiber, currentFirstChild, newChild, expirationTime, ); } if (getIteratorFn(newChild)) { return reconcileChildrenIterator( returnFiber, currentFirstChild, newChild, expirationTime, ); } if (isObject) { throwOnInvalidObjectType(returnFiber, newChild); } if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) { } return deleteRemainingChildren(returnFiber, currentFirstChild); } return reconcileChildFibers; }
这里对应的大场景有两个:
单一子节点构建 这其中不管是那种情况,都必然会有child & return的设置。我们来看看调用栈。
首先是常规的单一的子节点。此时面对的情况是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (isObject) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild( reconcileSingleElement( returnFiber, currentFirstChild, newChild, expirationTime, ), ); case REACT_PORTAL_TYPE: } } placeSingleChild(reconcileSingleElement(args)) ->reconcileSingleElement(args)
看看这个函数的代码:
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 function reconcileSingleElement ( returnFiber: Fiber, currentFirstChild: Fiber | null , element: ReactElement, expirationTime: ExpirationTime, ): Fiber { const key = element.key; let child = currentFirstChild; while (child !== null ) { } if (element.type === REACT_FRAGMENT_TYPE) { const created = createFiberFromFragment( element.props.children, returnFiber.mode, expirationTime, element.key, ); created.return = returnFiber; return created; } else { const created = createFiberFromElement( element, returnFiber.mode, expirationTime, ); created.ref = coerceRef(returnFiber, currentFirstChild, element); created.return = returnFiber; return created; } }
此时配合这里的created.return
赋值语句,整个具体场景里面的return和child就做好了配对。
多节点配对 多节点配对的核心代码是:
1 2 3 4 5 6 7 8 if (isArray(newChild)) { return reconcileChildrenArray( returnFiber, currentFirstChild, newChild, 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 function reconcileChildrenArray ( returnFiber: Fiber, currentFirstChild: Fiber | null , newChildren: Array <*>, expirationTime: ExpirationTime, ): Fiber | null { let resultingFirstChild: Fiber | null = null ; let previousNewFiber: Fiber | null = null ; let oldFiber = currentFirstChild; let lastPlacedIndex = 0 ; let newIdx = 0 ; let nextOldFiber = null ; for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { } if (newIdx === newChildren.length) { deleteRemainingChildren(returnFiber, oldFiber); return resultingFirstChild; } if (oldFiber === null ) { for (; newIdx < newChildren.length; newIdx++) { const newFiber = createChild( returnFiber, newChildren[newIdx], expirationTime, ); if (!newFiber) { continue ; } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null ) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } return resultingFirstChild; } }
关键地方的构建这里都在注释里面有写了。
当children是数组,那么遍历,看时机将sibling绑定到下一个同级元素。结束后将firstChild返回。此时workInProgress.child
就被绑定到了这个firstChild上了。
那么新的childrenItem上的return呢?它们在createChild里面被绑定好了。买个元素的return都指定到了workInProgress节点。
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 function createChild ( returnFiber: Fiber, newChild: any, expirationTime: ExpirationTime, ): Fiber | null { if (typeof newChild === 'string' || typeof newChild === 'number' ) { const created = createFiberFromText( '' + newChild, returnFiber.mode, expirationTime, ); created.return = returnFiber; return created; } if (typeof newChild === 'object' && newChild !== null ) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { const created = createFiberFromElement( newChild, returnFiber.mode, expirationTime, ); created.ref = coerceRef(returnFiber, null , newChild); created.return = returnFiber; return created; } case REACT_PORTAL_TYPE: { const created = createFiberFromPortal( newChild, returnFiber.mode, expirationTime, ); created.return = returnFiber; return created; } } if (isArray(newChild) || getIteratorFn(newChild)) { const created = createFiberFromFragment( newChild, returnFiber.mode, expirationTime, null , ); created.return = returnFiber; return created; } throwOnInvalidObjectType(returnFiber, newChild); } return null ; }
这里分支有点多,但是只需要关注return的赋值即可。
递归&串联 我们就仅有两层的树进行了分析。那么我们如何将多个层级递归串联起来,让它们一直从顶部走到终点呢?答案当然是workLoop!
如果对workLoop还有陌生感觉,那么不妨重新区看看。workLoop循环结束的唯一条件是FiberNode.child===null。只要child不为null,他会按照算法涉及一路next或者child遍历下去。
小总结 到这里,我们的链表构建就结束了。我们清晰的看到了,这个链表是如何完整架构的。但是这里还是不太够,我们对current和workInProgress树的流程还缺乏一个整体认知。
Fiber里面的树 workInProgress树 这棵树是有迹可循的。
实质上,构建workInProgress树其实就是链表构建的更深入的探究。它和上一节其实密不可分。
换句话说,上一节其实也只是构建workInProgress过程之一。所以,两部分参考着看是很有意义的。
创建 第一个创建是在renderRoot函数里面:
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 nextUnitOfWork = createWorkInProgress( nextRoot.current, null , nextRenderExpirationTime, ); export function createWorkInProgress ( current: Fiber, pendingProps: any, expirationTime: ExpirationTime, ): Fiber { let workInProgress = current.alternate; if (workInProgress === null ) { workInProgress = createFiber( current.tag, pendingProps, current.key, current.mode, ); workInProgress.elementType = current.elementType; workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; current.alternate = workInProgress; } else { } workInProgress.childExpirationTime = current.childExpirationTime; workInProgress.expirationTime = current.expirationTime; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; workInProgress.contextDependencies = current.contextDependencies; workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref; if (enableProfilerTimer) { workInProgress.selfBaseDuration = current.selfBaseDuration; workInProgress.treeBaseDuration = current.treeBaseDuration; } return workInProgress; }
关于这个源头我们穷根究底一下。
ReactDom.render(App, container)首先获取到的root是一个App经过createElement函数返回的结果。举个栗子:
1 2 3 4 5 6 7 8 { $$typeof: Symbol (react.element) key: null props: {} ref: null type: ƒ App() _owner: null }
后续引用到的ReactRoot构造器根据给出的container元素构造了一个Fiber节点。这事所有Fiber节点的root。前面提到的nextRoot也是它。
1 2 var root = createContainer(container, isConcurrent, hydrate); this._internalRoot = root;
这个createContainer设置了current,current由createHostRootFiber函数创建了一个FiberNode,除了tag==3之外和空的FiberNode没有什么区别
createWorkInProgress环节。
此时,workInProgress.alternate = current.alternate = current = createHostRootFiber()
基本都是空Fiber,只不过设置了tag===3。
初始化render里的变更 接下是workLoop+performUnitOfWork了。performUnitOfWork引用了beginWork。
根节点 当根节点进入这个beginWork分支。触发return updateHostRoot(current, workInProgress, renderExpirationTime)
,也就是nextUnitOfWork变量初始化后第一次获得返回值。
这个函数中有这样一个调用栈:
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 function updateHostRoot (current, workInProgress, renderExpirationTime ) { pushHostRootContext(workInProgress); const updateQueue = workInProgress.updateQueue; const nextProps = workInProgress.pendingProps; const prevState = workInProgress.memoizedState; const prevChildren = prevState !== null ? prevState.element : null ; processUpdateQueue( workInProgress, updateQueue, nextProps, null , renderExpirationTime, ); const nextState = workInProgress.memoizedState; const nextChildren = nextState.element; if (nextChildren === prevChildren) { resetHydrationState(); return bailoutOnAlreadyFinishedWork( current, workInProgress, renderExpirationTime, ); } const root: FiberRoot = workInProgress.stateNode; if ( (current === null || current.child === null ) && root.hydrate && enterHydrationState(workInProgress) ) { workInProgress.effectTag |= Placement; workInProgress.child = mountChildFibers( workInProgress, null , nextChildren, renderExpirationTime, ); } else { reconcileChildren( current, workInProgress, nextChildren, renderExpirationTime, ); resetHydrationState(); } return workInProgress.child; }
这里processcessUpdateQueue函数在上一篇render里面有相对详细的解读。这里关注点是内部引用:
1 2 3 4 5 6 7 8 9 resultState = getStateFromUpdate( workInProgress, queue, update, resultState, props, instance, ); workInProgress.memoizedState = resultState;
这里getStateFromUpdate函数为空白的workInProgress注入了props数据。它更新了workInProgress.memoizedState,就这里而言,它将{element: reactElement}
的数据结构赋值过去了。
关于这个变量,参考scheduleRootUpdate函数里面的:
1 2 const update = createUpdate(expirationTime);update.payload = {element};
让我们把关注点继续返回updateHostRoot函数,此时这个函数结束之后,正常情况会进入以下分支:
1 2 3 4 5 6 7 8 reconcileChildren( current, workInProgress, nextChildren, renderExpirationTime, ); resetHydrationState(); return workInProgress.child;
因为返回的是workInProgress.child,所以这里重点关注一下reconcileChildren对workInProgress.child的处理。这里可以重新返回之前的「链表构建」来看。
子孙节点 结合我们到链表构建这一小节。到此为止,我们可以做出这样的论断:
nextUnitOfWork实质上就是当前遍历过程中要处理的节点,通过我们的workLoop遍历,当这个工作到最终节点的时候,我们就完成了整个workProgeress树的构建。
不过这里稍微往下走走,当我们FiberRoot处理完毕,beginWork第二次运行时候应该是怎样的场景呢 ?此时返回的第二个FiberNode节点就是App组件对应的节点。这里关注点核心属性:
1 2 3 4 5 6 { effectTag: 2 elementType: ƒ App() type: ƒ App() return : FiberRoot }
此时走到beginWork.就会进入如下分支:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 case IndeterminateComponent: { const elementType = workInProgress.elementType; return mountIndeterminateComponent( current, workInProgress, elementType, renderExpirationTime, ); } function mountIndeterminateComponent ( ) { value = renderWithHooks( null , workInProgress, Component, props, context, renderExpirationTime, ); reconcileChildren(null , workInProgress, value, renderExpirationTime); return workInProgress.child; }
接下来就是reconcileChildren。关于这个函数可以看看上面提过的「单一子节点构建」部分。总之这里对workProgress有一些修改。最后它将child设为了下面分支返回值:
1 2 3 4 5 6 const created = createFiberFromFragment( element.props.children, returnFiber.mode, expirationTime, element.key, );
最后就是返回了FiberNode。这个FiberNode根据参数设定了以下非null值:
1 2 3 4 5 pendingProps: children 完整VDOM树 tag: Fragment = 7 expirationTime: expirationTime key: element.key mode: returnFiber.mode
这里贯彻一个重点,那就是这个FiberNode是下一个nextUnitOfWork变量,如果对这个概念不熟,那么上一篇需要回看一下。总之,这里脉络是reconcileChildren函数为当前FiberNode构建了child值并返回这个child。
这个child上的pendingProps是workLoop能够持续加下去的核心所在。假设一下,当App里面内容是这样的:
1 2 3 4 5 6 7 8 9 function App ( ) { return ( <div className="App" > <header className="App-header" > <div>Test</div> </header> </div> ); }
beginWork这里分支判断主要靠fiberNode.tag判断。我们这里思考一下。这个App构建下来,会有多少个FiberNode, tag分别是?
答案是: 这里一共会有五个FiberNode, 组成一个workProgress链表树。对应的tag只分别是3、2、5、5、5。换成Enum值,那就是: HostRoot、IndeterminateComponent、HostComponent、HostComponent、HostComponent。很显然,ReactDomComponent都被设置为tag=HostComponent了。
那么问题来了,这里需要将tag和children(ReactElement)联结起来。这里是通过一系列的createFiberFromXXX函数做到的(FiberRoot除外)。而具体采用哪个函数,则取决于ReactElement.type。这里常规(ReactDomComponent)是使用createFiberFromElement函数。
就常规来说,如果type === REACT_FRAGMENT_TYPE
则使用createFiberFromFragment
,否则使用createFiberFromElement
,后者是常规情况。
搞明白前面这些,现在我们可以认认真真看看,children是怎样一路层层拆包,只到遍历完毕为止的。这事workLoop和VDOM的关联脉络所在。
认真观察createFiberFromElement
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 export function createFiberFromElement ( element: ReactElement, mode: TypeOfMode, expirationTime: ExpirationTime, ): Fiber { let owner = null ; const type = element.type; const key = element.key; const pendingProps = element.props; const fiber = createFiberFromTypeAndProps( type, key, pendingProps, owner, mode, expirationTime, ); return fiber; } function updateHostComponent (current, workInProgress, renderExpirationTime ) { let nextChildren = nextProps.children; reconcileChildren( current, workInProgress, nextChildren, renderExpirationTime, ); return workInProgress.child; }
可以看到,这里我们通过这个函数里面这一句const pendingProps = element.props
将整个VDOM树层层解包。最终完成了整个FiberNode树的构建&链接。
同时,VDOM和FiberNode的关系这里也有了清晰认知。
finishedWork树 && current树 renderRoot函数在workLoop结束后有以下处理:
1 2 3 4 5 6 7 8 9 10 11 12 const rootWorkInProgress = root.current.alternate;onComplete(root, rootWorkInProgress, expirationTime); function onComplete ( root: FiberRoot, finishedWork: Fiber, expirationTime: ExpirationTime, ) { root.pendingCommitExpirationTime = expirationTime; root.finishedWork = finishedWork; }
root.current.alternate
显然是root.current
的一个副本。
关于这个root上下文逻辑。不妨在本文内搜索workInProgress.alternate = current.alternate = current = createHostRootFiber()
看看。当renderRoot结束后,它的child指向了我们的App节点。最终的情况就是: 当初始化渲染render阶段完成后,finishedWork == current == FiberRoot。
就整个流程来说。React在初次完成后,都会拥有一个current树,它的内部数据对应到整个UI上。如果后续有更新rootWorkInProgress树会被重新构建。
updateQueue树 updateQueue也是一个链表,不过它和FiberNode不一样,只有next一个链接属性。
updateQueue的脉络还是得从render这里入手。回顾之前的render文章,看看调用栈。我们可以发现,初始render第一次处理它,是在scheduleRootUpdate函数中的enqueueUpdate(current, update)
。
这里针对FiberRoot设置的updateQueue是一个初始值。里面的callback值来源于上级调用。它在ReactRoot.prototype.render函数中。
1 2 const work = new ReactWork();updateContainer(children, root, null , work._onCommit);
配合processUpdateQueue函数,可以很容易知道fiberRoot上的初始updateQueue是一个callback设为work._onCommit的初始值。
Side-Effect树 我们可以将 React 中的一个组件视为一个使用 state 和 props 来计算 UI 表示的函数。改变 DOM 或调用生命周期方法,被视为Side-Effect。
在v15版本中DOM树通过它的_owner属性是否为null来判断是否是一个自定义组件。它的更新纯粹是判断props是否有变更来处理后续生命周期&渲染。v16这里则是在FiberNode上记录了一个nextEffect属性来标记下一个自定义组件——v16叫类组件,它里面有生命周期之类的Side-Effect操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 enqueueSetState(inst, payload, callback) { // 此时inst为App实例 payload={text: 'Hello World'} callback=undefined 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节点上的更新队列才是实质核心。
这里enqueueUpdate主要是调用appendUpdateToQueue。这个函数基本可以理解为向update数组push一个元素。
1 2 3 4 5 6 7 8 9 10 function appendUpdateToQueue(queue, update) { // lastUpdate===null说明之前是空的队列 if (queue.lastUpdate === null) { queue.firstUpdate = queue.lastUpdate = update; } else { // 否则将update放到链表队列尾部 queue.lastUpdate.next = update; queue.lastUpdate = update; } }
仔细想想,在v15进行Diff阶段,实质上是进行Diff对比中的ComponentDiff环节。这个环节如果props变化不会直接生成新的替换下级所有旧的节点。这里换了链表,但是这个对比环节还是需要依据。这里想来就是nextEffect属性存在的意义了。它定位了所有ClassComponent方便进行ComponentDiff。
但是需要注意的是: 这个链表的顺序,它不是按层级做顺序链接的。而是基于我们的FiberNode遍历理论,在这个遍历过程中,遇到ClassComponent时候,按照先后顺序进行连接、遍历。这里偷懒偷个图。。。
这里关于组件对比的想法仅作为猜想了,后面看Update环节时候再做确认。 这里疑问是,当这个next不再以DOM层级作为依据,底部组件可能比上层组件更先遍历到,那么问题在于如果下级和上级同时变化了,如果下级先处理一些事情会不会造成浪费?
这里问题暂且留到Update环节分析。