React 源码解析 - React 创建更新回顾和 React 的批量更新
回顾 React 更新
创建更新
ReactDOM.render 初始渲染
每次调用都通过传入的<App />, getElementById('app')
构建 root 节点,每个 rootFiber 都有独立的 updateQueue 和 fiberTree,最后调用 ReactRoot.prototypye.render 来创建更新。setState & forceUpdate 更新渲染
都是 Component 构造函数的原型方法,目的都是给节点的 fiber 对象上创建更新,区别在于更新的类型不同。
创建更新 update,记录当前时间,计算 expirationTime,设置当前更新的 payload,再把 update 推入 fiber 对象的 updateQueue 属性上, 之后进入调度流程。
expirationTime
由 ReactFiberReconciler.js 包,updateContainer 中的 const expirationTime = computeExpirationForFiber(currentTime, current);
计算出
1 | function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) { |
- 简化 computeExpirationForFiber 函数
- 发现 expirationTime 正常情况是 Sync = 1 同步的
- 只有在 fiber.mode 存在并且使用 ConcurrentMode 新版本的异步更新模式时才会真正的计算 expirationTime
- ConcurrentMode 模式下还会根据 isBatchingInteractiveUpdates 全局变量判断当前更新的上下文环境来决定 expirationTime 是高优先级还是低优先级的运算结果。(isBatchingInteractiveUpdates 在 batchedUpdates 中讲解)
scheduleWork 开始调度
核心功能
- 找到更新对应的 FiberRoot 节点
setState 时传入的都是组件的 Fiber 节点而不是 FiberRoot 节点 - 符合条件时 - 重置 stack
具有公共变量,用于调度和更新 - 符合条件时 - 请求工作调度
回顾 FiberTree
FiberTree 属性
1 child 为第一个子节点
2 sibling 为兄弟节点
3 return 为父节点,只有 RootFiber 对象 renturn 为 null
4 FiberRoot.current 和 RootFiber.stateNode 互相引用执行操作时的 Fiber 对象
1 点击组件上的元素
2 执行组件的原型方法调用 setState
3 把 RootFiber 加入到调度中
scheduleWork 进入调度队列
每一次进入调度队列的只有 FiberRoot 对象, 更新也是从 FiberRoot 对象上开始的。
1 | function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { |
scheduleWorkToRoot 通过 Fiber 对象找到 RootFiber 对象进行调度
- 根据传入的 Fiber 对象向上寻找到 RootFiber 对象
- 同时更新所有子树上面的 expirationTime
1 | function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null { |
resetStack
- 当发现当前任务的优先级大于下一个任务的优先级时,把下个任务的优先级重置执行当前任务
- resetStack 在重置下个任务时,会先记录这个任务,等待以后执行,并且使用 unwindInterruptedWork 来重置这个任务 fiber 上级的状态
1 | if ( |
1 | function resetStack() { |
何时执行 requestWork
isWorking, isCommitting 是 react 渲染的两个不同阶段,
- isWorking
working 包含 committing(不可打断) - isCommitting
fiberTree 的更新已经结束,正在提交也就是更新dom 树的渲染阶段, 不可打断1
2
3
4
5
6
7
8if (
!isWorking || // 没有正在工作
isCommitting || // 或者正在提交,也就是更新dom 树的渲染阶段
nextRoot !== root // 不同的 root 一般不存在不同
) {
const rootExpirationTime = root.expirationTime; // 重新查找 root expirationTime,因为可能会改变
requestWork(root, rootExpirationTime); // 请求工作
}
requestWork
核心功能
- 将 root 节点加入到 root调度队列中
- 判断是否是批量更新
- 最后根据 expirationTime 的类型判断调度的类型
requestWork 流程
1 | function requestWork(root: FiberRoot, expirationTime: ExpirationTime) { |
addRootToSchedule
- 判断当前 root 是否调度过, 单个或多个 root 构建成单向链表结构
- 如果调度过,设置当前任务优先级最高
1 | function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) { |
batchedUpdates 批量更新
- 每次 react 创建更新都会执行 requestWork。如: setState
- 在 requestWork 中决定 react 的更新是异步调度还是同步执行
setState 的调用
1 | import React from 'react' |
requestWork
- 当 setState 创建更新后进入调度,执行到 requestWork 里时会判断一个 isBatchingUpdates 的全局变量。
- 在 requestWork 中断点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
debugger
// ...
if (isRendering) {
return;
}
// 批量处理相关
// 调用 setState 时在 enqueueUpdates 前 batchedUpdates 会把 isBatchingUpdates 设置成 true
if (isBatchingUpdates) {
if (isUnbatchingUpdates) {
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, true);
}
return; // isBatchingUpdates true // 普通的 setState 在进入 enqueueUpdates 时在这里直接不执行了,下面其实没进入调度
}
// 只有异步模式任务时才会执行
} - 在 requestWork 中断点,发现在判断 isBatchingUpdates 变量时就直接返回了,虽然 expirationTime 是 Sync 但是下面的 performSyncWork() 并不会执行。
- setState 时先执行了一个 batchedUpdates 的函数。
- 多次的 setState 在 enqueueUpdates 函数中,fiber 对象的 baseState 仍然是 0, 但是 fiber 对象上的 updateQueue 更新队列上已经记录好了多次 update 对象将要更新 state 的 payload。
batchedUpdates 的源码
- setState 在 batchedUpdates 中先把 isBatchingUpdates 暂存为 previousIsBatchingUpdates, 再设置为 true 防止在 requestWork 中执行。
- 在 try 代码块中执行组件的方法 fn,fn 不论执行多少次 setState 执行完了都会通过 finally 进入把 isBatchingUpdates 再设置回 false。
- 最后通过执行 performSyncWork() 方法,而不是在 requestWork 中调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const previousIsBatchingUpdates = isBatchingUpdates; // 初始为 false
isBatchingUpdates = true;
try {
return fn(a); // 执行组件绑定的方法, 走到 requestWork 里
} finally {
// setState 最终 enqueueUpdates 全部走到 requestWork 后变回 false 再一同 performSyncWork 才真正的执行并改变 state
isBatchingUpdates = previousIsBatchingUpdates; // 变回 false
// 如果是 setTimeout(() => { this.setState }) setTimeout 走到这里后才执行 this.setState 这时上下文环境是 window isBatchingUpdates 已经 false,setState 就是同步的
if (!isBatchingUpdates && !isRendering) {
performSyncWork(); // 当所有 setState 执行完全部enqueueUpdates 后代替 requestWork 来调度
}
}
}
方法二 setTimout 执行方式
1 | setTimeout(() => { |
- setTimeout 等浏览器 API 执行的方式结果都会把三次 setState 结算的结果打印出来,像是一种同步的执行方式
- 再次 debugger ,这时在 batchedUpdates 函数中的 fn 的执行内容只是 setTimeout。
- 当 setTimeout 执行完后直接进入了 finally 代码块中,isBatchingUpdates 变回了 false
- 当 setTimeout 结束执行回调中的 setState 进入 requestWork 时 isBatchingUpdates 已经变为 false,requestWork 将会执行下去,最终执行自己 performSyncWork()
- 三次 setState 都会通过 requestWork 执行 performSyncWork(),而不是之前通过 batchedUpdates 执行一次,所以每次 setState 的 update 都会立刻改变 state,结果也是同步的输出。
方法三 使用 batchedUpdates API
batchedUpdates 让 setState 的更新仍然为批量更新
1 | setTimeout(() => { |
- batchedUpdates API 其实就是 batchedUpdates 函数
- setTimeout 执行回调时 batchedUpdates API 又把 isBatchingUpdates 设置为 true,让 多次的 setState 又能进行批量更新。
1
2
3
4
5
6
7
8
9
10
11
12function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const previousIsBatchingUpdates = isBatchingUpdates; // 初始为 false
isBatchingUpdates = true;
try {
return fn(a); // 执行组件绑定的方法, 走到 requestWork 里
} finally {
isBatchingUpdates = previousIsBatchingUpdates; // 变回 false
if (!isBatchingUpdates && !isRendering) {
performSyncWork(); // 当所有 setState 执行完全部enqueueUpdates 后代替 requestWork 来调度
}
}
}
总结 setState 是同步还是异步
- setState 本身的方法调用时同步的,但是调用 setState 不表示 state 立即更新的,state 的更新是根据我们执行环境的上下文来判断的。
- 如果处于批量更新的情况下 state 就不是立即更新的,如果不处于批量更新情况下有可能立即更新.
- 现在有 asyncMode 异步渲染的情况,state 也不是立即更新的,需要进入异步调度的过程。