React源码-completeWork
# 0.前言
争取以提问的方式将 React
知识结构。
尽量精简代码,将无关逻辑省去。
# 1.结构及作用
本章节主要对应于 React
源码中 reconcile
阶段的 归
阶段。
其核心逻辑主要存在于两个函数中,completeWork
以及 completeUnitOfWOrk
中,两者的区别如下:
- 代码执行次序:
completeUnitOfWork
completeWork
- 文件位置不同,
completeUnitOfWork
位于ReactFiberWorkLoop
中,而completeWork
位于ReactFiberComplteWork
- 功能不同:
completeWork
为核心处理逻辑,作用见后续详述。completeUnitOfWork
属于上层函数,主要是用于对complteWork
回调的结果进行编排处理,如EffectTag
链表的产生。
# 2.workInprogress状态
在 React
中无论代码逻辑怎么发生变化,实际上都是对 workInProgress
结构的修改,所以只需要把握住每个阶段,改变的是 workInProgress
上的什么属性,基本上能用一句话能说清 React
的更新逻辑。
在 completeWork
的阶段:
mount
阶段:workInProgress.stateNode
:底层由document.createElement
产生DOM
结构,并保存在此节点属性上。意义:
fiber
双缓存的核心意义,一颗树用于显示页面,一颗树用于将动态计算的结果缓存,而stateNode
的作用就是当commit
阶段切换current
指针时,可以立马将DOM
结构渲染到页面上,而不存在空白中间态过程。
update
阶段:workInProgress.updateQueue
保存的是更新数组。结合案例具体结构如下:workInProgress.updateQueue = ["key1", "value1" , "key2", "value2"];
1workInProgress.firstEffect|nextEffect|lastEffect
:保存EffectList
链表。
# 3. 提问 completeWork
流程
# 问题:对下面 jsx
结构,分析 completeWork
递归逻辑?
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
</header>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
通过打断点,可知 complteWork
的次序如下:
img
:进入HostComponent
(5) 更新逻辑Edit
:文本节点,进入HostText
(6)code
:进入HostComponent
(5) 更新逻辑" and save to reload."
:文本节点,进入HostText
(6)p
:注意此时p
对应的workInProgress.child
存在,appendChild
只有在此刻才会生效。header
:进入HostComponent
(5) 更新逻辑div
:进入HostComponent
(5) 更新逻辑
为了帮助更好的理解,以问答的方式解答上述过程:
为什么第一个
completeWork
进入的<img />
?在
React
中children
的链表结构,当存在多个子节点的情况下,只会为第一个children
生成fiber
节点,其余节点均以sibling
的方式与第一个children
链接。又,
header
存在两个子节点img
和p
,因此第一个completeWork
会进入<img />
为什么
Edit
要先于父节点p
?因为
completeWork
是深度优先遍历,当子节点全部遍历结束后,才会遍历父节点。为什么
<code>
存在文本子节点不遍历?
React
中对单文本子节点有做特殊优化,beginWork
阶段压根就没有这个Fiber
节点。
# 问题:请简述 completeWork
的核心逻辑?
completWork
的核心逻辑如下:
根据 tag
类型,对不同的节点类型进行逻辑处理。
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
switch (workInProgress.tag){
case 组件1:
/** 处理逻辑 */
return null;
case 组件2;
/** 处理逻辑 */
return null;
case 组件3;
return null
....
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
因此在案例中:
对于
div
或者img
这类型结构,会进入case
为HostComponent
的处理逻辑。对于
Eidt
或者" and save to reload."
这类结构,会进入case
为HostText
的处理逻辑。......其余组件类似,就不再赘述。
需要注意的是在 completeWork
中有一大类组件是直接跳过处理的,因为在后续分析中可以发现 completeWork
中主要针对的是具有 DOM
结构的 fiber
节点,对于不会生成具体的 DOM
结构的节点,在 switch...case
阶段会直接跳过,代码如下:
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
2
3
4
5
6
7
8
9
10
11
12
IndeterminateComponent
:IndeterminateComponent 是 React 内部使用的一个中间类型,用于表示还未确定具体类型的组件。它不会直接被应用开发者创建,而是在 React 的协调过程中根据组件的类型进行转化。
LazyComponent
:LazyComponent 是懒加载组件,通过 React.lazy() 函数来创建。React.lazy() 接受一个动态 import() 函数作为参数,用于异步加载组件的代码。在需要渲染该组件时,React 会自动按需加载该组件。
SimpleMemoComponent
:SimpleMemoComponent 是一个使用了浅比较的记忆组件(Memoized Component)。通过 React.memo() 函数来创建,它会将组件包裹起来并缓存组件的渲染结果。只有在组件接收的 props 发生变化时,才会触发重新渲染。
FunctionComponent
:FunctionComponent 是一种函数式组件,在 React 中最常见的组件类型。它由一个纯函数来定义,接收 props 作为输入并返回渲染的内容。可以使用函数组件来编写简单、可复用的 UI 组件。
ForwardRef
:ForwardRef 是一种用于转发 ref 的组件包装方式。通过 React.forwardRef() 函数来创建,可以将 ref 特性传递给子组件。这在某些情况下对于访问子组件上的 DOM 节点或实例非常有用。
Fragment
:Fragment 是一种不会输出 DOM 元素的容器组件。由 React.Fragment 或者简写形式<>...</>
来创建。可以用它来返回多个子元素而无需包裹到额外的 DOM 节点中。
Mode
:Mode 是一个用于设置渲染模式的组件,用于控制 React 渲染的行为。通过<React.StrictMode>
组件在应用根部使用,可以进行严格模式的检查和警告提示。
Profiler
:Profiler 是一个性能分析工具组件,用于度量和诊断 React 组件的渲染性能。通过<Profiler>
组件来创建,并提供一个回调函数用于收集组件渲染过程的性能信息。
ContextConsumer
:ContextConsumer 是一个使用 React Context 的消费者组件。通过在组件中使用useContext()
钩子或者在类组件中使用<MyContext.Consumer>
组件来创建,用于接收和使用 Context 提供的值。
MemoComponent
:MemoComponent 是使用记忆化技术优化后的组件,通过 React.memo() 函数来创建。它会对组件的输入 props 进行浅比较,并只在 props 发生变化时才重新渲染。
# 问题:对不同节点的逻辑处理包含哪两块逻辑?
包含两部分逻辑:update
处理逻辑和 mount
处理逻辑。
举例: HostComponent
类型节点的处理逻辑,伪代码如下:
case HostComponent: {
const isMount = current !== null && workInProgress.stateNode != null;
if(!isMount){
// 更新 HostComponent 逻辑
updateComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
}else{
// 主要逻辑如下:【以 HostComponent 为主】
// 1. `createInstance`:基于 `Fiber` 对象创建 `DOM` 结构。
// 2. `appendAllChildren`:如何处理 `workInProgress.child` 节点
// 3. `finalizeInitialChildren` :属性绑定。
}
// do something else......
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
首先判断是 Update
阶段还是 Mount
阶段
如果是
Update
阶段,则调用UpdateComponent
函数,如对于HostComponent
会调用UpdateHostComponent
;对于HostText
会调用UpdateHostText
函数。如果是
Mount
阶段,实际上会生成真实的DOM
结构,并挂载在workInProgress.stateNode
上。
对于多节点结构,需要处理下链表连接,代码逻辑:
case HostComponent: {
const isMount = current !== null && workInProgress.stateNode != null;
if(!isMount){
// workInProgress.upadateQueue = ["title",2,"children",2]
}else{
// workInProgress.stateNode = <div>...</div> (dom 结构)
}
node = (node: Fiber);
if (node === workInProgress) {
return;
}
/** 主要处理的存在节点存在子节点场景时,需要额外处理链表 */
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
主要针对如下 JSX
结构:
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
2
3
若此时
node
为Edit
节点,node.sibling
为<code />
节点。// node = "Edit" // node.sibling = <code /> // node.return = <p> // 结果:<code />.return = <p>; node.sibling.return = node.return; // node = code 继续找兄弟 node = node.sibling;
1
2
3
4
5
6
7结果为:所有子节点的
return
均执行父节点<p>
# 问题:描述下 completeWork
的 mount
阶段工作?
为 HostComponent
生成 DOM
结构,并缓存在 workInProgress.stateNode
结构中。
依次调用如下:
createInstance
:基于Fiber
对象创建DOM
结构。appendAllChildren
:如何处理workInProgress.child
节点finalizeInitialChildren
:属性绑定。
createInstance
本质调用 window.createElement
此函数还不好找,在
ReactDOMHostConfig.js
中,此函数不同宿主环境中还不同,以下示例web
环境:
export function createInstance(
type: string, /* workInProgress.type */
props: Props, /* workInProgress.pendingProps */
rootContainerInstance: Container, /* 根节点 */
hostContext: HostContext, /* 请无视这个属性 */
internalInstanceHandle: Object, /* workInProgress */
): Instance {
let parentNamespace: string;
// DOM 原生的 `createElement`
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
return domElement;
}
var randomKey = Math.random().toString(36).slice(2);
// 辅助函数:precacheFiberNode: 往 DOM 帮上 fiber 结构
function precacheFiberNode(hostInst, domElement) {
domElement[__reactFiber$' + randomKey] = hostInst;
}
// 辅助函数:updateFiberProps: 往 DOM 帮上 props 属性
function updateFiberProps(domElement, props) {
domElement['__reactProps$' + randomKey] = props;
}
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
appendAllChildren
功能:将子节点的 dom
全部挂载在父节点 dom
下
举例:
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
</header>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
complteWork
的流程:
img
:进入HostComponent
(5) 更新逻辑Edit
:文本节点,进入HostText
(6)code
:进入HostComponent
(5) 更新逻辑" and save to reload."
:文本节点,进入HostText
(6)p
:注意此时p
对应的workInProgress.child
存在,appendChild
只有在此刻才会生效。
注:workInProgress.type === "p"
才会进入逻辑:
appendAllChildren = function (
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
let node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent) {
/** img 标签或 code 标签会进入此逻辑*/
let instance = node.stateNode;
if (needsVisibilityToggle && isHidden) {
// This child is inside a timed out tree. Hide it.
const props = node.memoizedProps;
const type = node.type;
instance = cloneHiddenInstance(instance, type, props, node);
}
appendInitialChild(parent, instance);
} else if (node.tag === HostText) {
/** Edit 标签或 and save to reload. 标签会进入此逻辑*/
let instance = node.stateNode;
if (needsVisibilityToggle && isHidden) {
// This child is inside a timed out tree. Hide it.
const text = node.memoizedProps;
instance = cloneHiddenTextInstance(instance, text, node);
}
appendInitialChild(parent, instance);
}
/* 省略其余场景代码 */
node = (node: Fiber);
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
// 将所有 children 的return 值都指向父节点。
node.sibling.return = node.return;
node = node.sibling;
}
};
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
核心逻辑:appendInitialChild(parent, instance)
步骤:
p
标签进入completeWork
逻辑,此时其子children
均已完成completeWork
流程。appendAllChildren
会循环将子children
上的dom
结构,以及通过appendChild
挂载到p
上。- 所有子节点的
return
值均指向p
- 最终结果:
p
标签拥有完整的dom
结构。
finalizeInitialChildren
此函数还不好找,在
ReactDOMHostConfig.js
中,此函数不同宿主环境中还不同,以下示例web
环境:
export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): boolean {
setInitialProperties(domElement, type, props, rootContainerInstance);
return shouldAutoFocusHostComponent(type, props);
}
2
3
4
5
6
7
8
9
10
此函数主要用途,给 DOM
结构挂载属性。
比如此结构:
<img src={logo} className="App-logo" alt="logo" />
判断
<img />
是否为CustomComponent
React
中需要对DOM
的原生事件进行监听,核心函数listenToNonDelegatedEvent
switch (tag) { case 'dialog': listenToNonDelegatedEvent('cancel', domElement); listenToNonDelegatedEvent('close', domElement); props = rawProps; break; case 'iframe': case 'object': case 'embed': // We listen to this event in case to ensure emulated bubble // listeners still fire for the load event. listenToNonDelegatedEvent('load', domElement); props = rawProps; break; case 'video': case 'audio': // We listen to these events in case to ensure emulated bubble // listeners still fire for all the media events. for (var i = 0; i < mediaEventTypes.length; i++) { listenToNonDelegatedEvent(mediaEventTypes[i], domElement); } props = rawProps; break; case 'source': // We listen to this event in case to ensure emulated bubble // listeners still fire for the error event. listenToNonDelegatedEvent('error', domElement); props = rawProps; break; case 'img': case 'image': case 'link': // We listen to these events in case to ensure emulated bubble // listeners still fire for error and load events. listenToNonDelegatedEvent('error', domElement); listenToNonDelegatedEvent('load', domElement); props = rawProps; break; case 'details': // We listen to this event in case to ensure emulated bubble // listeners still fire for the toggle event. listenToNonDelegatedEvent('toggle', domElement); props = rawProps; break; case 'input': initWrapperState(domElement, rawProps); props = getHostProps(domElement, rawProps); // We listen to this event in case to ensure emulated bubble // listeners still fire for the invalid event. listenToNonDelegatedEvent('invalid', domElement); break; case 'option': validateProps(domElement, rawProps); props = rawProps; break; case 'select': initWrapperState$1(domElement, rawProps); props = getHostProps$1(domElement, rawProps); // We listen to this event in case to ensure emulated bubble // listeners still fire for the invalid event. listenToNonDelegatedEvent('invalid', domElement); break; case 'textarea': initWrapperState$2(domElement, rawProps); props = getHostProps$2(domElement, rawProps); // We listen to this event in case to ensure emulated bubble // listeners still fire for the invalid event. listenToNonDelegatedEvent('invalid', domElement); break; default: props = rawProps; }
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
79
80
81
82
83调用
setInitialDOMProperties
函数,将jsx
中获取的props
绑定给node
会循环遍历
nextProps
值,最终会调用node.setAttribute(attributeName, attributeValue)
函数。其中,对于单次循环,执行前:
node
为<img />
attributeName: src
attributeValue: "/static/media/logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg"
执行后
node
为<img src="/static/media/logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg">
注意的是,在执行
node.setAttribute
函数时,DOM
结构也是存在很多的保留字段,如下:const DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML'; const SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning'; const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning'; const AUTOFOCUS = 'autoFocus'; const CHILDREN = 'children'; const STYLE = 'style'; const HTML = '__html';
1
2
3
4
5
6
7具体的逻辑在
setInitialDOMProperties
函数中,伪代码如下:function setInitialDOMProperties( tag: string, domElement: Element, rootContainerElement: Element | Document, nextProps: Object, isCustomComponentTag: boolean, ): void { for (const propKey in nextProps) { if(propKey === "style"){ // 执行 setValueForStyles() 函数 }else if(propKey === "children"){ // 执行 setTextConent() 函数 }else if(propKey === "dangerouslySetInnerHTML"){ // 执行 setInnerHTML() 函数 }else if(propKey === "suppressHydrationWarning" || 'suppressContentEditableWarning'){ // Noop }else if(propKey === "autoFocus"){ // React 会对此参数在 commit 阶段进行特殊处理, }else{ // 调用 setValueForProperty() } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23我们刚才
node.setAttribute()
是在setValueForProperty
中处理的。
# 问题:描述下 completeWrk
的 update
阶段工作?
为了能够简化更新流程,只保留更新阶段所需最简洁的 JSX
结构:
import { useState } from "react";
function App() {
const [num, setNum] = useState(1);
return (
<div className="App">
<header className="App-header">
<p onClick={() => setNum(x => x + 1)}>
<code title={num}>{num}</code>
</p>
</header>
</div>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
在上述代码中,存在副作用的节点有两个 <p />
和 <code />
。
其中
p
标签也会标记为副作用的原因,每次更新时箭头函数的引用地址也发生改变。
操作:在状态更新前, num
值为 1
,点击 p
标签后,num
被修改为 2
。
最终结果:
workInProgress.updateQueue = ["title", 2, "children", "2"];
伪代码如下:
// 获取旧属性 {"title": 1 , "children": 1}
const oldProps = current.memoizedProps
// 获取新属性 {"title": 2 , "children": 2}
const newProps = workInProgress.pendingProps;
// 获取 DOM 结构
var instance = workInProgress.stateNode;
// 需要比较新旧节点 【diffProperties】
// 注:此部分工作牵涉 DOM 结构的比对,实际是由 `react-dom` 实现
workInProgress.updateQueue = diffProperties(instance,oldProps,nextProps)
// 打 EffcectTag
if(updatePayload){
workInProgress.flags |= Update;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
看下 react-dom
中是如何实现 props diff
比对的?
相比上直接对比两个对象结构的比对, DOM
上的 props
的 diff
操作需要额外的考虑更多:
不同的
dom
结构,不同的tag
具有不同的attributeName
。如:
<img />
上src
属性,对于特殊
props
的处理,如style
的变化逻辑,会专门标注出来。const DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML'; const SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning'; const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning'; const AUTOFOCUS = 'autoFocus'; const CHILDREN = 'children'; const STYLE = 'style'; const HTML = '__html';
1
2
3
4
5
6
7判断变化属性的性质
删除:
{"title": 1 , "children": 1}
{"childre": 1}
新增:
{"children":1}
{"title": 1 , "children": 1}
更新:
{"title": 1 , "children": 1}
{"title": 2 , "children": "2"}
伪代码:
function diffProperties(instance,oldProps, nextProps){
for(propKey in oldProps){
if(
nextProps.hasOwnProperty(propKey) // nextProps 存在
|| !oldProps.hasOwnProperty(propKey) // oldProps 不存在
|| oldProps[propKey] == null // oldProps 存在但为 null 值
){
continue;
}
// 将删除逻辑 push 到 updateQueue 数组中 {"title", null}
(updatePayload = updatePayload || []).push(propKey, null);
}
for (propKey in nextProps) {
// 从 oldProps 和 nextProps 取出属性值
const nextProp = nextProps[propKey];
const lastProp = lastProps != null ? lastProps[propKey] : undefined;
if (
!nextProps.hasOwnProperty(propKey) // nextProps 不存在
|| nextProp === lastProp // nextProp 和 oldProps 相同
|| (nextProp == null && lastProp == null) // 都为null
) {
continue;
}
// 对于 DOM 元素上各种保护字段的处理
// 将新增或更新逻辑 push 到 updateQueue 数组中
(updatePayload = updatePayload || []).push(propKey, nextProp);
}
// 将 style 的变化 push 到 updateQueue 数组中
if (styleUpdates) {
(updatePayload = updatePayload || []).push("style", styleUpdates);
}
}
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
# 4. 提问 completUnitOfWork
流程
# 问题:在 completeUnitOfWork
中是如何实现 completeWork
的深度优先遍历?
深度遍历的逻辑,是由 ReactFiberWorkLoop
文件中编写的。
在协调器的入口函数中:
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// 调度:beginWork → completeWork
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
const next = beginWork(current, unitOfWork);
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如果忽略掉 performUnitOfWork
这个函数,workLoopSync
会循环检查 workInProgress
是否有值,即隐藏一个条件:workInProgress
会跳出循环。
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do{
const current = completedWork.alternate;
const next = completeWork(current, completedWork);
// 注解1:跳出 completeWork 循环
if (next !== null) {
workInProgress = next;
return;
}
const siblingFiber = completedWork.sibling;
// 注解2:当存在兄弟节点时,也会跳出循环。
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
// 省略生成 EffectList 链表,见下
// 将链表移动到父节点
workInProgress = completedWork.return;
} while (completedWork !== null);
}
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
注解1
:completeWork
处理通过看源码中
completeWork
的回调值,正常返回null
,只有少数几个case
会返回Fiber
结构,错误处理或SuspendCompnent
处理。case SuspenseComponent: .... if ((workInProgress.flags & DidCapture) !== NoFlags) { // Something suspended. Re-render with the fallback children. workInProgress.lanes = renderLanes; return workInProgress; } case SuspenseListComponent: .... return workInProgress.child;
1
2
3
4
5
6
7
8
9
10
11
注解2
:主要考虑对于以下jsx
结构<div> <p> Edit <code>src/App.js</code> and save to reload. </p> </div>
1
2
3
4
5当进入
Edit
这个文本节点时,存在其兄弟节点(<code />
) ,此时应该结束Edit
的completeWork
遍历,并进入<code />
节点的beginWork
流程,即生成<code />
这个DOM
元素,直到遍历完所有p
的子节点,最后执行p
节点的completeWork
。
# 问题:如何生成 EffectList
链表?
此部分逻辑,也是在 completeUnitOfWork
中实现的。
链表逻辑如下:
const returnFiber = completedWork.return;
if (
returnFiber !== null &&
(returnFiber.flags & Incomplete) === NoFlags
) {
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
上面的代码实在过于抽象,结合 jsx
结构来分析:
function App() {
const [num, setNum] = useState(1);
return (
<div className="App">
<header className="App-header">
<p onClick={() => setNum(x => x + 1)}>
<code title={num}>{num}</code>
</p>
</header>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
第一个进入 completeUnitOfWork
的节点为 <code />
,注,
进入第一个具有副作用的节点 <code />
,对其父节点 <p />
做如下操作:
p.firstEffect = <code />
p.lastEffect = <code />
向上遍历,进入 p
节点 ,对其父节点 <header />
做如下操作:
header.firstEffect = <code />
<p />.nextEffect = <code />
header.lastEffect = <p />
ok,后续不再分析了,简单来说,EffectList
形成的是一条链表,任意一个 Fiber
节点,通过 firstEffect
可以找到第一个需要更新的节点,通过 nextEffect
找到下一个需要更新的节点。
在上述 jsx
中只存在两个副作用节点 <code> | <p>
节点,因此无论是 workInProgress
指向的是 <div />
或者 <headers />
其副作用链表均为:
workInProgress(div|header)
→ workInProgress.firstEffect(code)
→ workInProgress.firstEffect.nextEffect(p)
2
3
下一个问题,通过打印入口也是类似的结果。
# 问题:reconciler
递归结束
核心代码:
// 调度: render 阶段 => commit 阶段
function performConcurrentWorkOnRoot(root) {
// 省略若干代码.....
var finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
// commit 阶段入口
commitRoot(root);
}
2
3
4
5
6
7
8
通过在终端打印如下:
> finishedWork
FiberNode {tag: 3, key: null, elementType: null, type: null, stateNode: FiberRootNode, …}
> finishedWork.firstEffect
FiberNode {tag: 5, key: null, elementType: 'code', type: 'code', stateNode: code, …}
> finishedWork.firstEffect.updateQueue
(4) ['title', 2, 'children', '2']
> finishedWork.firstEffect.nextEffect
FiberNode {tag: 5, key: null, elementType: 'p', type: 'p', stateNode: p, …}
> finishedWork.firstEffect.nextEffect.nextEffect
null
2
3
4
5
6
7
8
9
10
进入 commmit
阶段的实际上是 root
根节点(tag
一定为 3
)
通过
firstEffect
可以找到第一个需要更新的节点,并且通过updateQueue
中保存了对应需要改变的props
属性(此部分由domDifPropties
函数比较产生)通过
nextEffect
可以继续查找节点,直至最后一个节点null
# 5.额外说明
# 1. 源码 ReactFiberHostConfig
为空函数?
在 ReactFiberCompletWork.old.js
中的开头一部分比较疑惑,点击进去,发现此文件为一个空函数。
import {
createInstance,
createTextInstance,
appendInitialChild,
finalizeInitialChildren,
prepareUpdate,
supportsMutation,
supportsPersistence,
cloneInstance,
cloneHiddenInstance,
cloneHiddenTextInstance,
createContainerChildSet,
appendChildToContainerChildSet,
finalizeContainerChildren,
getFundamentalComponentInstance,
mountFundamentalComponent,
cloneFundamentalInstance,
shouldUpdateFundamentalComponent,
preparePortalMount,
prepareScopeUpdate,
} from './ReactFiberHostConfig';
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
此 ReactFiberHostConfig.js
在运行时会被定位到 ReactDOMHostConfig.js
文件,这是 react
在架构上单独将跨端抽离出来了。
因此可根据不同的宿主环境进行切换的,比如说在浏览器环境下,该文件路径实际解析执行的是 ReactDOM
文件下的 ReactDOMHostConfig.js
文件。
这也是在 React
中将 div
或 img
对标的处理组件命名为 HostComponent
的原因。
Host
在编程语言里头感觉上可以翻译为 宿主环境
,如在 web
环境下,HostComponent
可以被认为值得是 div
或 img
这类标签;而在 Native
下 HostComponent
对标的是类似于 View
这类基础组件。
# 2. 待补充案例
style 属性的变化
属性更新的变化
属性删除的变化
2
3