react fiber 代码梳理篇

scheduleWork

正如 react fiber 搜罗整理篇 已点明的,fiber reconciler 对首次渲染和再次渲染会采用不同的机制。

首次渲染 —— 比如用户侧在浏览器环境调用 ReactDOM.render(rootReactElement, rootDomElement) —— 时,fiber reconciler 首先会通过 createContainer 将根节点 rootDomElement 转化成 fiberRoot 根节点;然后通过 updateContainer 执行 scheduleWork(fiberRoot, expirationTime) 调度作业,完成渲染。

再次渲染 —— 比如类组件或函数组件状态更新 —— 时,react fiber 类组件状态更新篇react fiber hooks钩子篇 已指出,两者都会执行 scheduleWork(fiber, expirationTime) 调度作业,实现重绘。

可见,两种场景均依赖于 scheduleWork。

executionContext & expirationTime

scheduleWork 内有两个核心要素:executionContext 执行上下文,用于更新方式以及所处阶段;expirationTime 过期时间。

executionContext

react 使用位运算更新或判断 executionContext 值,如使用 executionContext |= RenderContext 更新;使用 executionContext & RenderContext !== NoContext 判断是否处于 render 阶段。fiber reconciler 在每次变更 executionContext 时都会保存先前值,等操作完成后再将 executionContext 置回先前值(本文中贴示代码均省略这一过程)。介于 executionContext 初始值为 NoContext,使用 executionContext === NoContext 可判断调用栈是否回到底部。以下是 executionContext 状态值的基础枚举:

  • 0b000000 NoContext:空状态,作为初始值,可用于推断调用栈是否回到底部、或者推断 executionContext 处于那种状态
  • 0b000001 BatchedContext:批量执行更新任务,如调用 batchedUpdates 更新时。
  • 0b000010 EventContext:执行更新任务,如调用 batchedEventUpdates 更新时。
  • 0b000100 DiscreteEventContext:以 UserBlockingPriority 优先级执行任务,如调用 discreteUpdates 更新时。
  • 0b001000 LegacyUnbatchedContext:传统的非批量执行更新任务,如首次挂载时调用 unbatchedUpdates 更新时。
  • 0b010000 RenderContext:当前更新处于 render 阶段
  • 0b100000 CommitContext:当前更新处于 commit 阶段

expirationTime

fiber 的 expirationTime 基于 computeExpirationForFiber 计算。

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
function computeExpirationForFiber(
currentTime: ExpirationTime,
fiber: Fiber,
suspenseConfig: null | SuspenseConfig,
): ExpirationTime {
const mode = fiber.mode;
if ((mode & BlockingMode) === NoMode) {
return Sync;
}

const priorityLevel = getCurrentPriorityLevel();
if ((mode & ConcurrentMode) === NoMode) {
return priorityLevel === ImmediatePriority ? Sync : Batched;
}

if ((executionContext & RenderContext) !== NoContext) {
// Use whatever time we're already rendering
// TODO: Should there be a way to opt out, like with `runWithPriority`?
return renderExpirationTime;
}

let expirationTime;
if (suspenseConfig !== null) {
// Compute an expiration time based on the Suspense timeout.
expirationTime = computeSuspenseExpiration(
currentTime,
suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION,
);
} else {
// Compute an expiration time based on the Scheduler priority.
switch (priorityLevel) {
case ImmediatePriority:
expirationTime = Sync;
break;
case UserBlockingPriority:
// TODO: Rename this to computeUserBlockingExpiration
expirationTime = computeInteractiveExpiration(currentTime);
break;
case NormalPriority:
case LowPriority: // TODO: Handle LowPriority
// TODO: Rename this to... something better.
expirationTime = computeAsyncExpiration(currentTime);
break;
case IdlePriority:
expirationTime = Idle;
break;
default:
invariant(false, 'Expected a valid priority level');
}
}

// If we're in the middle of rendering a tree, do not update at the same
// expiration time that is already rendering.
// TODO: We shouldn't have to do this if the update is on a different root.
// Refactor computeExpirationForFiber + scheduleUpdate so we have access to
// the root when we check for this condition.
if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
// This is a trick to move this update into a separate batch
expirationTime -= 1;
}

return expirationTime;
}
  • 0 NoWork:
  • 1 Never:
  • 2 Idle:
  • 3 ContinuousHydration:
  • 1073741822 Batched:
  • 1073741823 Sync:
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
109
110
111
112
113
114
115
const scheduleWork = scheduleUpdateOnFiber;
function scheduleUpdateOnFiber(
fiber: Fiber,
expirationTime: ExpirationTime,
) {
const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
if (expirationTime === Sync) {
if (
(executionContext & LegacyUnbatchedContext) !== NoContext &&
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// This is a legacy edge case. The initial mount of a ReactDOM.render-ed
// root inside of batchedUpdates should be synchronous, but layout updates
// should be deferred until the end of the batch.
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root);
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
flushSyncCallbackQueue();
}
}
} else {
ensureRootIsScheduled(root);
}
}

function ensureRootIsScheduled(root: FiberRoot) {
const lastExpiredTime = root.lastExpiredTime;
if (lastExpiredTime !== NoWork) {
// Special case: Expired work should flush synchronously.
root.callbackExpirationTime = Sync;
root.callbackPriority = ImmediatePriority;
root.callbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
return;
}

const expirationTime = getNextRootExpirationTimeToWorkOn(root);
const existingCallbackNode = root.callbackNode;// 上一个调度任务
if (expirationTime === NoWork) {
// There's nothing to work on.
if (existingCallbackNode !== null) {
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority = NoPriority;
}
return;
}

const currentTime = requestCurrentTimeForUpdate();
const priorityLevel = inferPriorityFromExpirationTime(
currentTime,
expirationTime,
);

// 对于之前的渲染任务,校验其是否有足够的优先级及过期时间,否则取消
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
const existingCallbackExpirationTime = root.callbackExpirationTime;
if (
// Callback must have the exact same expiration time.
existingCallbackExpirationTime === expirationTime &&
existingCallbackPriority >= priorityLevel
) {
return;
}
cancelCallback(existingCallbackNode);
}

root.callbackExpirationTime = expirationTime;
root.callbackPriority = priorityLevel;
let callbackNode;
// 同步作业,以 ImmediatePriority 优先级调度执行
if (expirationTime === Sync) {
callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
// 禁用定时调度时,以指定的 priorityLevel 优先级调度
} else if (disableSchedulerTimeoutBasedOnReactExpirationTime) {
callbackNode = scheduleCallback(
priorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
// 指定 timeout 延时调度,优先级为 priorityLevel
} else {
callbackNode = scheduleCallback(
priorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
{timeout: expirationTimeToMs(expirationTime) - now()},
);
}

root.callbackNode = callbackNode;
}

// 1. 同步任务添加到 syncQueue 队列中
// 2. 调度 flushSyncCallbackQueue 以 ImmediatePriority 优先级执行
// 3. 若报错,重试调度作业
function scheduleSyncCallback(callback: SchedulerCallback) {
if (syncQueue === null) {
syncQueue = [callback];
// 使用 scheduler 模块透出接口执行调度作业
immediateQueueCallbackNode = Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushSyncCallbackQueueImpl,
);
} else {
syncQueue.push(callback);
}
return fakeCallbackNode;
}

同步模式:

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
function performSyncWorkOnRoot(root) {
// Check if there's expired work on this root. Otherwise, render at Sync.
const lastExpiredTime = root.lastExpiredTime;
const expirationTime = lastExpiredTime !== NoWork ? lastExpiredTime : Sync;

// If we have a work-in-progress fiber, it means there's still work to do
// in this root.
if (workInProgress !== null) {
executionContext |= RenderContext;

do {
try {
workLoopSync();
break;
} catch (thrownValue) {
// 错误边界捕获处理
handleError(root, thrownValue);
}
} while (true);

// 同步作业,此时 workInProgress 必须为 null
if (workInProgress !== null) {
invariant(
false,
'Cannot commit an incomplete root. This error is likely caused by a ' +
'bug in React. Please file an issue.',
);
// render 阶段完成,将 work-in-progress fiber 置为 finished-work fiber
} else {
root.finishedWork = (root.current.alternate: any);
root.finishedExpirationTime = expirationTime;
finishSyncRender(root);
}

// 调度的渲染任务,避免排队中的任务长期阻塞
ensureRootIsScheduled(root);
}
}

// render 阶段
function workLoopSync() {
while (workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress);
}
}

// commit 阶段
function finishSyncRender(root) {
workInProgressRoot = null;
commitRoot(root);
}

异步模式:

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
function performConcurrentWorkOnRoot(root, didTimeout) {
// Since we know we're in a React event, we can clear the current
// event time. The next update will compute a new event time.
currentEventTime = NoWork;

if (didTimeout) {
// The render task took too long to complete. Mark the current time as
// expired to synchronously render all expired work in a single batch.
const currentTime = requestCurrentTimeForUpdate();
markRootExpiredAtTime(root, currentTime);
// This will schedule a synchronous callback.
ensureRootIsScheduled(root);
return null;
}

// Determine the next expiration time to work on, using the fields stored
// on the root.
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
if (expirationTime !== NoWork) {
const originalCallbackNode = root.callbackNode;

// If we have a work-in-progress fiber, it means there's still work to do
// in this root.
if (workInProgress !== null) {
executionContext |= RenderContext;

do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);

if (workInProgress !== null) {
// There's still work left over. Exit without committing.
stopInterruptedWorkLoopTimer();
} else {
// We now have a consistent tree. The next step is either to commit it,
// or, if something suspended, wait to commit it after a timeout.
stopFinishedWorkLoopTimer();

const finishedWork: Fiber = ((root.finishedWork =
root.current.alternate): any);
root.finishedExpirationTime = expirationTime;
finishConcurrentRender(
root,
finishedWork,
workInProgressRootExitStatus,
expirationTime,
);
}

ensureRootIsScheduled(root);
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performConcurrentWorkOnRoot.bind(null, root);
}
}
}
return null;
}

function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}

附录

fiber.tag 类型

ReactWorkTags.js 展示了 fiber 的类型(基于不同的类型会作不同处理):

  • FunctionComponent:函数式组件。React$ElementType 为构造函数。
  • ClassComponent:类组件。React$ElementType 为函数。
  • HostComponent:Host 组件(如 DOM 节点)。React$ElementType 为 string。
  • Fragment:Fragment 组件。React$ElementType 为 REACT_FRAGMENT_TYPE。
  • Mode:React$ElementType 为 REACT_CONCURRENT_MODE_TYPE 或 REACT_STRICT_MODE_TYPE。
  • Profiler:React$ElementType 为 REACT_PROFILER_TYPE。
  • SuspenseComponent:React$ElementType 为 REACT_SUSPENSE_TYPE。
  • SuspenseListComponent:React$ElementType 为 REACT_SUSPENSE_LIST_TYPE。
  • IndeterminateComponent:
  • HostRoot:
  • HostPortal:
  • HostText:
  • ContextConsumer:Context.Consumer 组件。React$ElementType 为对象,React$ElementType$$typeof 为 REACT_CONTEXT_TYPE。
  • ContextProvider:Context.Provider 组件。React$ElementType 为对象,React$ElementType$$typeof 为 REACT_PROVIDER_TYPE。
  • ForwardRef:React$ElementType 为对象,React$ElementType$$typeof 为 REACT_FORWARD_REF_TYPE。
  • MemoComponent:React$ElementType 为对象,React$ElementType$$typeof 为 REACT_MEMO_TYPE。
  • LazyComponent:React$ElementType 为对象,React$ElementType$$typeof 为 REACT_LAZY_TYPE。
  • FundamentalComponent:React$ElementType 为对象,React$ElementType$$typeof 为 REACT_FUNDAMENTAL_TYPE。
  • ScopeComponent:React$ElementType 为对象,React$ElementType$$typeof 为 REACT_SCOPE_TYPE。
  • SimpleMemoComponent:
  • IncompleteClassComponent:
  • DehydratedFragment:
  • Chunk:

参考

React源码解析之ExpirationTime