React 源码漂流记:React 调和器核心源码解读(四)
# 目录
# 前言
在上一篇文章中,我们探讨了 React 调和器中 renderRootSync
、 renderRootConcurrent
、 workLoopSync
、 workLoopConcurrent
、 performUnitOfWork
5 个核心函数,整体过程偏于梗概和流程,虽然过程较为简单,但是对于理解整体调和的过程却是至关重要的。因此,上文与本文的衔接较为紧密。
先回顾一下上文的情节,5 个函数,从 Render
阶段的开始,到 WorkLoop
的启动,再到 performUnitOfWork
的具体的调和工作,整个调和过程其实比较清晰。在以下几篇文章中,将着重分析调和中的捕获和冒泡过程,即单个 Fiber 节点的调和过程。
注意
因篇幅限制,本文只探讨重要类型的组件的调和过程的梗概,具体组件的调和过程细节不探讨,后文详述。
# beginWork
从整体上看, beginWork
是根据 workInProgress
Fiber 的类型,而决定采取不同的 mount
或者 update
的策略。
# didReceiveUpdate
didReceiveUpdate
对于理解这部分代码很重要,因此我们先来分析下 didReceiveUpdate
的含义和意义。 didReceiveUpdate
表示是否接收到变化(更新),如果未接收到更新则可以复用 Fiber 节点(意思是复用下一级的子节点提前返回 null 阻断后续的捕获过程)。
简化代码如下:
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps !== newProps) {
// 如果有 props 变化,则标记接收到更新
didReceiveUpdate = true;
} else {
// ......
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
// ......
}
2
3
4
5
6
7
8
9
10
11
12
13
14
分析如下:
- 参数含义:通过上文中的探讨,我们可以明了的是:
current
表示 current FiberTree 的 FiberNode,是已经渲染过的节点;workInProgress
是 workInProgress FiberTree 的 FiberNode,是即将要被渲染的节点。 current === null
可以区分 mount 阶段和 update 阶段(注意:这里不是指调和的阶段,而是指从 React 的应用的渲染周期来看,包含初次渲染即 mount 阶段和更新渲染即 update 阶段)。
注意
这里为什么比较 Props 的引用而不是浅比较呢?为什么在执行 Component
之前需要比较 Props? 在 performUnitOfWork
中 unitOfWork.memoizedProps = unitOfWork.pendingProps
中有如下代码,那次此处 oldProps
大概率会与 newProps
相等?
弄清楚 didReceiveUpdate
含义和复用节点的条件之后,下面我们再具体探讨 beginWork
的内容。
# beginWork
注意
以下代码只保留了核心内容,细节内容有删改。
// src/react/packages/react-reconciler/src/ReactFiberBeginWork.new.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
switch (workInProgress.tag) {
case IndeterminateComponent:
return mountIndeterminateComponent(/*......*/);
case LazyComponent:
return mountLazyComponent(/*......*/);
case FunctionComponent:
return updateFunctionComponent(/*......*/);
case ClassComponent:
return updateClassComponent(/*......*/);
case HostRoot:
return updateHostRoot(/*......*/);
case HostComponent:
return updateHostComponent(/*......*/);
case HostText:
return updateHostText(/*......*/);
case SuspenseComponent:
return updateSuspenseComponent(/*......*/);
case HostPortal:
return updatePortalComponent(/*......*/);
case ForwardRef:
return updateForwardRef(/*......*/);
case Fragment:
return updateFragment(/*......*/);
// ......
case ContextProvider:
return updateContextProvider(/*......*/);
case ContextConsumer:
return updateContextConsumer(/*......*/);
case MemoComponent:
return updateMemoComponent(/*......*/);
case SimpleMemoComponent:
return updateSimpleMemoComponent(/*......*/);
case IncompleteClassComponent:
return mountIncompleteClassComponent(/*......*/);
// ......
case OffscreenComponent:
return updateOffscreenComponent(/*......*/);
// ......
}
}
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
# mountIndeterminateComponent
此函数针对首次渲染时的未知类型的组件(并且是立即挂载的组件)进行判断,区分函数式组件和 伪装成函数的类组件
进行相关处理,并且调和 ReactChildren(本质上是 ReactElement)。
源码如下:
// src/react/packages/react-reconciler/src/ReactFiberBeginWork.new.js
function mountIndeterminateComponent(
_current,
workInProgress,
Component,
renderLanes,
) {
// ......
const props = workInProgress.pendingProps;
// 清理 workInProgress context,即 workInProgress.dependencies.firstContext = null
prepareToReadContext(workInProgress, renderLanes);
// 执行 Component 并获得返回值,如果是类组件,value 是组件实例
const value = Component(props, context)
// 判断是否是合法的类组件
if (
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
// 将 workInProgress 标记为类组件
workInProgress.tag = ClassComponent;
workInProgress.updateQueue = null;
// ......
// 更新 state
workInProgress.memoizedState =
value.state !== null && value.state !== undefined ? value.state : null;
// 初始化 state 更新队列
initializeUpdateQueue(workInProgress);
// 将类组件实例设置到 workInProgress Fiber 上
adoptClassInstance(workInProgress, value);
// 挂载类组件实例,触发 `getDerivedStateFromProps` 和 `componentWillMount`
mountClassInstance(workInProgress, Component, props, renderLanes);
// 调和 ReactElement
return finishClassComponent(
null,
workInProgress,
Component,
true,
false,
renderLanes,
);
} else {
// 将 workInProgress 标记为 FunctionComponent
workInProgress.tag = FunctionComponent;
// ......
// 调和 ReactElement
reconcileChildren(null, workInProgress, value, renderLanes);
// 返回 child 节点
return workInProgress.child;
}
}
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
需要注意一下几点:
- 此处传入的 Component,实际上是
workInProgress.type
,在前文中我们已经了解到Fiber.type
挂载着 fiber 所对应的function/class/module
类型的组件,属于 Fiber 和 ReactElement 沟通的媒介。 - 如果函数 Component 执行之后返回
object
且包含render
方法,则会被当做类组件处理。 - 注意:此函数只在初次渲染函数式组件或者
伪装成函数的类组件
时调用。调用之后,该组件会被判定为ClassComponent
或者FunctionComponent
,调用结束后返回child
节点,以继续完成WorkLoop
中的捕获过程。
# mountLazyComponent
mountLazyComponent
是针对 LazyComponent
(延迟加载组件) 而言的,需要加载组件时本质上还是根据 beginWork
中的组件类型策略进行处理,因此此处不再赘述。Lazy 组件将在之后的章节单独探讨。
提示
在下文的几种类型的组件的处理中,我们需要强化两条思路,复用(简单的复用 current ChildrenFiberTree 以提前结束当前节点的捕获过程)和不复用。
# updateFunctionComponent
updateFunctionComponent
函数调和 FunctionComponent
类型的组件(组件为函数式组件,一般非初次调和)。
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes,
) {
// ......
// 获取 ReactChildren
const nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
undefined,
renderLanes,
);
// 如果非初次渲染且未接收到更新则复用 Fiber 节点,提前退出
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
// 调用 `cloneChildFibers` 复用节点,返回 child 节点
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// ......
// 调和 ReactChildren
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
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
从以上过程可以看出调和组件大概分为如下的步骤:
- 获取组件的 ReactChildren。
- 判断是否可以复用节点,如果可以则调用
cloneChildFibers
复用子层级节点并提前返回(返回子节点),否则则调用reconcileChildren
重新调和子层级节点并返回子节点。
# updateClassComponent
updateClassComponent
函数调和 ClassComponent
类型的组件(class 语法的类组件)。
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
// ......
// 获取类组件实例
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
// ......
// 若实例尚未创建,则构建组件实例,new Component(props, context)
constructClassInstance(workInProgress, Component, nextProps);
// 挂载组件,调用相关生命周期钩子
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
} else if (current === null) {
// 应用加载时,如果已经有组件实例,则复用此实例,并且调用相关的生命周期钩子 `componentWillReceiveProps`、`getDerivedStateFromProps`
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderLanes,
);
} else {
// 如果在更新阶段,且已经有组件实例,则更新组件实例,并且调用相关的生命周期函数 `componentWillReceiveProps`、`getDerivedStateFromProps`,同时更新实例的 props, state 和 context
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
}
// 根据 shouldUpdate 决定是否需要复用节点,reconcileChildren 或者 cloneChildFibers,并返回子节点
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
false,
renderLanes,
);
// ......
return nextUnitOfWork;
}
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
这里根据 instance
和 current
分成了 3 种情况,如下:
阶段 / 有无实例 | 无实例 | 有实例 |
---|---|---|
mount | 构建实例,挂载实例(应用初始化,mount 钩子) | 复用实例(update 钩子) |
update | 构建实例,挂载实例(suspended 组件,mount 钩子) | 更新实例(update 钩子) |
挂载或者更新完组件实例之后,根据 shouldUpdate
判断是否可以复用节点,复用则调用 cloneChildFibers
,否则调用 reconcileChildren
(与 FunctionComponent 一致)。
# updateHostRoot
updateHostRoot
处理 HostRootFiber
节点(FiberTree 的根节点,与 FiberRoot 容器双向链接)的调和过程。
function updateHostRoot(current, workInProgress, renderLanes) {
// ......
const nextProps = workInProgress.pendingProps;
const prevChildren = workInProgress.memoizedState.element;
// 从 current 上复制 updateQueue 到 workInProgress
cloneUpdateQueue(current, workInProgress);
// 消费 workInProgress 上的 updateQueue 更新 workInProgress.memoizedState
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
const nextChildren = workInProgress.memoizedState.element;
// 判断消费 updateQueue 之后 children 是否发生了变化,如果没有发生变化则复用节点,否则重新调和子树,并返回子节点
if (nextChildren === prevChildren) {
// ......
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// ......
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
// ......
return workInProgress.child;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cloneUpdateQueue
从 current 复制updateQueue
到workInProgress
,以确保processUpdateQueue
在处理时并不是直接在 current 上操作。processUpdateQueue
reduceupdateQueue
中的 update,并且获得最新的 state 和 effect。- 在处理
updateQueue
前后,判断 children 是否发生变化,如未发生变化可直接复用节点。
# updateHostComponent
HostComponent 指原生的 HTML 节点。 updateHostComponent
调和原生的 HTML 节点。
function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
// ......
const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;
const prevProps = current !== null ? current.memoizedProps : null;
let nextChildren = nextProps.children;
// 判断该节点是否应该直接设置文本内容,如 textarea, noscript 等,对于此类节点不需要再建立 nextChildren
// 为 HostText 以节省性能
const isDirectTextChild = shouldSetTextContent(type, nextProps);
if (isDirectTextChild) {
nextChildren = null;
// 如果原节点存在,原来是 DirectTextChild,现在不是了,标记 ContentReset,表示重置文本内容
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
workInProgress.flags |= ContentReset;
}
// ......
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 对于
HostComponent
而言不需要复用节点,一律重新调和节点。
# updateHostText
updateHostText
调和 HostText
节点。
function updateHostText(current, workInProgress) {
// ......
return null;
}
2
3
4
文本节点一定是叶子节点,因此不需要再调和 children。注意:这里返回 null,会使捕获过程暂时结束,在 performUnitOfWork
函数中,会进而转入冒泡阶段。
# 扩展
# 问题
# Component
即 workInProgress.type
是如何初始化的?
在应用挂载阶段, Component
所对应的 FiberNode 是在 prepareFreshStack
函数中创建的,参见 workInProgress 是如何初始化的?,而具体的 Fiber.type
的初始化请参见 React 首次渲染过程解读。
# mountIndeterminateComponent
为什么都能够执行 Component(props, context)
?
在 mountIndeterminateComponent
是有做类组件和函数式组件的判断的,那么大家可能会有这样的疑问,类组件时 class
,应该是无法被执行的才对? class
定义的类组件确实无法被执行,但是并非所有的类组件都是 class
定义的、都是无法执行的。
- 利用 class 写类组件只是一种语法糖,并非只有这一种写法。其实,类组件还可以这样写:
function TestIndeterminateComponent() {
return {
componentDidMount() {
console.log('componentDidMount')
},
state: { count: 1 },
updateCount() {
const { count } = this.state;
this.updater.enqueueSetState(
this,
{ count: count + 1 },
undefined,
"setState"
);
},
render() {
return <div onClick={() => this.updateCount()}>{this.state.count}</div>;
},
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
我们可能注意到,在 mountIndeterminateComponent
判断为 function 语法的类组件
(暂时这样称呼)之后,相比于函数式组件,多出 initializeUpdateQueue
、 adoptClassInstance
、 mountClassInstance
的步骤,实际上是为了挂载相关的属性、调用声明周期钩子等以完成完成的类组件的行为。注意一下以下几点:
- 类组件继承自
React.component
实际上是语法糖的实现,帮助我们初始化组件的必要属性如 props、state、updater 等,并且实现setState
、forceUpdate
等方法。(参见源码文件src/react/packages/react/src/ReactBaseClasses.js
) mountIndeterminateComponent
只在首次渲染时处理函数式组件或者像如上这种伪装成函数式组件的类组件
。
# 总结
- 本文介绍了常用的组件类型的捕获过程。主要分成一下 3 个主要步骤:获取 Component 的
nextChildren
;采用一定的复用策略判断是否可以复用子节点调和过程,如果可以则通过复用子层级节点提前进入下一层的捕获,否则则重新调和子层级节点;返回workInProgress.child
继续捕获过程。 beginWork
的返回值有两种情况:返回当前节点的子节点,然后会以该子节点作为下个工作单元继续beginWork
,不断往下生成 fiber 节点,构建 workInProgress 树(捕获);返回 null,当前 fiber 子树的遍历就此终止,从当前 fiber 节点开始往回进行completeWork
(冒泡)。beginWork
主要作用就是针对当前捕获到的节点进行处理,并且返回子节点继续捕获,捕获过程中逐渐创建 workInProgress FiberTree。- 本文只是探讨
beginWork
的大致流程,具体的流程会在后文继续分析,包括状态的更新,fiber 的 diff 算法等。