Skip to content

Commit a39578a

Browse files
committed
2 parents a327ccc + 3de3a48 commit a39578a

File tree

5 files changed

+234
-142
lines changed

5 files changed

+234
-142
lines changed

docs/Hooks/概述.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11

22
# 组件的初始化和更新在hooks上的体现
33

4-
当函数组件进入render阶段时,会被renderWithHooks函数处理。函数组件作为一个函数,它的渲染其实就是函数调用,它内部调用了React提供的hooks函数。初始挂载和更新时,所用的hooks
5-
函数是不同的,但它们都由`ReactCurrentDispatcher`来提供,hooks函数存储在其current属性中。
4+
当函数组件进入render阶段时,会被renderWithHooks函数处理。函数组件作为一个函数,它的渲染其实就是函数调用,而函数组件又会调用React提供的hooks函数。初始挂载和更新时,所用的hooks
5+
函数是不同的,比如初次挂载时调用的useEffect,和后续更新时调用的useEffect,虽然都是同一个hook,但是因为在两个不同的渲染过程中调用它们,所以本质上他们两个是不一样的。这种不一样来源于
6+
函数组件要维护一个hooks的链表,初次挂载时要创建链表,后续更新的时候要更新链表。
67

7-
所以在调用函数组件之前,当务之急是根据当前所处的阶段来决定ReactCurrentDispatcher的current,这样才可以在正确的阶段调用到正确的hooks函数。
8+
分属于两个过程的hook函数会在各自的过程中被赋值到`ReactCurrentDispatcher`的current属性上。所以在调用函数组件之前,
9+
当务之急是根据当前所处的阶段来决定ReactCurrentDispatcher的current,这样才可以在正确的阶段调用到正确的hook函数。
810

911
```javascript
1012
export function renderWithHooks<Props, SecondAwrg>(
@@ -241,6 +243,5 @@ function updateWorkInProgressHook(): Hook {
241243
}
242244
return workInProgressHook;
243245
}
244-
245246
```
246247

docs/commit阶段/mutation/节点删除.md

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[点击](https://github.com/neroneroffy/react-source-code-debug)进入React源码调试仓库。
22

3-
Fiber架构使得React维护两类树结构,一类是Fiber树,另一类是DOM树。当删除DOM节点时,Fiber树也要同步变化。但请注意整个mutation阶段删除操作执行的时机
4-
**在完成DOM节点的其他变化前,要先删除fiber节点,避免其他操作被干扰。** 这是因为进行其他DOM操作时需要循环fiber树,此时如果有需要删除的fiber节点却还没删除的话,就会发生混乱。
3+
Fiber架构使得React需要维护两类树结构,一类是Fiber树,另一类是DOM树。当删除DOM节点时,Fiber树也要同步变化。但请注意删除操作执行的时机
4+
**在完成DOM节点的其他变化(增、改)前,要先删除fiber节点,避免其他操作被干扰。** 这是因为进行其他DOM操作时需要循环fiber树,此时如果有需要删除的fiber节点却还没删除的话,就会发生混乱。
55

66
```javascript
77
function commitMutationEffects(
@@ -40,7 +40,7 @@ function commitMutationEffects(
4040
```
4141
*fiber.deletions是render阶段的diff过程检测到fiber的子节点如果有需要被删除的,就会被加到这里来。*
4242

43-
最终,`commitDeletion`函数是删除节点的入口,它通过调用`unmountHostComponents`实现删除。搞懂删除操作之前,先看看场景。
43+
`commitDeletion`函数是删除节点的入口,它通过调用`unmountHostComponents`实现删除。搞懂删除操作之前,先看看场景。
4444

4545
有如下的Fiber树,Node(Node是一个代号,并不指的某个具体节点)节点即将被删除。
4646
```
@@ -62,21 +62,18 @@ function commitMutationEffects(
6262
a
6363
```
6464

65-
通过这种场景可以推测出当删除该节点时,它下面子树中的所有节点都要被删除。现在直接以这个场景为例,走一下删除过程。这个过程实际上也就是
66-
`unmountHostComponents`函数的运行机制。
65+
通过这种场景可以推测出当删除该节点时,它下面子树中的所有节点都要被删除。现在直接以这个场景为例,走一下删除过程。这个过程实际上也就是`unmountHostComponents`函数的运行机制。
6766

6867
# 删除过程
6968
删除Node节点需要父DOM节点的参与:
7069
```javascript
7170
parentInstance.removeChild(child)
7271
```
73-
所以首先要定位到父级节点。过程是在Fiber树中,以Node的父节点为起点往上找,找到的第一个原生DOM节点即为父节点。在例子中,父节点就是div。此后以Node为起点,
74-
遍历子树,子树也是fiber树,因此遍历是深度优先遍历,将每个子节点都删除。
72+
所以首先要定位到父级节点。过程是在Fiber树中,以Node的父节点为起点往上找,找到的第一个原生DOM节点即为父节点。在例子中,父节点就是div。此后以Node为起点,遍历子树,子树也是fiber树,因此遍历是深度优先遍历,将每个子节点都删除。
7573

76-
**需要特别注意的一点是,对循环节点进行删除,每个节点都会被删除操作去处理,这里的每个节点是fiber节点而不是DOM节点。DOM节点的删除时机是从Node开始遍历进行删除的时候,
77-
遇到了第一个原生DOM节点(HostComponent或HostText)这个时刻,在删除了它子树的所有fiber节点后,才会被删除。**
74+
**需要特别注意的一点是,对循环节点进行删除,每个节点都会被删除操作去处理,这里的每个节点是fiber节点而不是DOM节点。DOM节点的删除时机是从Node开始遍历进行删除的时候,遇到了第一个原生DOM节点(HostComponent或HostText)这个时刻,在删除了它子树的所有fiber节点后,才会被删除。**
7875

79-
以上的过程是完整过程的简述,对于详细过程要明确几个关键函数的职责和调用关系才行。删除fiber节点的是`unmountHostComponents`函数,被删除的节点称为目标节点,它的职责为:
76+
以上是完整过程的简述,对于详细过程要明确几个关键函数的职责和调用关系才行。删除fiber节点的是`unmountHostComponents`函数,被删除的节点称为目标节点,它的职责为:
8077
1. 找到目标节点的DOM层面的父节点
8178
2. 判断目标节点如果是原生DOM类型的节点,那么执行3、4,否则先卸载自己之后再往下找到原生DOM类型的节点之后再执行3、4
8279
3. 遍历子树执行fiber节点的卸载
@@ -91,7 +88,7 @@ parentInstance.removeChild(child)
9188

9289
下面来看一下不同类型的组件它们的具体删除过程是怎样的。
9390

94-
# 类别细分
91+
# 区分被删除组件的类别
9592
Node节点的类型有多种可能性,我们以最典型的三种类型(`HostComponent、ClassComponent、HostPortal`)为例分别说明一下删除过程。
9693

9794
首先执行`unmountHostComponents`,会向上找到DOM层面的父节点,然后根据下面的三种组件类型分别处理,我们挨个来看。
@@ -107,8 +104,7 @@ Node 是HostComponent,调用`commitNestedUnmounts`,以Node为起点,遍历
107104
```
108105
对节点逐个执行`commitUnmount`进行卸载,这个遍历过程其实对于三种类型的节点,都是类似的,为了节省篇幅,这里只表述一次。
109106

110-
Node的fiber被卸载,然后向下,p的fiber被卸载,p没有child,找到它的sibling`<Child>``<Child>`的fiber被卸载,向下找到a,a的fiber被卸载。此时到了整个子树的叶子节点,开始向上return。
111-
由a 到 `<Child>`,再回到Node,遍历卸载的过程结束。
107+
Node的fiber被卸载,然后向下,p的fiber被卸载,p没有child,找到它的sibling`<Child>``<Child>`的fiber被卸载,向下找到a,a的fiber被卸载。此时到了整个子树的叶子节点,开始向上return。由a 到 `<Child>`,再回到Node,遍历卸载的过程结束。
112108

113109
在子树的所有fiber节点都被卸载之后,才可以安全地将Node的DOM节点从父节点中移除。
114110

@@ -125,8 +121,7 @@ Node的fiber被卸载,然后向下,p的fiber被卸载,p没有child,找
125121
a
126122
```
127123

128-
Node是ClassComponent,它没有对应的DOM节点,要先调用`commitUnmount`卸载它自己,之后会先往下找,找到第一个原生DOM类型的节点span,以它为起点遍历子树,
129-
确保每一个fiber节点都被卸载,之后再将span从父节点中删除。
124+
Node是ClassComponent,它没有对应的DOM节点,要先调用`commitUnmount`卸载它自己,之后会先往下找,找到第一个原生DOM类型的节点span,以它为起点遍历子树,确保每一个fiber节点都被卸载,之后再将span从父节点中删除。
130125

131126
## HostPortal
132127
```
@@ -145,12 +140,10 @@ Node是ClassComponent,它没有对应的DOM节点,要先调用`commitUnmount
145140
|
146141
a
147142
```
148-
Node是HostPortal,它没有对应的DOM节点,因此删除过程和ClassComponent基本一致,不同的是删除它下面第一个子fiber的DOM节点时不是从这个被删除的HostPortal类
149-
型节点的DOM层面的父节点中删除,而是从HostPortal的containerInfo中移除,图示上为div2,因为HostPortal会将子节点渲染到父组件以外的DOM节点。
143+
Node是HostPortal,它没有对应的DOM节点,因此删除过程和ClassComponent基本一致,不同的是删除它下面第一个子fiber的DOM节点时不是从这个被删除的HostPortal类型节点的DOM层面的父节点中删除,而是从HostPortal的containerInfo中移除,图示上为div2,因为HostPortal会将子节点渲染到父组件以外的DOM节点。
150144

151145

152-
以上是三种类型节点的删除过程,这里值得注意的是,`unmountHostComponents`函数执行到遍历子树卸载每个节点的时候,一旦遇到HostPortal类型的子节点,会再次调用`unmountHostComponents`
153-
以它为目标节点再进行它以及它子树的卸载删除操作,相当于一个递归过程。
146+
以上是三种类型节点的删除过程,这里值得注意的是,`unmountHostComponents`函数执行到遍历子树卸载每个节点的时候,一旦遇到HostPortal类型的子节点,会再次调用`unmountHostComponents`,以它为目标节点再进行它以及它子树的卸载删除操作,相当于一个递归过程。
154147

155148
# commitUnmount
156149
HostComponent 和 ClassComponent的删除都调用了commitUnmount,除此之外还有FunctionComponent也会调用它。它的作用对三种组件是不同的:
@@ -223,10 +216,9 @@ function commitUnmount(
223216
* 删除的目标是谁
224217
* 从哪里删除
225218

226-
mutation在基于Fiber节点对DOM做其他操作之前,需要先删除节点,保证留给后续操作的fiber节点都是有效的。删除的目标是Fiber节点及其子树和Fiber节点对应的DOM节点,整个轨迹循着fiber树,对目标节点和所有子节点都进行卸载,对目标节点对应的(或之下的第一个)DOM节点进行删除。
227-
对于原生DOM类型的节点,直接从其父DOM节点删除,对于HostPortal节点,它会把子节点渲染到外部的DOM节点,所以会从这个DOM节点中删除。
219+
mutation在基于Fiber节点对DOM做其他操作之前,需要先删除节点,保证留给后续操作的fiber节点都是有效的。删除的目标是Fiber节点及其子树和Fiber节点对应的DOM节点,整个轨迹循着fiber树,对目标节点和所有子节点都进行卸载,对目标节点对应的(或之下的第一个)DOM节点进行删除。对于原生DOM类型的节点,直接从其父DOM节点删除,对于HostPortal节点,它会把子节点渲染到外部的DOM节点,所以会从这个DOM节点中删除。
228220

229-
明确以上三个点再结合上述梳理的过程,就可以逐渐理清删除的脉络
221+
明确以上三个点再结合上述梳理的过程,就可以逐渐理清删除操作的脉络
230222

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

docs/commit阶段/mutation/节点插入.md

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[点击](https://github.com/neroneroffy/react-source-code-debug)进入React源码调试仓库。
22

3-
插入DOM节点操作的是fiber节点上的stateNode,对于原生DOM类型的fiber节点来说stateNode存储着DOM节点。插入节点的操作就是循着fiber树把DOM节点插入到真实的DOM树中
3+
插入DOM节点操作的是fiber节点上的stateNode,对于原生DOM类型的fiber节点来说stateNode存储着DOM节点。commit阶段插入节点的操作就是循着fiber树把DOM节点插入到真实的DOM树中
44

55
`commitPlacement`是插入节点的入口,
66
```javascript
@@ -14,6 +14,7 @@ function commitMutationEffectsImpl(
1414

1515
switch (primaryEffectTag) {
1616
case Placement: {
17+
// 插入操作
1718
commitPlacement(fiber);
1819
fiber.effectTag &= ~Placement;
1920
break;
@@ -26,10 +27,10 @@ function commitMutationEffectsImpl(
2627

2728
```
2829

29-
它的功能明确,将需要被执行stateNode插入操作的fiber节点称为目标节点。
30+
我们将需要被执行插入操作的fiber节点称为目标节点,`commitPlacement`函数的功能如下:
3031
1. 找到目标节点DOM层面的父节点(parent)
31-
2. 根据目标节点类型,改变parent
32-
3. 如果目标节点对应的DOM节点目前只有文字内容,类似`<div>hello</div>`并且持有ContentReset的effectTag,那么插入节点之前先设置一下文字内容
32+
2. 根据目标节点类型,找到对应的parent
33+
3. 如果目标节点对应的DOM节点目前只有文字内容,类似`<div>hello</div>`并且持有ContentReset(内容重置)的effectTag,那么插入节点之前先设置一下文字内容
3334
4. 找到基准节点
3435
5. 执行插入
3536

@@ -85,12 +86,12 @@ function commitPlacement(finishedWork: Fiber): void {
8586

8687
```
8788

88-
这里要明确的一点是DOM节点插入到哪,也就是第二步**根据目标节点类型,改变parent**
89+
这里要明确的一点是DOM节点插入到哪,也就是要**根据目标节点类型,找到对应的parent**
8990

9091
如果是`HostRoot`或者`HostPortal`类型的节点,第一它们都没有对应的DOM节点,第二实际渲染时它们会将DOM子节点渲染到对应的外部节点上(containerInfo)。
9192
所以当fiber节点类型为这两个时,就将节点插入到这个外部节点上,即:
9293
```javascript
93-
// 改变parent为fiber上的containerInfo
94+
// 将parent赋值为fiber上的containerInfo
9495
parent = parentStateNode.containerInfo
9596

9697
...
@@ -111,12 +112,11 @@ insertOrAppendPlacementNode(finishedWork, before, parent);
111112
React插入节点的时候,分两种情况,新插入的DOM节点在它插入的位置是否已经有兄弟节点,没有,执行`parentInstance.appendChild(child)`
112113
有,调用`parentInstance.insertBefore(child, beforeChild)`。这个`beforeChild`就是上文提到的`before`,它是新插入的DOM节点的基准节点,
113114
有了它才可以在父级DOM节点已经存在子节点的情况下,将新节点插入到正确的位置。试想如果已经有子节点还用`parentInstance.appendChild(child)`去插入,
114-
那是不是就把新节点插入到最末尾了?这显然是不对的。所以找到before的位置十分重要,它通过`getHostSibling`函数来定位到。
115+
那是不是就把新节点插入到最末尾了?这显然是不对的。所以找到before的位置十分重要,`before`(基准节点)通过`getHostSibling`函数来定位到。
115116

116117
我们用一个例子来说明一下`getHostSibling`的原理:
117118

118-
p为新生成的DOM节点。a为已存在且无变化的DOM节点。它们在fiber树中的位置如下,p需要插入到DOM树中,我们可以根据这棵fiber树
119-
来推断出最终的DOM树形态。
119+
p为新生成的DOM节点。a为已存在且无变化的DOM节点。它们在fiber树中的位置如下,p需要插入到DOM树中,我们可以根据这棵fiber树来推断出最终的DOM树形态。
120120
```
121121
Fiber树 DOM树
122122
@@ -163,12 +163,11 @@ h1是原生DOM节点并且h1已存在于DOM树,那么h1作为结果返回,p
163163
2. 过滤不出来就查找同级节点的子节点,过滤出原生DOM组件。
164164
3. 重复查找兄弟节点再查找子节点的过程,直到再也找不到兄弟节点。
165165
4. 向上查找到父节点,兄对父节点也重复前三步。
166-
5. 直到过滤出原生DOM节点,判断它如果不是需要插入的节点,那么它作为结果返回,新节点需要插入到它的前面。
166+
5. 直到过滤出原生DOM节点,如果该DOM节点不是需要插入的节点,那么它作为结果返回,也就是定位到了`before`(基准节点),新节点需要插入到它的前面。
167167

168168
这其中有如下规律:
169169

170-
**`需要插入的节点`如果有同级fiber节点且是原生DOM节点,那么它一定是插入到这个节点之前的。如 果同级节点不是
171-
原生DOM节点,那么它和同级节点的子节点`在DOM层面是兄弟节点`的关系。**
170+
**`需要插入的节点`如果有同级fiber节点且是原生DOM节点,那么它一定是插入到这个节点之前的。如果同级节点不是原生DOM节点,那么它和同级节点的子节点`在DOM层面是兄弟节点`的关系。**
172171

173172
**`需要插入的节点`如果没有同级节点,那么它和父节点的兄弟节点的子节点`在DOM层面是兄弟节点`的关系。**
174173

@@ -265,11 +264,9 @@ insertOrAppendPlacementNode(finishedWork, before, parent);
265264
* before: `<Child/>`的sibling节点,该场景下为null
266265
* parent: `<Child/>`的parent节点,也就是`div#App`
267266

268-
进入函数,它的任务是将DOM节点插入到parent之下或before之前,如果finishedWork是原生DOM节点,那么依据有无before来决定节点的插入方式,
269-
无论哪种方式都会将DOM实实在在地插入到正确的位置上。
267+
进入函数,它的任务是将DOM节点插入到parent之下或before之前,如果finishedWork是原生DOM节点,那么依据有无before来决定节点的插入方式,无论哪种方式都会将DOM实实在在地插入到正确的位置上。
270268

271-
如果不是原生DOM节点,就是`<Child/>`这种,不能对它进行插入操作,那么怎么办呢?向下,从它的child切入,再次调用`insertOrAppendPlacementNode`
272-
也就是递归地调用自己,将child一个不剩地全插入到parent中。在例子中,会把p插入到parent。
269+
如果不是原生DOM节点,就是`<Child/>`这种,不能对它进行插入操作,那么怎么办呢?向下,从它的child切入,再次调用`insertOrAppendPlacementNode`,也就是递归地调用自己,将child一个不剩地全插入到parent中。在例子中,会把p插入到parent。
273270

274271
此时`<Child/>`的子节点已经全部完成插入,这时会再找到p的兄弟节点span,对它进行插入,然后发现span还有兄弟节点h1,将h1也插入。
275272

@@ -342,16 +339,15 @@ Placement --> <Child/>----> span / | \
342339
```
343340

344341
# 总结
345-
结合实际插入节点产生的问题不难总结出commit阶段节点插入过程的特点
342+
结合实际插入节点产生的问题不难总结出commit阶段插入节点过程的特点
346343
1. 定位DOM节点插入的正确位置
347344
2. 避免DOM节点的多余插入
348345

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

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

354-
如下,可以看到最终的effestList中,最下层的节点排在最前面:
350+
如下,可以看到最终的effectList中,最下层的节点排在最前面:
355351

356352
![](http://neroht.com/commit_fiber_effect_list.jpg)
357353

0 commit comments

Comments
 (0)