Fiber 树的构建
我们先来看一个简单的 demo:
import * as react from 'react'; import * as reactdom from 'react-dom'; class app extends react.component { render() { return ( <div classname="container"> <div classname="section"> <h1>this is the title.</h1> <p>this is the first paragraph.</p> <p>this is the second paragraph.</p> </div> </div> ); } } reactdom.render(<app />, document.getelementbyid('root'));
首次渲染的调用栈如下图
以 performsyncworkonroot 和 commitroot 两个方法为界限,可以把 reactdom.render 分为三个阶段:
- init
- render
- commit
init phase
render
很简单,直接调用 legacyrendersubtreeintocontainer。
export function render( element: react$element<any>, container: container, callback: ?function, ) { // 省略对 container 的校验逻辑 return legacyrendersubtreeintocontainer( null, element, container, false, callback, ); }
这里需要注意一点,此时的 element 已经不是 render 中传入的
legacyrendersubtreeintocontainer
在这里我们可以看到方法取名的重要性,一个好的方法名可以让你一眼就看出这个方法的作用。legacyrendersubtreeintocontainer,顾名思义,这是一个遗留的方法,作用是渲染子树并将其挂载到 container 上。再来看一下入参,children 和 container 分别是之前传入 render 方法的 app 元素和 id 为 root 的 dom 元素,所以可以看出这个方法会根据 app 元素生成对应的 dom 树,并将其挂在到 root 元素上。
function legacyrendersubtreeintocontainer( parentcomponent: ?react$component<any, any>, children: reactnodelist, container: container, forcehydrate: boolean, callback: ?function, ) { let root: roottype = (container._reactrootcontainer: any); let fiberroot; if (!root) { root = container._reactrootcontainer = legacycreaterootfromdomcontainer( container, forcehydrate, ); fiberroot = root._internalroot; // 省略对 callback 的处理逻辑 unbatchedupdates(() => { updatecontainer(children, fiberroot, parentcomponent, callback); }); } else { // 省略 else 逻辑 } return getpublicrootinstance(fiberroot); }
下面来细看一下这个方法:
- 首次挂载时,会通过 legacycreaterootfromdomcontainer 方法创建 container.reactrootcontainer 对象并赋值给 root。 container 对象现在长这样:
- 初始化 fiberroot 为 root.internalroot,类型为 fiberrootnode。fiberroot 有一个极其重要的 current 属性,类型为 fibernode,而 fibernode 为 fiber 节点的对应的类型。所以说 current 对象是一个 fiber 节点,不仅如此,它还是我们要构造的 fiber 树的头节点,我们称它为 rootfiber。到目前为止,我们可以得到下图的指向关系:
- 将 fiberroot 以及其它参数传入 updatecontainer 形成回调函数,将回调函数传入 unbatchedupdates 并调用。
unbatchedupdates
主要逻辑就是调用回调函数 fn,也就是之前传入的 updatecontainer。
export function unbatchedupdates<a, r>(fn: (a: a) => r, a: a): r { const prevexecutioncontext = executioncontext; executioncontext &= ~batchedcontext; executioncontext |= legacyunbatchedcontext; try { // fn 为之前传入的 updatecontainer return fn(a); } finally { executioncontext = prevexecutioncontext; if (executioncontext === nocontext) { resetrendertimer(); flushsynccallbackqueue(); } } }
updatecontainer
updatecontainer 方法做的还是一些杂活,我们简单总结一下:
- 计算当前 fiber 节点的 lane(优先级)。
- 根据 lane(优先级),创建当前 fiber 节点的 update 对象,并将其入队。
- 调度当前 fiber 节点(rootfiber)。
export function updatecontainer( element: reactnodelist, container: opaqueroot, parentcomponent: ?react$component<any, any>, callback: ?function, ): lane { const current = container.current; const eventtime = requesteventtime(); // 计算当前节点的 lane(优先级) const lane = requestupdatelane(current); if (enableschedulingprofiler) { markrenderscheduled(lane); } const context = getcontextforsubtree(parentcomponent); if (container.context === null) { container.context = context; } else { container.pendingcontext = context; } // 根据 lane(优先级)计算当前节点的 update 对象 const update = createupdate(eventtime, lane); update.payload = {element}; callback = callback === undefined ? null : callback; if (callback !== null) { update.callback = callback; } // 将 update 对象入队 enqueueupdate(current, update); // 调度当前 fiber节点(rootfiber) scheduleupdateonfiber(current, lane, eventtime); return lane; }
scheduleupdateonfiber
接着会进入 scheduleupdateonfiber 方法,根据 lane(优先级)等于 synclane,代码最终会执行 performsyncworkonroot 方法。performsyncworkonroot 翻译过来,就是指执行根节点(rootfiber)的同步任务,所以 reactdom.render 的首次渲染其实是一个同步的过程。
到这里大家可能会有个疑问,为什么 reactdom.render 触发的首次渲染是一个同步的过程呢?不是说在新的 fiber 架构下,render 阶段是一个可打断的异步过程。
我们先来看看 lane 是怎么计算得到的,相关逻辑在 updatecontainer 中的 requestupdatelane 方法里:
export function requestupdatelane(fiber: fiber): lane { const mode = fiber.mode; if ((mode & blockingmode) === nomode) { return (synclane: lane); } else if ((mode & concurrentmode) === nomode) { return getcurrentprioritylevel() === immediateschedulerpriority ? (synclane: lane) : (syncbatchedlane: lane); } else if ( !deferrenderphaseupdatetonextbatch && (executioncontext & rendercontext) !== nocontext && workinprogressrootrenderlanes !== nolanes ) { return pickarbitrarylane(workinprogressrootrenderlanes); } // 省略非核心代码 }
可以看出 lane 的计算是由当前 fiber 节点(rootfiber)的 mode 属性决定的,这里的 mode 属性其实指的就是当前 fiber 节点的渲染模式,而 rootfiber 的 mode 属性其实最终是由 react 的启动方式决定的。
react 其实有三种启动模式:
- legacy mode:
reactdom.render(<app />, rootnode)
。这是目前 react app 使用的方式,当前没有删除这个模式的计划,但是这个模式不支持一些新的功能。 - blocking mode:
reactdom.createblockingroot(rootnode).render(<app />)
。目前正在实验中,作为迁移到 concurrent 模式的第一个步骤。 - concurrent mode:
reactdom.createroot(rootnode).render(<app />)
。目前正在实验中,在未来稳定之后,将作为 react 的默认启动方式。此模式启用所有新功能。
因此不同的渲染模式在挂载阶段的差异,本质上来说并不是工作流的差异(其工作流涉及 初始化 → render → commit 这 3 个步骤),而是 mode 属性的差异。mode 属性决定着这个工作流是一气呵成(同步)的,还是分片执行(异步)的。
render phase
performsyncworkonroot
核心是调用 renderrootsync 方法
renderrootsync
有两个核心方法 preparefreshstack 和 workloopsync,下面来逐个分析。
preparefreshstack
首先调用 preparefreshstack 方法,preparefreshstack 中有一个重要的方法 createworkinprogress。
export function createworkinprogress(current: fiber, pendingprops: any): fiber { let workinprogress = current.alternate; if (workinprogress === null) { // 通过 current 创建 workinprogress workinprogress = createfiber( current.tag, pendingprops, current.key, current.mode, ); workinprogress.elementtype = current.elementtype; workinprogress.type = current.type; workinprogress.statenode = current.statenode; // 使 workinprogress 与 current 通过 alternate 相互指向 workinprogress.alternate = current; current.alternate = workinprogress; } else { // 省略 else 逻辑 } // 省略对 workinprogress 属性的处理逻辑 return workinprogress; }
下面我们来看一下 workinprogress 究竟是什么?workinprogress 是 createfiber 的返回值,接着来看一下 createfiber。
const createfiber = function( tag: worktag, pendingprops: mixed, key: null | string, mode: typeofmode, ): fiber { return new fibernode(tag, pendingprops, key, mode); };
可以看出 createfiber 其实就是在创建一个 fiber 节点。所以说 workinprogress 其实就是一个 fiber 节点。
从 createworkinprogress 中,我们还可以看出:
- workinprogress 节点是 current 节点(rootfiber)的一个副本。
- workinprogress 节点与 current 节点(rootfiber)通过 alternate 属性相互指向。
所以到现在为止,我们的 fiber 树如下:
workloopsync
接下来调用 workloopsync 方法,代码很简单,若 workinprogress 不为空,调用 performunitofwork 处理 workinprogress 节点。
function workloopsync() { while (workinprogress !== null) { performunitofwork(workinprogress); } }
performunitofwork
performunitofwork 有两个重要的方法 beginwork 和 completeunitofwork,在 fiber 的构建过程中,我们只需重点关注 beginwork 这个方法。
function performunitofwork(unitofwork: fiber): void { const current = unitofwork.alternate; setcurrentdebugfiberindev(unitofwork); let next; if (enableprofilertimer && (unitofwork.mode & profilemode) !== nomode) { startprofilertimer(unitofwork); next = beginwork(current, unitofwork, subtreerenderlanes); stopprofilertimerifrunningandrecorddelta(unitofwork, true); } else { next = beginwork(current, unitofwork, subtreerenderlanes); } resetcurrentdebugfiberindev(); unitofwork.memoizedprops = unitofwork.pendingprops; if (next === null) { completeunitofwork(unitofwork); } else { workinprogress = next; } reactcurrentowner.current = null; }
目前我们只能看出,它会对当前的 workinprogress 节点进行处理,至于怎么处理的,当我们解析完 beginwork 方法再来总结 performunitofwork 的作用。
beginwork
根据 workinprogress 节点的 tag 进行逻辑分发。tag 属性代表的是当前 fiber 节点的类型,常见的有下面几种:
- functioncomponent:函数组件(包括 hooks)
- classcomponent:类组件
- hostroot:fiber 树根节点
- hostcomponent:dom 元素
- hosttext:文本节点
function beginwork( current: fiber | null, workinprogress: fiber, renderlanes: lanes, ): fiber | null { // 省略非核心(针对树构建)逻辑 switch (workinprogress.tag) { // 省略部分 case 逻辑 // 函数组件(包括 hooks) case functioncomponent: { const component = workinprogress.type; const unresolvedprops = workinprogress.pendingprops; const resolvedprops = workinprogress.elementtype === component ? unresolvedprops : resolvedefaultprops(component, unresolvedprops); return updatefunctioncomponent( current, workinprogress, component, resolvedprops, renderlanes, ); } // 类组件 case classcomponent: { const component = workinprogress.type; const unresolvedprops = workinprogress.pendingprops; const resolvedprops = workinprogress.elementtype === component ? unresolvedprops : resolvedefaultprops(component, unresolvedprops); return updateclasscomponent( current, workinprogress, component, resolvedprops, renderlanes, ); } // 根节点 case hostroot: return updatehostroot(current, workinprogress, renderlanes); // dom 元素 case hostcomponent: return updatehostcomponent(current, workinprogress, renderlanes); // 文本节点 case hosttext: return updatehosttext(current, workinprogress); // 省略部分 case 逻辑 } // 省略匹配不上的错误处理 }
当前的 workinprogress 节点为 rootfiber,tag 对应为 hostroot,会调用 updatehostroot 方法。
rootfiber 的 tag(hostroot)是什么来的?核心代码如下:
export function createhostrootfiber(tag: roottag): fiber { // 省略非核心代码 return createfiber(hostroot, null, null, mode); }
在创建 rootfiber 节点的时候,直接指定了 tag 参数为 hostroot。
updatehostroot
updatehostroot 的主要逻辑如下:
- 调用 reconcilechildren 方法创建 workinprogress.child。
- 返回 workinprogress.child。
function updatehostroot(current, workinprogress, renderlanes) { // 省略非核心逻辑 if (root.hydrate && enterhydrationstate(workinprogress)) { // 省略 if 成立的逻辑 } else { reconcilechildren(current, workinprogress, nextchildren, renderlanes); resethydrationstate(); } return workinprogress.child; }
这里有一点需要注意,通过查看源码,你会发现不仅是 updatehostroot 方法,所以的更新方法最终都会调用下面这个方法:
reconcilechildren(current, workinprogress, nextchildren, renderlanes);
只是针对不同的节点类型,会有一些不同的处理,最终殊途同归。
reconcilechildren
reconcilechildren 根据 current 是否为空进行逻辑分发。
export function reconcilechildren( current: fiber | null, workinprogress: fiber, nextchildren: any, renderlanes: lanes, ) { if (current === null) { workinprogress.child = mountchildfibers( workinprogress, null, nextchildren, renderlanes, ); } else { workinprogress.child = reconcilechildfibers( workinprogress, current.child, nextchildren, renderlanes, ); } }
此时 current 节点不为空,会走 else 逻辑,调用 reconcilechildfibers 创建 workinprogress.child 对象。
reconcilechildfibers
根据 newchild 的类型进行不同的逻辑处理。
function reconcilechildfibers( returnfiber: fiber, currentfirstchild: fiber | null, newchild: any, lanes: lanes, ): fiber | null { // 省略非核心代码 const isobject = typeof newchild === 'object' && newchild !== null; if (isobject) { switch (newchild.$$typeof) { case react_element_type: return placesinglechild( reconcilesingleelement( returnfiber, currentfirstchild, newchild, lanes, ), ); // 省略其他 case 逻辑 } } // 省略非核心代码 if (isarray(newchild)) { return reconcilechildrenarray( returnfiber, currentfirstchild, newchild, lanes, ); } // 省略非核心代码 }
newchild 很关键,我们先明确一下 newchild 究竟是什么?通过层层向上寻找,你会在 updatehostroot 方法中发现它其实是最开始传入 render 方法的 app 元素,它在 updatehostroot 中被叫做 nextchildren,到这里我们可以做出这样的猜想,rootfiber 的下一个是 app 节点,并且 app 节点是由 app 元素生成的,下面来看一下 newchild 的结构:
可以看出 newchild 类型为 object,$$typeof 属性为 react_element_type,所以会调用:
placesinglechild( reconcilesingleelement( returnfiber, currentfirstchild, newchild, lanes, ), );
reconcilesingleelement
下面继续看 reconcilesingleelement 这个方法:
function reconcilesingleelement( returnfiber: fiber, currentfirstchild: fiber | null, element: reactelement, lanes: lanes, ): fiber { const key = element.key; let child = currentfirstchild; // 省略 child 不存在的处理逻辑 if (element.type === react_fragment_type) { // 省略 if 成立的处理逻辑 } else { const created = createfiberfromelement(element, returnfiber.mode, lanes); created.ref = coerceref(returnfiber, currentfirstchild, element); created.return = returnfiber; return created; } }
方法的调用比较深,我们先明确一下入参,returnfiber 为 workinprogress 节点,element 其实就是传入的 newchild,也就是 app 元素,所以这个方法的作用为:
- 调用 createfiberfromelement 方法根据 app 元素创建 app 节点。
- 将新生成的 app 节点的 return 属性指向当前 workinprogress 节点(rootfiber)。此时 fiber 树如下图:
- 返回 app 节点。
placesinglechild
接下来调用 placesinglechild:
function placesinglechild(newfiber: fiber): fiber { if (shouldtracksideeffects && newfiber.alternate === null) { newfiber.flags = placement; } return newfiber; }
入参为之前创建的 app 节点,它的作用为:
- 当前的 app 节点打上一个 placement 的 flags,表示新增这个节点。
- 返回 app 节点。
之后 app 节点会被一路返回到的 reconcilechildren 方法:
workinprogress.child = reconcilechildfibers( workinprogress, current.child, nextchildren, renderlanes, );
此时 workinprogress 节点的 child 属性会指向 app 节点。此时 fiber 树为:
beginwork 小结
beginwork 的链路比较长,我们来梳理一下:
- 根据 workinprogress.tag 进行逻辑分发,调用形如 updatehostroot、updateclasscomponent 等更新方法。
- 所有的更新方法最终都会调用 reconcilechildren,reconcilechildren 根据 current 进行简单的逻辑分发。
- 之后会调用 mountchildfibers/reconcilechildfibers 方法,它们的作用是根据 reactelement 对象生成 fiber 节点,并打上相应的 flags,表示这个节点是新增,删除还是更新等等。
- 最终返回新创建的 fiber 节点。
简单来说就是创建新的 fiber 字节点,并将其挂载到 fiber 树上,最后返回新创建的子节点。
performunitofwork 小结
下面我们来小结一下 performunitofwork 这个方法,先来回顾一下 workloopsync 方法。
function workloopsync() { while (workinprogress !== null) { performunitofwork(workinprogress); } }
它会循环执行 performunitofwork,而 performunitofwork,我们已经知道它会通过 beginwork 创建新的 fiber 节点。它还有另外一个作用,那就是把 workinprogress 更新为新创建的 fiber 节点,相关逻辑如下:
// 省略非核心代码 // beginwork 返回新创建的 fiber 节点并赋值给 next next = beginwork(current, unitofwork, subtreerenderlanes); // 省略非核心代码 if (next === null) { completeunitofwork(unitofwork); } else { // 若 fiber 节点不为空则将 workinprogress 更新为新创建的 fiber 节点 workinprogress = next; }
所以当 performunitofwork 执行完,当前的 workinprogress 都存储着下次要处理的 fiber 节点,为下一次的 workloopsync 做准备。
performunitofwork 作用总结如下:
- 通过调用 beginwork 创建新的 fiber 节点,并将其挂载到 fiber 树上
- 将 workinprogress 更新为新创建的 fiber 节点。
app 节点的处理
rootfiber 节点处理完成之后,对应的 fiber 树如下:
接下来 performunitofwork 会开始处理 app 节点。app 节点的处理过程大致与 rootfiber 节点类似,就是调用 beginwork 创建新的子节点,也就是 classname 为 container 的 div 节点,处理完成之后的 fiber 树如下:
这里有一个很关键的地方需要大家注意。我们先回忆一下对 rootfiber 的处理,针对 rootfiber,我们已经知道在 updatehostroot 中,它会提取出 nextchildren,也就是最初传入 render 方法的 element。
那针对 app 节点,它是如何获取 nextchildren 的呢?先来看下我们的 app 组件:
class app extends react.component { render() { return ( <div classname="container"> <div classname="section"> <h1>this is the title.</h1> <p>this is the first paragraph.</p> <p>this is the second paragraph.</p> </div> </div> ); } }
我们的 app 是一个 class,react 首先会实例化会它:
之后会把生成的实例挂在到当前 workinprogress 节点,也就是 app 节点的 statenode 属性上:
然后在 updateclasscomponent 方法中,会先初始化 instance 为 workinprogress.statenode,之后调用 instance 的 render 方法并赋值给 nextchildren:
此时的 nextchildren 为下面 jsx 经过 react.createelement 转化后的结果:
<div classname="container"> <div classname="section"> <h1>this is the title.</h1> <p>this is the first paragraph.</p> <p>this is the second paragraph.</p> </div> </div>
接着来看一下 nextchildren 长啥样:
props.children 存储的是其子节点,它可以是对象也可以是数组。对于 app 节点和第一个 div 节点,它们都只有一个子节点。对于第二个 div 节点,它有三个子节点,分别是 h1、p、p,所以它的 children 为数组。
并且 props 还会保存在新生成的 fiber 节点的 pendingprops 属性上,相关逻辑如下:
export function createfiberfromelement( element: reactelement, mode: typeofmode, lanes: lanes, ): fiber { let owner = null; const type = element.type; const key = element.key; const pendingprops = element.props; const fiber = createfiberfromtypeandprops( type, key, pendingprops, owner, mode, lanes, ); return fiber; } export function createfiberfromtypeandprops( type: any, // react$elementtype key: null | string, pendingprops: any, owner: null | fiber, mode: typeofmode, lanes: lanes, ): fiber { // 省略非核心逻辑 const fiber = createfiber(fibertag, pendingprops, key, mode); fiber.elementtype = type; fiber.type = resolvedtype; fiber.lanes = lanes; return fiber; }
第一个 div 节点的处理
app 节点的 nextchildren 是通过构造实例并调用 app 组件内的 render 方法得到的,那对于第一个 div 节点,它的 nextchildren 是如何获取的呢?
针对 div 节点,它的 tag 为 hostcomponent,所以在 beginwork 中会调用 updatehostcomponent 方法,可以看出 nextchildren 是从当前 workinprogress 节点的 pendingprops 上获取的。
function updatehostcomponent( current: fiber | null, workinprogress: fiber, renderlanes: lanes, ) { // 省略非核心逻辑 const nextprops = workinprogress.pendingprops; // 省略非核心逻辑 let nextchildren = nextprops.children; // 省略非核心逻辑 reconcilechildren(current, workinprogress, nextchildren, renderlanes); return workinprogress.child; }
我们之前说过,在创建新的 fiber 节点时,我们会把下一个子节点元素保存在 pendingprops 中。当下次调用更新方法(形如 updatehostcomponent )时,我们就可以直接从 pendingprops 中获取下一个子元素。
之后的逻辑同上,处理完第一个 div 节点后的 fiber 树如下图:
第二个 div 节点的处理
我们先看一下第二个 div 节点:
<div classname="section"> <h1>this is the title.</h1> <p>this is the first paragraph.</p> <p>this is the second paragraph.</p> </div>
它比较特殊,有三个字节点,对应的 nextchildren 为
下面我们来看看 react 是如何处理多节点的情况,首先我们还是会进入 reconcilechildfibers 这个方法:
function reconcilechildfibers( returnfiber: fiber, currentfirstchild: fiber | null, newchild: any, lanes: lanes, ): fiber | null { // 省略非核心代码 if (isarray(newchild)) { return reconcilechildrenarray( returnfiber, currentfirstchild, newchild, lanes, ); } // 省略非核心代码 }
newchild 即是 nextchildren,为数组,会调用 reconcilechildrenarray 这个方法
function reconcilechildrenarray( returnfiber: fiber, currentfirstchild: fiber | null, newchildren: array<*>, lanes: lanes, ): fiber | null { // 省略非核心逻辑 let previousnewfiber: fiber | null = null; let oldfiber = currentfirstchild; // 省略非核心逻辑 if (oldfiber === null) { for (; newidx < newchildren.length; newidx++) { const newfiber = createchild(returnfiber, newchildren[newidx], lanes); if (newfiber === null) { continue; } lastplacedindex = placechild(newfiber, lastplacedindex, newidx); if (previousnewfiber === null) { resultingfirstchild = newfiber; } else { previousnewfiber.sibling = newfiber; } previousnewfiber = newfiber; } return resultingfirstchild; } // 省略非核心逻辑 }
下面来总结一下这个方法:
- 遍历所有的子元素,通过 createchild 方法根据子元素创建子节点,并将每个字元素的 return 属性指向父节点。
- 用 resultingfirstchild 来标识第一个子元素。
- 将子元素用 sibling 相连。
最后我们的 fiber 树就构建完成了,如下图:
上一篇: 工厂模式之二——工厂方法模式
推荐阅读
-
《Node.js项目实践:构建可扩展的Web应用》
-
6月读书活动之《Node.js项目实践:构建可扩展的Web应用
-
vue构建动态表单的方法示例
-
【树】B051_LC_二叉树中所有距离为 K 的结点(广搜 + 深搜存储父节点)
-
EasyNVR摄像机网页无插件直播方案H5前端构建之:bootstrap弹窗功能的实现方案与代码
-
用 Composer构建自己的 PHP 框架之基础准备,composer构建
-
构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(24)-权限管理系统-将权限授权给角色
-
搜索与图论 - 树的重心
-
数据结构_图的应用_最小生成树1(普里姆算法)
-
搜索与图论---DFS和BFS、树与图的存储和遍历