【JS】React和DOM的那些事-节点新增算法

React和DOM的那些事-节点新增算法

nero发布于 10 分钟前

点击进入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函数的功能如下:

  1. 找到目标节点DOM层面的父节点(parent)
  2. 根据目标节点类型,找到对应的parent
  3. 如果目标节点对应的DOM节点目前只有文字内容,类似<div>hello</div>,并且持有ContentReset(内容重置)的effectTag,那么插入节点之前先设置一下文字内容
  4. 找到基准节点
  5. 执行插入

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上的containerInfo

parent = 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节点的过程可以总结为:

  1. 优先查找同级兄弟节点,过滤出原生DOM组件。
  2. 过滤不出来就查找同级节点的子节点,过滤出原生DOM组件。
  3. 重复查找兄弟节点再查找子节点的过程,直到再也找不到兄弟节点。
  4. 向上查找到父节点,兄对父节点也重复前三步。
  5. 直到过滤出原生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阶段插入节点过程的特点:

  1. 定位DOM节点插入的正确位置
  2. 避免DOM节点的多余插入

找到基准节点before是第1点的关键,有了基准节点就能知道即将插入的父级节点上是否有已经存在,并且位置在目标节点之后的子节点。根据有无基准节点来决定执行哪种插入策略。

如何避免DOM节点的多余插入呢?上面分析插入过程的时候已经讲过,只会将目标节点的第一层子DOM节点插入到正确的位置,因为子DOM节点的插入工作已经完成了。这和effectList中收集的fiber节点的顺序有关,因为是自下而上收集的,所以fiber的顺序也是自下而上,导致DOM节点的插入也是自下而上的,可以类比一下累加的过程。

如下,可以看到最终的effectList中,最下层的节点排在最前面:

【JS】React和DOM的那些事-节点新增算法

以上,是依据Fiber树插入DOM节点的过程。

欢迎扫码关注公众号,发现更多技术文章

【JS】React和DOM的那些事-节点新增算法

javascript前端react.js

阅读 14发布于 10 分钟前

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议


React的秘密

最平实的语言,解读复杂React

avatar

nero

前端工程师

3.5k 声望

1.8k 粉丝

0 条评论

得票时间

avatar

nero

前端工程师

3.5k 声望

1.8k 粉丝

宣传栏

点击进入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函数的功能如下:

  1. 找到目标节点DOM层面的父节点(parent)
  2. 根据目标节点类型,找到对应的parent
  3. 如果目标节点对应的DOM节点目前只有文字内容,类似<div>hello</div>,并且持有ContentReset(内容重置)的effectTag,那么插入节点之前先设置一下文字内容
  4. 找到基准节点
  5. 执行插入

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上的containerInfo

parent = 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节点的过程可以总结为:

  1. 优先查找同级兄弟节点,过滤出原生DOM组件。
  2. 过滤不出来就查找同级节点的子节点,过滤出原生DOM组件。
  3. 重复查找兄弟节点再查找子节点的过程,直到再也找不到兄弟节点。
  4. 向上查找到父节点,兄对父节点也重复前三步。
  5. 直到过滤出原生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阶段插入节点过程的特点:

  1. 定位DOM节点插入的正确位置
  2. 避免DOM节点的多余插入

找到基准节点before是第1点的关键,有了基准节点就能知道即将插入的父级节点上是否有已经存在,并且位置在目标节点之后的子节点。根据有无基准节点来决定执行哪种插入策略。

如何避免DOM节点的多余插入呢?上面分析插入过程的时候已经讲过,只会将目标节点的第一层子DOM节点插入到正确的位置,因为子DOM节点的插入工作已经完成了。这和effectList中收集的fiber节点的顺序有关,因为是自下而上收集的,所以fiber的顺序也是自下而上,导致DOM节点的插入也是自下而上的,可以类比一下累加的过程。

如下,可以看到最终的effectList中,最下层的节点排在最前面:

【JS】React和DOM的那些事-节点新增算法

以上,是依据Fiber树插入DOM节点的过程。

欢迎扫码关注公众号,发现更多技术文章

【JS】React和DOM的那些事-节点新增算法

以上是 【JS】React和DOM的那些事-节点新增算法 的全部内容, 来源链接: utcz.com/a/107130.html

回到顶部