文中 React 相关源码版本, v16.8.0
# Fiber 是什么?
Fiber 是 React v16 中启用的一种全新的架构,旨在解决大型 React 项目的性能问题。
在开始详细介绍 Fiber 之前,让我们先了解本文的核心:Reconciliation 模块。
# Reconciliation 模块
Reconciliation 模块也叫协调模块。主要负责任务协调、生命周期函数管理等。React 16 版本之前这个模块使用 Stack Reconciler 调度算法,16 版本之后重构为 Fiber Reconciler,一种全新的调度算法。在 React Conf 2017 上 Lin Clark 的A Cartoon intro to Fiber,就用漫画的方式形象的展示了 Stack Reconciler 与 Fiber Reconciler 运行区别。
# Stack Reconciler
Stack Reconciler 调度算法,通过自顶向下递归的形式遍历 Virtual DOM, 递归一但进行,想要中断和恢复就不容易操作,如果递归栈比较多,那么 JS 线程会一直占用主进程,就有可能导致页面卡顿。为什么呢?因为浏览器除了 JS 进程以外,还包括 UI 渲染线程、事件线程、定时器触发线程、HTTP 请求线程等。JS 线程是可以操作 DOM 的,如果在操作 DOM 的同时 UI 线程也在进行渲染的话,就会发生不可预期的展示结果,因此 JS 线程与 UI 渲染线程是互斥的,每当 JS 线程执行时,UI 渲染线程会挂起,UI 更新会被保存在队列中,等待 JS 线程空闲后立即被执行。对于事件线程而言,当一个事件被触发时该线程会把事件添加到队列末尾,等待 JS 线程空闲后处理。因此,长时间的 JS 持续执行,就会造成 UI 渲染线程长时间地挂起,触发的事件也得不到响应,用户层面就会感知到页面卡顿甚至卡死了。那么 JS 执行时间多长会是合理的呢?现在大多数设备的帧率都是 60 帧/秒,也就是每帧 16.6ms 左右用户会感觉相当流畅。浏览器一帧内完成的任务如下图所示:
包括以下几个部分
- 处理用户的交互
- JS 解析执行
- 帧开始。窗口尺寸变更,页面滚动处理
- requestAnimationFrame callbacks
- 布局 Layout
- 绘制 Paint 在一帧中,我们需要将 JS 执行时间控制在合理的范围内,不影响后续 Layout 与 Paint 的过程,尽量保证 JS 的执行时间加上其他步骤的时间不能超过 16ms。
那么如何让单线程的 JS 把执行时间控制在合理的范围内?对于前端框架来说,解决这个问题有三个方向:
1、优化每一个任务,让他有多快就多快,挤压 CPU 运算量
2、快速响应用户,让用户觉得够快,不能阻塞用户的交互
3、尝试 Worker 多线程
Vue 选择的是第一种,使用模板
让它有了很多优化的空间,配合响应式的机制可以让 Vue 精确的更新节点,有兴趣的读者可以了解下
尤雨溪 2019 Vue Conf 的演讲。React 选择了第二种方案,也就是引入 Fiber 架构。Worker 多线程在保证状态和视图一致性会相当的麻烦。
下面我们来看看 Fiber 架构具体是怎样优化 Reconcilation 这种 CPU 密集型操作的。
# Fiber Reconciler
为了防止 Reconcilation 阶段长时间的占用浏览器资源,React 通过 Fiber 架构改进 Reconcilation 过程,使得渲染过程可以被中断,允许渲染过程分段完成,而不必需一次性完成,中间可以返回至主进程控制执行其他高优先级的任务,浏览器空闲后恢复渲染。 这种改进后的调度算法为 Fiber Reconciler。
那么问题来了:
- 渲染过程如何分段?
- 如何知道浏览器空闲?
- 如何设定优先级?
- 高优先级如何插队,低优先级如何恢复?
接下来整个 Fiber 架构就是来解决这些问题的。
# requestIdleCallback
上面说到理想的一帧时间是16.6ms
,如果浏览器处理完布局和绘制任务之后,还有剩余的时间,就会执行requestIdleCallback
回调,回调函数会接受到一个名为IdleDeadline
的参数,这个参数可以获取当前空闲时间以及回调是否在超时时间前执行的状态。由于浏览器可能始终处于繁忙的状态,导致 callback 一直无法执行,它还能够设置超时时间(timeout),一旦超过时间(didTimeout)会使任务强制执行。
// 浏览器执行线程空闲时间调用 work,超过 2000ms 后立即必须执行
requestIdleCallback(work, { timeout: 2000 })
function work(deadline) {
// 如果有剩余时间,或者任务已经超时,并且存在任务就需要执行
while (
(deadline.timeRemaining() > 0 || deadline.didTimeout) &&
tasks.length > 0
) {
// do work
}
// 当前存在任务,再次调用 requestIdleCallback,会在空闲时间执行 work
if (tasks.length > 0) {
requestIdleCallback(work, { timeout: 2000 })
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
结合上图得知,requestIdleCallback
是在 Layout 与 Paint 之后执行的,因此不建议在requestIdleCallback
里做 DOM 操作(如 getBoundingClientRect、clientWidth 等操作),会重新触发 Layout 与 Paint,另外由于修改 DOM 操作的时间是不可预测的,因此很容易超出当前帧空闲时间的阈值,如果要操作 DOM, 推荐的做法是在requestAnimationFrame
里面做 DOM 的修改,可以在requestIdleCallback
里面构建 Document Fragment ,然后在下一帧的requestAnimationFrame
里面应用 Fragment。因为 requestIdleCallback
的兼容性比较差,React 内部通过 requestAnimationFrame
作为 polyfill,通过帧率动态调整,计算 timeRemaing,模拟requestIdleCallback
,从而实现时间分片(Time Slicing),一个时间片就是一个渲染帧内 JS 能获得的最大执行时间。
- polyfill
// ref: https://github.com/facebook/react/blob/v16.8.0/packages/scheduler/src/Scheduler.js#L455
var ANIMATION_FRAME_TIMEOUT = 100
// ...
// 当前tab激活, requestAnimationFrame 工作
// 当前tab不激活,setTimeout 工作
// 两者交替接管任务执行
var requestAnimationFrameWithTimeout = function(callback) {
// schedule rAF and also a setTimeout
rAFID = localRequestAnimationFrame(function(timestamp) {
// cancel the setTimeout
localClearTimeout(rAFTimeoutID)
callback(timestamp)
})
rAFTimeoutID = localSetTimeout(function() {
// cancel the requestAnimationFrame
localCancelAnimationFrame(rAFID)
callback(getCurrentTime())
}, ANIMATION_FRAME_TIMEOUT)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 计算每一帧的截止时间
// ref: https://github.com/facebook/react/blob/v16.8.0/packages/scheduler/src/Scheduler.js#L615
var scheduledHostCallback = null //代表任务链表的执行器
var timeoutTime = -1 //代表最高优先级任务firstCallbackNode的过期时间
var activeFrameTime = 33 // 一帧的渲染时间33ms,这里假设 1s 30帧
var frameDeadline = 0 //代表一帧的过期时间,通过rAF回调入参t加上activeFrameTime来计算
// rAF的回调是每一帧开始的时候,所以适合做一些轻量任务,不然会阻塞渲染。
function animationTick(rafTime) {
// 有任务再进行递归,没任务的话不需要工作
if (scheduledHostCallback !== null) {
requestAnimationFrame(animationTick)
}
var nextFrameTime = rafTime - frameDeadline + activeFrameTime
// 动态帧率计算,一开始假设设备是 30hz, 即一帧是 33ms, 这里对每一帧渲染时间进行优化,
// 会在渲染过程中不断压缩每一帧的渲染时间,达到系统的刷新频率(60hz为16.6ms)
activeFrameTime =
nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime
//计算当前帧的截止时间,用开始时间加上每一帧的渲染时间
frameDeadline = rafTime + activeFrameTime
}
// 某个地方会调用
requestAnimationFrame(animationTick)
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
- 通过消息通道
MessageChannel
把回调延迟到 Paint 操作后执行
// ref: https://github.com/facebook/react/blob/v16.8.0/packages/scheduler/src/Scheduler.js#L517
var channel = new MessageChannel()
var port = channel.port2 //port2用来发消息
channel.port1.onmessage = function(event) {
// port1监听消息的回调来做任务调度的具体工作
// onmessage的回调函数的调用时机是在一帧的paint完成之后,所以适合做一些重型任务,也能保证页面流畅不卡顿
}
2
3
4
5
6
7
# 任务的优先级
为了避免任务被饿死,可以设置一个超时时间,根据不同的优先级超时时间有所差异。
// 分为5种优先级,数字越小优先级越高
var ImmediatePriority = 1 //最高优先级
var UserBlockingPriority = 2 //用户阻塞型优先级
var NormalPriority = 3 //普通优先级
var LowPriority = 4 // 低优先级
var IdlePriority = 5 // 空闲优先级
2
3
4
5
6
这 5 中优先级对应的 5 个过期时间
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
var maxSigned31BitInt = 1073741823
// 立马过期 ==> ImmediatePriority
var IMMEDIATE_PRIORITY_TIMEOUT = -1
// 250ms以后过期
var USER_BLOCKING_PRIORITY = 250
//
var NORMAL_PRIORITY_TIMEOUT = 5000
//
var LOW_PRIORITY_TIMEOUT = 10000
// 永不过期, 大概12.427天,就是打开一个tab,大概12.427天过期
var IDLE_PRIORITY = maxSigned31BitInt
2
3
4
5
6
7
8
9
10
11
12
13
14
React 内对任务的操作使用了双向循环链表结构,每个任务在添加到链表里的时候,都会通过 performance.now() + timeout来得出这个任务的过期时间,随着时间的推移,当前时间会越来越接近这个过期时间,所以过期时间越小的代表优先级越高。
以上,对 React 的基本概念进行了介绍,下面通过分析源码进一步的了解 Fiber 架构。
# 源码分析
React Fiber 架构引入了新的数据结构:fiber节点
# fiber
fiber节点结构关键重要字段如下:
export type Fiber = {
...
// 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
stateNode: any,
// 单链表树结构
return: Fiber | null,// 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
child: Fiber | null,// 指向自己的第一个子节点
sibling: Fiber | null, // 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
// 更新相关
pendingProps: any, // 新的变动带来的新的props
memoizedProps: any, // 上一次渲染完成之后的props
updateQueue: UpdateQueue<any> | null, // 该Fiber对应的组件产生的Update会存放在这个队列里面
memoizedState: any, // 上一次渲染的时候的state
// Scheduler 相关
expirationTime: ExpirationTime, // 代表任务在未来的哪个时间点应该被完成,不包括他的子树产生的任务
// 快速确定子树中是否有不在等待的变化
childExpirationTime: ExpirationTime,
// 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber
// 我们称他为`current <==> workInProgress`
// 在渲染完成之后他们会交换位置
alternate: Fiber | null,
// Effect 相关的
effectTag: SideEffectTag, // 用来记录Side Effect
nextEffect: Fiber | null, // 单链表用来快速查找下一个side effect
firstEffect: Fiber | null, // 子树中第一个side effect
lastEffect: Fiber | null, // 子树中最后一个side effect
....
}
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
fiber 树结构图(链表结构)如下:
# 源码函数调用流程
上文中说到 Reconciliation 模块(协调模块),主要负责任务协调、生命周期函数管理等。它的工作氛围两部分:
1、reconciliation
简单来说就是找到更新工作,通过 Diff Fiber Tree 找出要做的更新工作,因为使用了链表结构,在迭代的过程可以缓存计算结果,打断计算结果,以及恢复执行。
2、commit
提交更新并调用对应渲染模块进行渲染,为了防止页面抖动,该过程是同步且不能被打断。
接下来看看这两个阶段具体的函数调用流程。
# reconciliation阶段
我们以 ReactDOM.render() 方法为入口,看看 reconciliation 阶段的函数调用流程:
从上图可以看到,整个流程可以划分为三个部分,简单的概括下三部分的工作:
1、第一部分从 ReactDOM.render()
方法开始,把接收的 React Element 转换为 Fiber 节点,并为其设置优先级,创建 Update,加入到更新队列,这部分主要是做一些初始数据的准备。
2、第二部分主要是三个函数:scheduleWork
、requestWork
、performWork
,即安排工作、申请工作、正式工作三部曲,React 16 新增的异步调用功能在这部分实现,这部分就是 Schedule 阶段
。
3、第三部分是一个大循环,遍历所有的 Fiber 节点,通过 Diff 算法计算所有更新工作,产出 EffectList 给到 commit 阶段使用,这部分的核心是 beginWork
函数。
# 第一部分
第一部分主要是做一些初始数据的准备,感兴趣的读者可自行前往查阅。
# 第二部分:任务协调
核心函数:scheduleWork
、requestWork
、performWork
(安排工作、申请工作、正式工作三部曲)
scheduleWork
函数内部核心是scheduleWorkToRoot
,这个函数从触发 setState 的 Fiber 节点,不断向上回溯,通知沿途上的 Fiber 节点,你有子孙节点被更新了,直至最顶端 HostRoot。
function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
// Update the source fiber's expiration time
if (fiber.expirationTime < expirationTime) {
fiber.expirationTime = expirationTime;
}
let alternate = fiber.alternate;
if (alternate !== null && alternate.expirationTime < expirationTime) {
alternate.expirationTime = expirationTime;
}
// Walk the parent path to the root and update the child expiration time.
let node = fiber.return;
let root = null;
if (node === null && fiber.tag === HostRoot) {
root = fiber.stateNode;
} else {
while (node !== null) {
alternate = node.alternate;
if (node.childExpirationTime < expirationTime) {
node.childExpirationTime = expirationTime;
if (
alternate !== null &&
alternate.childExpirationTime < expirationTime
) {
alternate.childExpirationTime = expirationTime;
}
} else if (
alternate !== null &&
alternate.childExpirationTime < expirationTime
) {
alternate.childExpirationTime = expirationTime;
}
if (node.return === null && node.tag === HostRoot) {
root = node.stateNode;
break;
}
// 向上追溯
node = node.return;
}
}
return root;
}
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
requestWork
比较简短,判断什么样的方式去执行更新
// requestWork is called by the scheduler whenever a root receives an update.
// It's up to the renderer to call renderRoot at some point in the future.
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
addRootToSchedule(root, expirationTime);
if (isRendering) {
// Prevent reentrancy. Remaining work will be scheduled at the end of
// the currently rendering batch.
return;
}
if (isBatchingUpdates) {
// Flush work at the end of the batch.
if (isUnbatchingUpdates) {
// ...unless we're inside unbatchedUpdates, in which case we should
// flush it now.
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, false);
}
return;
}
// TODO: Get rid of Sync and use current time?
if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}
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
performSyncWork
function performSyncWork() {
performWork(Sync, false);
}
function performWork(minExpirationTime: ExpirationTime, isYieldy: boolean) {
// Keep working on roots until there's no more work, or until there's a higher
// priority event.
findHighestPriorityRoot();
if (isYieldy) {
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
if (enableUserTimingAPI) {
const didExpire = nextFlushedExpirationTime > currentRendererTime;
const timeout = expirationTimeToMs(nextFlushedExpirationTime);
stopRequestCallbackTimer(didExpire, timeout);
}
while (
nextFlushedRoot !== null &&
nextFlushedExpirationTime !== NoWork &&
minExpirationTime <= nextFlushedExpirationTime &&
!(didYield && currentRendererTime > nextFlushedExpirationTime)
) {
performWorkOnRoot(
nextFlushedRoot,
nextFlushedExpirationTime,
currentRendererTime > nextFlushedExpirationTime,
);
findHighestPriorityRoot();
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
}
} else {
while (
nextFlushedRoot !== null &&
nextFlushedExpirationTime !== NoWork &&
minExpirationTime <= nextFlushedExpirationTime
) {
performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
findHighestPriorityRoot();
}
}
// We're done flushing work. Either we ran out of time in this callback,
// or there's no more work left with sufficient priority.
// If we're inside a callback, set this to false since we just completed it.
if (isYieldy) {
callbackExpirationTime = NoWork;
callbackID = null;
}
// If there's work left over, schedule a new callback.
if (nextFlushedExpirationTime !== NoWork) {
scheduleCallbackWithExpirationTime(
((nextFlushedRoot: any): FiberRoot),
nextFlushedExpirationTime,
);
}
// Clean-up.
finishRendering();
}
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
scheduleCallback这个内部函数还没转正,使用的是unstable_scheduleCallback
function unstable_scheduleCallback(callback, deprecated_options) {
var startTime =
currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
var expirationTime;
if (
typeof deprecated_options === 'object' &&
deprecated_options !== null &&
typeof deprecated_options.timeout === 'number'
) {
// FIXME: Remove this branch once we lift expiration times out of React.
expirationTime = startTime + deprecated_options.timeout;
} else {
switch (currentPriorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
expirationTime = startTime + USER_BLOCKING_PRIORITY;
break;
case IdlePriority:
expirationTime = startTime + IDLE_PRIORITY;
break;
case LowPriority:
expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
}
}
var newNode = {
callback,
priorityLevel: currentPriorityLevel,
expirationTime,
next: null,
previous: null,
};
// Insert the new callback into the list, ordered first by expiration, then
// by insertion. So the new callback is inserted any other callback with
// equal expiration.
if (firstCallbackNode === null) {
// This is the first callback in the list.
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled();
} else {
var next = null;
var node = firstCallbackNode;
do {
if (node.expirationTime > expirationTime) {
// The new callback expires before this one.
next = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);
if (next === null) {
// No callback with a later expiration was found, which means the new
// callback has the latest expiration in the list.
next = firstCallbackNode;
} else if (next === firstCallbackNode) {
// The new callback has the earliest expiration in the entire list.
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled();
}
var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}
return newNode;
}
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
# 第三部分:beginWork
从上面的函数调用流程图可以看到,beginWork
在大循环中被调用,返回当前节点的子节点
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;
if (!hasLegacyContextChanged() && (updateExpirationTime === NoWork || updateExpirationTime > renderExpirationTime)) {
switch (workInProgress.tag) {
case HostRoot:
...
case HostComponent:
...
case ClassComponent:
pushLegacyContextProvider(workInProgress);
break;
case HostPortal:
...
case ContextProvider:
...
case Profiler:
...
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderExpirationTime,);
}
// Before entering the begin phase, clear the expiration time.
workInProgress.expirationTime = NoWork;
switch (workInProgress.tag) {
case IndeterminateComponent:
return mountIndeterminateComponent(current, workInProgress, renderExpirationTime,);
case FunctionalComponent:
return updateFunctionalComponent(current, workInProgress, renderExpirationTime,);
case ClassComponent:
return updateClassComponent(current, workInProgress, renderExpirationTime,);
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
return updateHostComponent(current, workInProgress, renderExpirationTime);
case HostText:
return updateHostText(current, workInProgress);
case PlaceholderComponent:
return updatePlaceholderComponent(current, workInProgress, renderExpirationTime,);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderExpirationTime,);
case ForwardRef:
return updateForwardRef(current, workInProgress, renderExpirationTime);
case Fragment:
return updateFragment(current, workInProgress, renderExpirationTime);
case Mode:
return updateMode(current, workInProgress, renderExpirationTime);
case Profiler:
return updateProfiler(current, workInProgress, renderExpirationTime);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderExpirationTime,);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderExpirationTime,);
default:
...
}
}
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
这里介绍下React Fiber架构的双缓冲(double-buffering)技术。
图片来源:《React Fiber》
上图是 Reconciliation 完成后的状态,左边是旧树,右边是WIP树。对于需要变更的节点,都打上了标签。 在提交阶段,React 就会将这些打上标签的节点应用变更。
在 React 中,workInProgress 树是一个缓冲,它在 Reconcilation 完毕后一次性提交给浏览器进行渲染,它可以减少内存分配和垃圾回收,旧的 Fiber 树不变动的节点,WIP 会克隆复用。
双缓存技术还有另外一个重要的场景就是异常的处理,比如当一个节点抛出异常,仍然可以继续沿用旧树的节点,避免整棵树挂掉。
Dan 在 Beyond React 16 演讲中用了一个非常恰当的比喻,那就是 Git 功能分支,你可以将 WIP 树想象成从旧树中 Fork 出来的功能分支,你在这新分支中添加或移除特性,即使是操作失误也不会影响旧的分支。当你这个分支经过了测试和完善,就可以合并到旧分支,将其替换掉。
从 beginWork 源码来看,主要分为两部分,一部分是对 Context 的处理,一部分是根据 fiber 对象的 tag 类型,调用对应的 update 方法。在这里我们重点关注第二部分,以 ClassComponent 类型为例,讲讲 updateClassComponent 函数中做了什么?
主要有两部分:生命周期函数的调用及 diff 算法,diff 算法我们单独一篇文章进行讲解。
current为null,意味着当前的update是组件第一次渲染
1、调用 constructClassInstance
构造组件实例,主要是调用 constructor 构造函数,并注入classComponentUpdater
2、mountClassInstance
则是调用 getDerivedStateFromProps
生命周期函数(v16) 及 UNSAFE_componentWillMount
生命周期函数
current不为null,调用 updateClassInstance 方法
1、如果新老 props 不一致,则会调用 UNSAFE_componentWillReceiveProps
生命周期函数
2、然后调用 shouldComponentUpdate
生命周期函数,获得 shouldUpdate
值,若未定义此生命周期函数,默认为 true(是否重新渲染),如果 shouldUpdate 为 true,则会调用 UNSAFE_componentWillUpdate
生命周期函数
最后调用 finishClassComponent
方法,那么 finishClassComponent
函数 中做了什么呢?
如果 shouldUpdate 为false,表示不需要更新,直接返回。
如果 shouldUpdate 为true,调用实例的 render 方法,返回新子节点。
如果是首次渲染,调用 mountChildFibers 创建子节点的Fiber实例。
否则,调用 reconcileChildFibers 对新老子节点进行Diff。
以上,就是beginWork函数的整个过程,可以知道遍历完 Fiber 树之后,通过 Diff 算法,可以产出 EffectList,给 commit 阶段使用。
# commit阶段
commit 阶段做的事情是拿到 reconciliation 阶段产出的 EffectList,即所有的更新工作,提交这些 更新工作并调用渲染模块(react-dom)渲染 UI。
1、commitBeforeMutationLifecycles
此函数主要保存当前 DOM 的一个快照,执行 getSnapshotBeforeUpdate
生命周期函数
2、commitAllHostEffects 提交所有更新并渲染
function commitAllHostEffects() {
while (nextEffect !== null) {
recordEffect();
const effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every
// possible bitmap value, we remove the secondary effects from the
// effect tag and switch on that value.
let primaryEffectTag = effectTag & (Placement | Update | Deletion);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted
// does and isMounted is deprecated anyway so we should be able
// to kill this.
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
nextEffect.effectTag &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
commitDeletion(nextEffect);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
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
此函数主要是遍历 EffectList,根据 effectTag,调用对应的 commit 方法,进而调动 react-dom 提供的原生操作 DOM 方法,渲染 UI,操作 DOM 方法有
{
getPublicInstance,
supportsMutation,
supportsPersistence,
commitMount,
commitUpdate,
resetTextContent,
commitTextUpdate,
appendChild,
appendChildToContainer,
insertBefore,
insertInContainerBefore,
removeChild,
removeChildFromContainer,
replaceContainerChildren,
createContainerChildSet,
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在 commit 删除操作时,会执行 componentWillUnmount
生命周期函数
3、commitAllLifeCycles
以 ClassComponent 为例,生成 UI 渲染之后,会执行后续的生命周期函数。
1)、首次渲染则执行 componentDidMount
生命周期函数。
2)、否则,执行 componentDidUpdate
生命周期函数。
以上3步就是 commit 阶段的全过程。
# 总结
总的下来介绍完了 Fiber 的整体架构,不得不说一下的就是看 React 源码真的很有难度,调用栈真的很深,需要多次打断点去了解函数的作用。暂时先写到这里,之后有新的体会再更新这篇文章。
# 参考文档
1、React Fiber Architecture
2、完全理解 React Fiber
3、深入剖析 React Concurrent
4、Deep In React 之浅谈 React Fiber 架构(一)
5、这可能是最通俗的 React Fiber(时间分片) 打开方式
6、React Scheduler 源码详解(1)
7、React Scheduler 源码详解(2)
8、Scheduling in React