背景

react 架构局限性

v16 采用 Stack Reconciliation,即递归的方式处理组件树更新,从根节点开始遍历整棵树,伴随着同步、阻塞,会出现一些问题

  • 渲染时间过长: 复杂度过高的任务导致主线程阻塞,Native GUI 线程中断,无法响应用户指令,影响体验
  • 优先级管理: 无优先级概念,导致关键任务可能被低优先级任务阻塞
  • 中断恢复机制: 一旦开始就无法中断和恢复

fiber 解决方案

  1. 性能提升:
  • 增量渲染: 将整个渲染过程分成多个可中断的、执行时间短的小任务,并使用任务调度器(Scheduler)来动态地调度这些任务,避免长时间的阻塞,提升了渲染的灵活性和效率
  • 任务分片: Time Slicing,fiber 可在空闲时间内执行渲染任务,从而最大限度地利用浏览器的空闲时间
    • 任务队列 (Task Queue): 将所有的更新操作封装成任务,并放入一个任务队列中
    • 执行任务: 执行队列任务,记录当前任务执行时间
    • 时间检查: 若任务执行时间超过阈值 (默认 5 毫秒),则中断执行
    • 让出线程: 使用 MessageChannelpostMessage 让出线程,浏览器执行其他任务
    • 通知执行: 浏览器执行完成后,通过 onmessage 事件继续执行之前中断的任务
    • 循环执行: 重复此过程直到任务队列清空
  1. 用户体验:
  • 分配优先级: 确保高优先级的任务尽快处理
    • 同步任务: 最高优先级的任务,通常用于处理用户交互事件和页面加载过程中的同步操作
    • 异步任务: 中等优先级的任务,包括普通的更新任务和网络请求等异步操作
    • 空闲任务: 最低优先级的任务,通常用于执行一些不紧急的任务,如日志记录或统计信息收集等
    1
    2
    3
    4
    5
    6
    export const NoPriority = 0; // 无优先级任务
    export const ImmediatePriority = 1; // 立即执行任务
    export const UserBlockingPriority = 2; // 用户阻塞任务
    export const NormalPriority = 3; // 正常任务
    export const LowPriority = 4; // 低优先级任务
    export const IdlePriority = 5; // 空闲执行任务
  1. 灵活和扩展:
  • 为未来特性提供基础,例如 Concurrent Mode 和 Suspense
  1. 维护和调试:
  • 改进 React 内部的代码结构,便于调试和维护

引入

浏览器帧

  1. 浏览器对每一帧的执行和渲染的流程分为 7 个阶段
  • 第 1-3 阶段: js 执行阶段,分别为用户事件回调、定时器回调、窗口变更事件回调
  • 第 4 阶段: rAF 阶段,即 window.requestAnimationFrame 回调执行阶段
  • 第 5-6 阶段: 页面渲染阶段,前 3 阶段 js 执行时间过长将阻塞渲染,导致页面卡顿
  • 第 7 阶段: 帧空闲阶段,即 window.requestIdleCallback 回调执行阶段。若前 6 阶段运行时间超过 16.6ms,则该回调不会执行
  1. requestIdleCallback: 回调会传入一个期限,表示浏览器有多少时间供事件执行, 为了不耽误事,最好在这个时间范围内执行完毕。requestAnimationFrame 的回调会在每一帧确认执行, 属于高优先级任务,而 requestIdleCallback 的回调不一定, 属于低优先级任务

  2. 任务优先级

  • Immediate(-1): 任务会同步执行,或者说要马上执行且不能中断
  • UserBlocking(250ms): 任务一般是用户交互的结果,需要即时得到反馈
  • Normal(5s): 不需要立即完成的任务,例如网络请求
  • Low(10s): 任务可以放后,但是最终应该得到执行,例如分析通知
  • Idle(没有超时时间): 一些没有必要做的任务

fiber 结构

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
function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode
) {
// 基本属性
this.tag = tag; // 描述此Fiber的启动模式的值(LegacyRoot = 0; ConcurrentRoot = 1)
this.key = key; // React key
this.elementType = null; // 描述React元素的类型。例如,对于JSX<App />,elementType是App
this.type = null; // 组件类型
this.stateNode = null; // 对于类组件,这是类的实例;对于DOM元素,它是对应的DOM节点。

// Fiber链接
this.return = null; // 指向父Fiber
this.child = null; // 指向第一个子Fiber
this.sibling = null; // 指向其兄弟Fiber
this.index = 0; // 子Fiber中的索引位置

this.ref = null; // 如果组件上有ref属性,则该属性指向它
this.refCleanup = null; // 如果组件上的ref属性在更新中被删除或更改,此字段会用于追踪需要清理的旧ref

// Props & State
this.pendingProps = pendingProps; // 正在等待处理的新props
this.memoizedProps = null; // 上一次渲染时的props
this.updateQueue = null; // 一个队列,包含了该Fiber上的状态更新和副作用
this.memoizedState = null; // 上一次渲染时的state
this.dependencies = null; // 该Fiber订阅的上下文或其他资源的描述

// 工作模式
this.mode = mode; // 描述Fiber工作模式的标志(例如Concurrent模式、Blocking模式等)。

// Effects
this.flags = NoFlags; // 描述该Fiber发生的副作用的标志(十六进制的标识)
this.subtreeFlags = NoFlags; // 描述该Fiber子树中发生的副作用的标志(十六进制的标识)
this.deletions = null; // 在commit阶段要删除的子Fiber数组

this.lanes = NoLanes; // 与React的并发模式有关的调度概念。
this.childLanes = NoLanes; // 与React的并发模式有关的调度概念。

this.alternate = null; // Current Tree和Work-in-progress (WIP) Tree的互相指向对方 tree里的对应单元

// 如果启用了性能分析
if (enableProfilerTimer) {
// ……
}

// 开发模式中
if (__DEV__) {
// ……
}
}

Concurrent Mode

渲染模式,提高性能和体验,利用了 fiber 架构的增量渲染和时间切片技术,能够在多个优先级任务之间动态地调度执行,使得高优先级任务能够优先得到处理。具体有以下实现

batchedUpdates

连续触发多次状态更新会被更智能地合并为单一的更新操作,以避免不必要的渲染

  • 早期版本的 batchedUpdates 无法合并一些脱离当前上下文环境的更新
  • 在 Concurrent Mode 模式下,状态更新的合并不再局限于当前上下文,而是根据更新的优先级来决定是否合并

Suspense

用于在组件树中等待异步加载内容的机制,能够在数据加载完成之前显示占位符或 loading 界面,提高用户体验

Lazy Loading

延迟加载组件或资源,能够提高页面的加载速度和响应速度,减少初次加载时的资源占用和等待时间

useDeferredValue

内部会调用 useState 并触发一次更新,但优先级很低,当前如果有正在进行中的更新,不会受 useDeferredValue 产生的更新影响,能够返回一个延后更新的值

1
const deferredValue = useDeferredValue(value);

优雅降级

用于处理低优先级任务无法立即执行的情况。在任务无法立即执行时,Concurrent Mode 能够自动调整任务的优先级,保证高优先级任务能够得到及时处理,避免页面加载和渲染的阻塞

工作原理

  1. 工作单元: 每个 fiber 节点代表一个单元,所有 fiber 节点共同组成一个 fiber 链表树,精确定位控制节点行为

  2. 链表属性: child、sibling 和 return 字段形成节点关系网

  3. 双缓冲:

    • 两棵树,当前树 (currentFiber),基于此创建的临时树 (workInProgressFiber),WIP 包含了当前更新受影响的顶层节点直至其所有后代节点
    • WIP 在后台进行比较更新,完成后复制添加其他未更新节点,最终替换 currentFiber,成为新的 currentFiber
    • 两棵树可随时进行比较、中断、恢复等操作,提升渲染性能及 UI 稳定
  4. 更新判断: 通过 memoizedProps、pendingProps 和 memoizedState 字段计算该节点上一个状态和即将应用的状态。从而判断是否需要更新,避免不必要的渲染

  5. 副作用收集: flags、subtreeFlags 字段标识 fiber 及其子树中需要执行的副作用,React 会收集并在 commit 阶段一次性执行

工作流程

Reconciliation

构建工作数,并通过协调算法比较新旧 props 确定更新的节点

  • 旧的协调算法: 深度优先遍历 VDOM 树,一旦开始便无法中断
  • 新的协调算法: 增量渲染、时间切片
  • 协调阶段可能被中断、恢复,甚至重做,协调阶段的生命周期钩子可能会被调用多次,例如 componentWillMount 可能会被调用两次,所以 v17 后一些生命周期废除了

beginWork

第一阶段: 创建与标记更新节点 (ReactFiberBeginWork.js)

  1. 判断 Fiber 节点是否要更新
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
// packages/react-reconciler/src/ReactFiberBeginWork.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
if (current !== null) {
// 这是旧节点,需要检查props和context是否有变化再确认是否需要更新节点
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;

if (oldProps !== newProps || hasLegacyContextChanged()) {
didReceiveUpdate = true; // props和context有变化,说明节点有更新
} else {
// 其它特殊情况的判断
}
} else {
didReceiveUpdate = false; // 这是新节点,要创建,而不是更新
}

workInProgress.lanes = NoLanes; // 进入beginWork表示开始新的工作阶段,所以要把旧的workInProgress优先级清除掉

switch (workInProgress.tag) {
// 通过workInProgress的tag属性来确定如何处理当前的Fiber节点
// 每一种tag对应一种不同的Fiber类型,进入不同的调和过程(reconcileChildren())
case IndeterminateComponent: // 尚未确定其类型的组件
// ……
case LazyComponent: // 懒加载组件
// ……
case FunctionComponent: // 函数组件
// ……
case ClassComponent: // 类组件
// ……

// 其它多种Fiber类型
// case ……
}
}
  1. 判断 Fiber 子节点是更新还是复用
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
// packages/react-reconciler/src/ReactFiberBeginWork.js
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any, // 要调和的新的子元素
renderLanes: Lanes
) {
if (current === null) {
// 如果current为空,说明这个Fiber是首次渲染,React会为nextChildren生成一组新的Fiber节点
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
// 当current非空时,React会利用现有的Fiber节点(current.child)和新的子元素(nextChildren)进行调和
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}

// react/packages/react-reconciler/src/ReactChildFiber.js
export const reconcileChildFibers: ChildReconciler =
createChildReconciler(true);
export const mountChildFibers: ChildReconciler = createChildReconciler(false);

completeUnitOfWork

第二阶段: 遍历 Fiber 节点,记录有副作用节点的关系,收集副作用列表 (ReactFiberWorkLoop.js)

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
// packages/react-reconciler/src/ReactFiberWorkLoop.js
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork: Fiber = unitOfWork; // 当前正在完成的工作单元
do {
const current = completedWork.alternate; // 当前Fiber节点在另一棵树上的版本
const returnFiber = completedWork.return; // 当前Fiber节点的父节点

let next;
next = completeWork(current, completedWork, renderLanes); // 调用completeWork函数

if (next !== null) {
// 当前Fiber还有工作要完成
workInProgress = next;
return;
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// 如果有兄弟节点,则进入兄弟节点的工作
workInProgress = siblingFiber;
return;
}
// 如果没有兄弟节点,回到父节点继续
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);

// 如果处理了整个Fiber树,更新workInProgressRootExitStatus为RootCompleted,表示调和已完成
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}

completeWork

第三阶段: 据 tag 进行不同的处理 (ReactFiberCompleteWork.js)
bubbleProperties: 记录 Fiber 的副作用标志,并为子 Fiber 创建链表

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
// packages/react-reconciler/src/ReactFiberCompleteWork.js
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
// 多种tag
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
bubbleProperties(workInProgress);
return null;
case ClassComponent:
// 省略逻辑
// ……
bubbleProperties(workInProgress);
return null;
case HostComponent:
// 省略逻辑
// ……
return null;
// 多种tag
// ……
}
}

function bubbleProperties(completedWork: Fiber) {
const didBailout =
completedWork.alternate !== null &&
completedWork.alternate.child === completedWork.child; // 当前的Fiber与其alternate(备用/上一次的Fiber)有相同的子节点,则跳过更新

let newChildLanes = NoLanes; // 合并后的子Fiber的lanes
let subtreeFlags = NoFlags; // 子树的flags。

if (!didBailout) {
// 没有bailout,需要冒泡子Fiber的属性到父Fiber
let child = completedWork.child;
// 遍历子Fiber,并合并它们的lanes和flags
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
);

subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;

child.return = completedWork; // Fiber的return指向父Fiber,确保整个Fiber树的一致性
child = child.sibling;
}
completedWork.subtreeFlags |= subtreeFlags; // 合并所有flags(副作用)
} else {
// 有bailout,只冒泡那些具有“静态”生命周期的flags
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
);

subtreeFlags |= child.subtreeFlags & StaticMask; // 不同
subtreeFlags |= child.flags & StaticMask; // 不同

child.return = completedWork;
child = child.sibling;
}
completedWork.subtreeFlags |= subtreeFlags;
}
completedWork.childLanes = newChildLanes; // 获取所有子Fiber的lanes。

return didBailout;
}
  1. Fiber 架构计算速度加快

    flags 或 subtreeFlags 是 16 进制的标识,进行按位或(|)运算后,可以记录当前节点本身和子树的副作用类型,通过运算结果可以减少节点的遍历

  2. 调和过程可中断

    Concurrent Mode 的能力使得 React 可以优先处理高优先级的更新

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// packages/react-reconciler/src/ReactFiberWorkLoop.js
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// 保存当前的执行上下文和 dispatcher
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher(root.containerInfo);
const prevCacheDispatcher = pushCacheDispatcher();

if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
// 如果当前的工作进度树与传入的 root 或 lanes 不匹配,我们需要为新的渲染任务准备一个新的堆栈。
// ……
}

// 持续的工作循环,除非中断发生,否则会一直尝试完成渲染工作
outer: do {
try {
if (
workInProgressSuspendedReason !== NotSuspended &&
workInProgress !== null
) {
// 如果当前的工作进度是由于某种原因而被挂起的,并且仍然有工作待处理,那么会处理它
const unitOfWork = workInProgress;
const thrownValue = workInProgressThrownValue;

// 根据不同挂起原因,进行中断、恢复等计算
resumeOrUnwind: switch (workInProgressSuspendedReason) {
case SuspendedOnError: {
// 如果工作因错误被挂起,那么工作会被中断,并从最后一个已知的稳定点继续
// ……省略逻辑
break;
}
case SuspendedOnData: {
// 工作因等待数据(通常是一个异步请求的结果)而被挂起,
// ……省略逻辑
break outer;
}
case SuspendedOnInstance: {
// 将挂起的原因更新为SuspendedOnInstanceAndReadyToContinue并中断工作循环,标记为稍后准备好继续执行
workInProgressSuspendedReason =
SuspendedOnInstanceAndReadyToContinue;
break outer;
}
case SuspendedAndReadyToContinue: {
// 表示之前的挂起工作现在已经准备好继续执行
if (isThenableResolved(thenable)) {
// 如果已解析,这意味着需要的数据现在已经可用
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
replaySuspendedUnitOfWork(unitOfWork); // 恢复执行被挂起的工作
} else {
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
throwAndUnwindWorkLoop(unitOfWork, thrownValue); // 继续循环
}
break;
}
case SuspendedOnInstanceAndReadyToContinue: {
// ……省略部分逻辑
const isReady = preloadInstance(type, props);
if (isReady) {
// 实例已经准备好
workInProgressSuspendedReason = NotSuspended; // 该fiber已完成,不需要再挂起
workInProgressThrownValue = null;
const sibling = hostFiber.sibling;
if (sibling !== null) {
workInProgress = sibling; // 有兄弟节点,开始处理兄弟节点
} else {
// 没有兄弟节点,回到父节点
const returnFiber = hostFiber.return;
if (returnFiber !== null) {
workInProgress = returnFiber;
completeUnitOfWork(returnFiber); // 收集副作用,前面有详细介绍
} else {
workInProgress = null;
}
}
break resumeOrUnwind;
}
}
// 还有其它case
}
}

workLoopConcurrent(); // 如果没有任何工作被挂起,那么就会继续处理工作循环。
break;
} catch (thrownValue) {
handleThrow(root, thrownValue);
}
} while (true);

// 重置了之前保存的执行上下文和dispatcher,确保后续的代码不会受到这个函数的影响
resetContextDependencies();
popDispatcher(prevDispatcher);
popCacheDispatcher(prevCacheDispatcher);
executionContext = prevExecutionContext;

// 检查调和是否已完成
if (workInProgress !== null) {
// 未完成
return RootInProgress; // 返回一个状态值,表示还有未完成
} else {
// 已完成
workInProgressRoot = null; // 重置root
workInProgressRootRenderLanes = NoLanes; // 重置Lane
finishQueueingConcurrentUpdates(); // 处理队列中的并发更新
return workInProgressRootExitStatus; // 返回当前渲染root的最终退出状态
}
}

Commit

通过 commitRoot 和 commitRootImpl 方法,遍历在 Reconciliation 阶段创建的副作用列表进行更新,更新 DOM 并执行副作用。一旦进入提交阶段,需要正确地处理各种副作用,无法中断

BeforeMutation

第一阶段: 遍历副作用列表 (ReactFiberCommitWork.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// packages/react-reconciler/src/ReactFiberCommitWork.js
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber
): boolean {
nextEffect = firstChild; // nextEffect是遍历此链表时的当前fiber
commitBeforeMutationEffects_begin(); // 遍历fiber,处理节点删除和确认节点在before mutation阶段是否有要处理的副作用

const shouldFire = shouldFireAfterActiveInstanceBlur; // 当一个焦点元素被删除或隐藏时,它会被设置为 true
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;

return shouldFire;
}

CommitMutation

第二阶段: 提交更新 (ReactFiberCommitWork.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// packages/react-reconciler/src/ReactFiberCommitWork.js
export function commitMutationEffects(
root: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes
) {
// lanes和root被设置为"in progress"状态,表示它们正在被处理
inProgressLanes = committedLanes;
inProgressRoot = root;

// 递归遍历Fiber,更新副作用节点
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);

// 重置进行中的lanes和root
inProgressLanes = null;
inProgressRoot = null;
}

commitLayout

第二阶段: 处理 layout effects (ReactFiberCommitWork.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// packages/react-reconciler/src/ReactFiberCommitWork.js
export function commitLayoutEffects(
finishedWork: Fiber,
root: FiberRoot,
committedLanes: Lanes
): void {
inProgressLanes = committedLanes;
inProgressRoot = root;

// 创建一个current指向就Fiber树的alternate
const current = finishedWork.alternate;
// 处理那些由useLayoutEffect创建的layout effects
commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);

inProgressLanes = null;
inProgressRoot = null;
}