点击进入React源码调试仓库。
本篇是具体解读React DOM操作的第二篇文章,文章所讲的内容产生在commit阶段。
插入DOM节点操作的是fiber节点上的stateNode,对于原生DOM类型的fiber节点来说stateNode存储着DOM节点。commit阶段插入节点的操作就是循着fiber树把DOM节点插入到实在的DOM树中。
commitPlacement
是插入节点的入口,
function commitMutationEffectsImpl( fiber: Fiber, root: FiberRoot, renderPriorityLevel,) { ... switch (primaryEffectTag) { case Placement: { // 插入操作 commitPlacement(fiber); fiber.effectTag &= ~Placement; break; } ... }}
咱们将须要被执行插入操作的fiber节点称为指标节点,commitPlacement
函数的性能如下:
- 找到指标节点DOM层面的父节点(parent)
- 依据指标节点类型,找到对应的parent
- 如果指标节点对应的DOM节点目前只有文字内容,相似
<div>hello</div>
,并且持有ContentReset(内容重置)的effectTag,那么插入节点之前先设置一下文字内容 - 找到基准节点
- 执行插入
function commitPlacement(finishedWork: Fiber): void { ... // 找到指标节点DOM层面的父节点(parent) const parentFiber = getHostParentFiber(finishedWork); // 依据指标节点类型,扭转parent let parent; let isContainer; const parentStateNode = parentFiber.stateNode; switch (parentFiber.tag) { case HostComponent: parent = parentStateNode; isContainer = false; break; case HostRoot: parent = parentStateNode.containerInfo; isContainer = true; break; case HostPortal: parent = parentStateNode.containerInfo; isContainer = true; break; case FundamentalComponent: if (enableFundamentalAPI) { parent = parentStateNode.instance; isContainer = false; } } if (parentFiber.effectTag & ContentReset) { // 插入之前重设文字内容 resetTextContent(parent); // 删除ContentReset的effectTag parentFiber.effectTag &= ~ContentReset; } // 找到基准节点 const before = getHostSibling(finishedWork); // 执行插入操作 if (isContainer) { // 在内部DOM节点上插入 insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); } else { // 间接在父节点插入 insertOrAppendPlacementNode(finishedWork, before, parent); }}
这里要明确的一点是DOM节点插入到哪,也就是要依据指标节点类型,找到对应的parent。
如果是HostRoot
或者HostPortal
类型的节点,第一它们都没有对应的DOM节点,第二理论渲染时它们会将DOM子节点渲染到对应的内部节点上(containerInfo)。
所以当fiber节点类型为这两个时,就将节点插入到这个内部节点上,即:
// 将parent赋值为fiber上的containerInfoparent = parentStateNode.containerInfo...// 插入到内部节点(containerInfo)中insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent)
如果是HostComponent
,则间接向它的父级DOM节点中插入,即
// 间接在父节点插入insertOrAppendPlacementNode(finishedWork, before, parent);
咱们看到,在理论执行插入的时候,都有一个before
参加,那它是干什么的呢?
定位基准节点
React插入节点的时候,分两种状况,新插入的DOM节点在它插入的地位是否曾经有兄弟节点,没有,执行parentInstance.appendChild(child)
,有,调用parentInstance.insertBefore(child, beforeChild)
。这个beforeChild
就是上文提到的before
,它是新插入的DOM节点的基准节点,有了它才能够在父级DOM节点曾经存在子节点的状况下,将新节点插入到正确的地位。试想如果曾经有子节点还用parentInstance.appendChild(child)
去插入,那是不是就把新节点插入到最开端了?这显然是不对的。所以找到before的地位非常重要,before
(基准节点)通过getHostSibling
函数来定位到。
咱们用一个例子来阐明一下getHostSibling
的原理:
p为新生成的DOM节点。a为已存在且无变动的DOM节点。它们在fiber树中的地位如下,p须要插入到DOM树中,咱们能够依据这棵fiber树来推断出最终的DOM树状态。
Fiber树 DOM树 div#root div#root | | <App/> div | / \ div p a / ↖ / ↖ Placement --> p ----> <Child/> | a
能够看到,在Fiber树中,a是p的父节点的兄弟节点,而在DOM树中,p和a是兄弟节点的关系,p最初要插入到a之前。
依照以上的图示咱们来推导一下过程:
p有兄弟节点<Child/>
,它有子节点a,a是一个原生DOM节点,并且a已存在于DOM树,那么a作为后果返回,p插入到a之前。
再来一个例子,p同样也是新插入的节点,h1作为已有节点存在于DOM树中。
Fiber树 DOM树 div#root div#root | | <App/> div | / \ div p h1 / ↖ / ↖ <Child1/>--><Child2/> | | | | Placement ---> p h1
p没有兄弟节点,往上找到<Child1/>
,它有兄弟节点<Child2/>
,<Child2/>
不是原生DOM节点,找<Child2/>
的子节点,发现了h1,h1是原生DOM节点并且h1已存在于DOM树,那么h1作为后果返回,p插入到h1之前。
通过两个例子,getHostSibling寻找到新插入节点的兄弟DOM节点的过程能够总结为:
- 优先查找同级兄弟节点,过滤出原生DOM组件。
- 过滤不进去就查找同级节点的子节点,过滤出原生DOM组件。
- 反复查找兄弟节点再查找子节点的过程,直到再也找不到兄弟节点。
- 向上查找到父节点,兄对父节点也反复前三步。
- 直到过滤出原生DOM节点,如果该DOM节点不是须要插入的节点,那么它作为后果返回,也就是定位到了
before
(基准节点),新节点须要插入到它的后面。
这其中有如下法则:
须要插入的节点
如果有同级fiber节点且是原生DOM节点,那么它肯定是插入到这个节点之前的。如果同级节点不是原生DOM节点,那么它和同级节点的子节点在DOM层面是兄弟节点
的关系。
须要插入的节点
如果没有同级节点,那么它和父节点的兄弟节点的子节点在DOM层面是兄弟节点
的关系。
基准节点和指标节点在DOM树中是兄弟关系,且它的地位肯定在指标节点之后
接下来依照下面总结的过程看一下它源码:
function getHostSibling(fiber: Fiber): ?Instance { let node: Fiber = fiber; siblings: while (true) { while (node.sibling === null) { if (node.return === null || isHostParent(node.return)) { // 代码执行到这里阐明没有兄弟节点,并且新节点的父节点为DOM节点, // 那么它将作为惟一的节点,插入父节点 return null; } // 如果父节点不为null且不是原生DOM节点,那么持续往上找 node = node.return; } // 首先从兄弟节点里找基准节点 node.sibling.return = node.return; node = node.sibling; // 如果node不是以下三种类型的节点,阐明必定不是基准节点, // 因为基准节点的要求是DOM节点 // 会始终循环到node为dom类型的fiber为止。 // 一旦进入循环,此时的node必然不是最开始是传进来的fiber while ( node.tag !== HostComponent && node.tag !== HostText && node.tag !== DehydratedFragment ) { if (node.effectTag & Placement) { // 如果这个节点也要被插入,持续siblings循环,找它的基准节点 continue siblings; } if (node.child === null || node.tag === HostPortal) { // node无子节点,或者遇到了HostPortal节点,持续siblings循环, // 找它的基准节点。 // 留神,此时会再进入siblings循环,循环的开始,也就是上边的代码 // 会判断这个节点有没有siblings,没有就向上找,有就从siblings里找。 continue siblings; } else { // 过滤不进去原生DOM节点,但它有子节点,就持续往下找。 node.child.return = node; node = node.child; } } if (!(node.effectTag & Placement)) { // 过滤出原生DOM节点了,并且这个节点不须要动, // stateNode就作为基准节点返回 return node.stateNode; } }}
此时基准节点曾经找到,接下来执行插入操作。
插入节点
插入节点操作的是DOM树,除了插入指标节点,还须要遍历它的fiber子树,保障所有子DOM节点都被插入,遍历的过程是深度优先。
咱们以将节点插入父节点的insertOrAppendPlacementNode
函数为主,来梳理一下插入的过程。
Fiber树 div#root | <App/> | div#App |Placement --> <Child/> / / p ------> span ----- h1 | a
当初要将<Child/>
插入到div#App
中,实在的插入过程是先找到div#App作为parent,尔后再找<Child/>
是否有sibling节点,而后调用insertOrAppendPlacementNode
来执行插入操作。
来看一下它的调用形式:
insertOrAppendPlacementNode(finishedWork, before, parent);
一共有三个参数:
- finishedWork:就是须要插入的fiber节点,以后是
<Child/>
- before:
<Child/>
的sibling节点,该场景下为null - parent:
<Child/>
的parent节点,也就是div#App
进入函数,它的工作是将DOM节点插入到parent之下或before之前,如果finishedWork是原生DOM节点,那么根据有无before来决定节点的插入方式,无论哪种形式都会将DOM实实在在地插入到正确的地位上。
如果不是原生DOM节点,就是<Child/>
这种,不能对它进行插入操作,那么怎么办呢?向下,从它的child切入,再次调用insertOrAppendPlacementNode
,也就是递归地调用本人,将child一个不剩地全插入到parent中。在例子中,会把p插入到parent。
此时<Child/>
的子节点曾经全副实现插入,这时会再找到p的兄弟节点span,对它进行插入,而后发现span还有兄弟节点h1,将h1也插入。
这就是节点插入的残缺过程。有一个特点与completeWork中的插入节点相似,也就是只将指标节点的第一层子DOM节点插入到正确的地位,因为子DOM节点的再上层的DOM节点曾经在解决该层的时候插入过了。
来对照着下面的过程看一下insertOrAppendPlacementNode
的源码
function insertOrAppendPlacementNode( node: Fiber, before: ?Instance, parent: Instance,): void { const {tag} = node; const isHost = tag === HostComponent || tag === HostText; if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) { // 如果是原生DOM节点,间接进行插入操作 const stateNode = isHost ? node.stateNode : node.stateNode.instance; if (before) { // 插入到基准节点之前 insertBefore(parent, stateNode, before); } else { // 插入到父节点之下 appendChild(parent, stateNode); } } else if (tag === HostPortal) { // HostPortal节点什么都不做 } else { // 不是原生DOM节点,找它的子节点 const child = node.child; if (child !== null) { // 对子节点进行插入操作 insertOrAppendPlacementNode(child, before, parent); // 而后找兄弟节点 let sibling = child.sibling; while (sibling !== null) { // 插入兄弟节点 insertOrAppendPlacementNode(sibling, before, parent); // 持续查看兄弟节点 sibling = sibling.sibling; } } }}
在递归调用insertOrAppendPlacementNode插入节点的时候也传入了before,这个before是最开始那个待插入的指标节点的基准节点。咱们来用源码中的两个场景看一下这样做的意义。
假如指标节点不是原生DOM节点,且有已存在DOM的兄弟节点(就是基准节点before,span):
- 有子节点,对子节点插入到div,最终的DOM状态是左边的DOM树,p尽管在fiber树里和span不是同级的关系,但在DOM层面是,所以要插入到span的后面,这是before在这种场景下存在的意义
Fiber树 DOM树 div div | / \Placement --> <Child/>----> span / \ | p span | p
- 子节点实现插入,最终造成的DOM树里,p、a、span三者是兄弟关系,p和a要顺次插入到span之前,所以这种场景也须要before。
Fiber树 DOM树 div div | /|\Placement --> <Child/>----> span / | \ | p a span | p ----> a
总结
结合实际插入节点产生的问题不难总结出commit阶段插入节点过程的特点:
- 定位DOM节点插入的正确地位
- 防止DOM节点的多余插入
找到基准节点before是第1点的要害,有了基准节点就能晓得行将插入的父级节点上是否有曾经存在,并且地位在指标节点之后的子节点。依据有无基准节点来决定执行哪种插入策略。
如何防止DOM节点的多余插入呢?下面剖析插入过程的时候曾经讲过,只会将指标节点的第一层子DOM节点插入到正确的地位,因为子DOM节点的插入工作曾经实现了。这和effectList中收集的fiber节点的程序无关,因为是自下而上收集的,所以fiber的程序也是自下而上,导致DOM节点的插入也是自下而上的,能够类比一下累加的过程。
如下,能够看到最终的effectList中,最上层的节点排在最后面:
以上,是根据Fiber树插入DOM节点的过程。
欢送扫码关注公众号,发现更多技术文章