十日谈 :React的更新
欢迎阅读我的React源码学习笔记
ExpirationTime的计算
在上一篇文章中反复提及到的ExpirationTime
在很多函数中都被作为参数在传递,说明他在React的更新中有着举足轻重的地位,这一次用这篇文章彻底搞明白这个值得计算方式,和他的具体作用。
ExpirationTime意义
我们先来看看ExpirationTime
被哪些函数作为过参数传递:
首先我们看源码可以得知,ExpirationTime
的引入位置
import type {ExpirationTime} from './ReactFiberExpirationTime';
Update
对象有使用ExpirationTime
createUpdate
将ExpirationTime
作为参数传递processUpdateQueue
中传递参数renderExpirationTime
,但是renderExpirationTime
也是ExpirationTime
类型的参数
const UNIT_SIZE = 10
const MAGIC_NUMBER_OFFSET = 2
export function msToExpirationTime(ms: number): ExpirationTime {
return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET
}
export function expirationTimeToMs(expirationTime: ExpirationTime): number {
return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE
}
function ceiling(num: number, precision: number): number {
return (((num / precision) | 0) + 1) * precision
}
function computeExpirationBucket(
currentTime,
expirationInMs,
bucketSizeMs,
): ExpirationTime {
return (
MAGIC_NUMBER_OFFSET +
ceiling(
currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
)
}
export const LOW_PRIORITY_EXPIRATION = 5000
export const LOW_PRIORITY_BATCH_SIZE = 250
export function computeAsyncExpiration(
currentTime: ExpirationTime,
): ExpirationTime {
return computeExpirationBucket(
currentTime,
LOW_PRIORITY_EXPIRATION,
LOW_PRIORITY_BATCH_SIZE,
)
}
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150
export const HIGH_PRIORITY_BATCH_SIZE = 100
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
return computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
)
}
React 中有两种类型的ExpirationTime
,一个是Interactive
的,另一种是普通的异步。Interactive
的比如说是由事件触发的,那么他的响应优先级会比较高因为涉及到交互。
在整个计算公式中只有currentTime
是变量,也就是当前的时间戳。我们拿computeAsyncExpiration
举例,在computeExpirationBucket
中接收的就是currentTime、5000和250
最终的公式就是:((((currentTime - 2 + 5000 / 10) / 25) | 0) + 1) * 25
其中25是250 / 10,| 0
的作用是取整数
翻译一下就是:当前时间加上498然后处以25取整再加1再乘以 5,需要注意的是这里的currentTime
是经过msToExpirationTime
处理的,也就是((now / 10) | 0) + 2
,所以这里的减去2可以无视,而除以 10 取整应该是要抹平 10 毫秒内的误差,当然最终要用来计算时间差的时候会调用expirationTimeToMs
恢复回去,但是被取整去掉的 10 毫秒误差肯定是回不去的。
现在应该很明白了吧?再解释一下吧:简单来说在这里,最终结果是以25为单位向上增加的,比如说我们输入10002 - 10026之间,最终得到的结果都是10525,但是到了10027的到的结果就是10550,这就是除以25取整的效果。
另外一个要提的就是msToExpirationTime
和expirationTimeToMs
方法,他们是想换转换的关系。有一点非常重要,那就是用来计算expirationTime
的currentTime
是通过msToExpirationTime(now)
得到的,也就是预先处理过的,先处以10再加了2,所以后面计算expirationTime
要减去2也就不奇怪了
React这么设计抹相当于抹平了25ms内计算过期时间的误差,那他为什么要这么做呢?我思考了很久都没有得到答案,直到有一天我盯着代码发呆,看到
LOW_PRIORITY_BATCH_SIZE
这个字样,bacth
,是不是就对应batchedUpdates
?再细想了一下,这么做也许是为了让非常相近的两次更新得到相同的expirationTime
,然后在一次更新中完成,相当于一个自动的batchedUpdates
。
msToExpirationTime 函数是用一个固定值减去当前 ms 值除以10取整。expirationTimeToMs 是将expirationTime还原为 ms 值。 这里为什么要除以10呢,原因是可以抹平10ms以内的误差,前后很近的两次更新,计算出的 expirationTime 是一样的,有利于批量更新。
msToExpirationTime
函数是用一个固定值减去当前 ms
值除以25取整。expirationTimeToMs
是将expirationTime
还原为 ms
值。 这里为什么要除以25呢,原因是可以抹平25ms以内的误差,前后很近的两次更新,计算出的 expirationTime
是一样的,有利于批量更新。
根据这个规则,优先级越高的任务,计算出的 expirationTime
越大(这和之前版本是完全反着的)。Sync
是最大的, 它等以MAX_SIGNED_31_BIT_INT
,是0b111111111111111111111111111111
。
updateContainer
每当在生成一个新的Fiber节点后,都需要更新这个Fibre节点的一些字段,调用 updateContainer 方法。
// react-reconciler\src\ReactFiberReconciler.js
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): ExpirationTime {
const current = container.current;
// 当前时间的一个变换
const currentTime = requestCurrentTime();
// 忽略,返回null
const suspenseConfig = requestCurrentSuspenseConfig();
// 计算过期时间
const expirationTime = computeExpirationForFiber(
currentTime,
current,
suspenseConfig,
);
return updateContainerAtExpirationTime(
element,
container,
parentComponent,
expirationTime,
suspenseConfig,
callback,
);
}
首先看下 requestCurrentTime 方法,我将逐句注释。
// react-reconciler\src\ReactFiberReconciler.js
export function requestCurrentTime() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
//首先需要看一个全局变量, 那就是 executionContext[1]。
// We're inside React, so it's fine to read the actual time.
return msToExpirationTime(now());
}
// We're not inside React, so we may be in the middle of a browser event.
if (currentEventTime !== NoWork) {
// Use the same start time for all updates until we enter React again.
return currentEventTime;
}
// This is the first update since React yielded. Compute a new start time.
currentEventTime = msToExpirationTime(now());
return currentEventTime;
}
[1]通过代码段
const NoContext = /* */ 0b000000;
const BatchedContext = /* */ 0b000001;
const EventContext = /* */ 0b000010;
const DiscreteEventContext = /* */ 0b000100;
const LegacyUnbatchedContext = /* */ 0b001000;
const RenderContext = /* */ 0b010000;
const CommitContext = /* */ 0b100000;
let executionContext: ExecutionContext = NoContext;
可以看出, executionContext
是一个二进制的枚举值, 初始值为 NoContext
。
这句话其实是判断当前 executionContext
是否处在 RenderContext
或者是 CommitContext
的阶段。
那么什么时候 executionContext 会是 RenderContext 或者是 CommitContext 的阶段呢。只有在你renderRoot 的 时候,executionContext有可能是这两个值。RenderContext代表着React正在计算更新,CommitContext代表着React正在提交更新。
注意记得|0
的取整操作
小结
为什么抹平25ms以内的误差,前后很近的两次更新,是利于批量更的呢?
因为如果说操作内多次的调用了setstate
,但是两次调用的前后时间间隔可能非常小,但是只要计算出的ExpirationTime
是不一样的就意味着两个更新任务的优先级不同,这将会导致React更新执行了多次,降低了效能。
ExpirationTime的模式
在 React 的调度过程中存在着非常多不同的expirationTime变量帮助 React 去实现在单线程环境中调度不同优先级的任务这个需求。
- NoWork,代表没有更新
- Sync,代表同步执行,不会被调度也不会被打断
- async模式下计算出来的过期时间,一个时间戳
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
let expirationTime;
if (expirationContext !== NoWork) { // expirationContext可以被其他方法设置 ,比如 syncUpdates ,用于强制改变expirationTime
// An explicit expiration context was set;
expirationTime = expirationContext; // 赋值为1,优先级超高 或者赋值为int最大值,优先级超低
} else if (isWorking) {
if (isCommitting) {
// Updates that occur during the commit phase should have sync priority
// by default.
expirationTime = Sync;
} else {
// Updates during the render phase should expire at the same time as
// the work that is being rendered.
expirationTime = nextRenderExpirationTime;
}
} else {
// No explicit expiration context was set, and we're not currently
// performing work. Calculate a new expiration time.
if (fiber.mode & ConcurrentMode) {
if (isBatchingInteractiveUpdates) {
// This is an interactive update
expirationTime = computeInteractiveExpiration(currentTime);
} else {
// This is an async update
expirationTime = computeAsyncExpiration(currentTime);
}
// If we're in the middle of rendering a tree, do not update at the same
// expiration time that is already rendering.
if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
expirationTime += 1;
}
} else {
// This is a sync update
expirationTime = Sync;
}
}
if (isBatchingInteractiveUpdates) {
// This is an interactive update. Keep track of the lowest pending
// interactive expiration time. This allows us to synchronously flush
// all interactive updates when needed.
if (expirationTime > lowestPriorityPendingInteractiveExpirationTime) {
lowestPriorityPendingInteractiveExpirationTime = expirationTime;
}
}
return expirationTime;
}
sync模式的ExpirationTime
sync模式是创建即更新的模式
function syncUpdates<A, B, C0, D, R>(
fn: (A, B, C0, D) => R,[1]
a: A,
b: B,
c: C0,
d: D,
): R {
const previousExpirationContext = expirationContext;
expirationContext = Sync;
try {
return fn(a, b, c, d);
} finally {
expirationContext = previousExpirationContext;
}
}
对expirationContext
进行了赋值
[1]这里的fn就是setstate
function flushSync<A, R>(fn: (a: A) => R, a: A): R {
invariant(
!isRendering,
'flushSync was called from inside a lifecycle method. It cannot be ' +
'called when React is already rendering.',
);
const previousIsBatchingUpdates = isBatchingUpdates;
isBatchingUpdates = true;
try {
return syncUpdates(fn, a);
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
performSyncWork();
}
}
这个API传入的是一个回调,里面调用了setstate
,在执行的同时通过expirationContext = Sync;
语句改变了expirationContext
的模式。这时进入computeExpirationForFiber
方法,第一个条件判断即可得出expirationTime = expirationContext;
赋值为1,优先级超高,立即执行。
expirationContext就是通过外部强制expirationTime 使用哪种模式更新的变量。
isWorking
字段和isCommitting
也是强制执行更新,涉及任务更新,后续再学习。
异步模式的ExpirationTime
有调度,可能会被中断的更新模式
没有外部强制更新的情况下才会进行异步更新,异步更新通过判断fiber.mode & ConcurrentMode
字段
这里涉及一个概念,我们首先看看fiber.mode
和ConcurrentMode
的定义
import {ConcurrentMode, ProfileMode, NoContext} from './ReactTypeOfMode';
export type TypeOfMode = number;
export const NoContext = 0b000;
export const ConcurrentMode = 0b001;
export const StrictMode = 0b010;
export const ProfileMode = 0b100;
这边都是二进制的定义,可以通过与或这种逻辑操作方便的组合和计算。
也就是说只有在处于ConcurrentMode下的fiber才可以进行异步的更新,否则都是同步的更新
附录:react中的数据结构
FiberRoot
type BaseFiberRootProperties = {|
// 页面上挂载的dom节点, 就是render方法接收的第二个参数
containerInfo: any,
// 只有在持久更新中会用到,也就是不支持增量更新的平台,react-dom不会用到
pendingChildren: any,
// 当前树的根节点,就是FiberRoot
current: Fiber,
// 以下的优先级是用来区分
// 1、没有提交(committed)的任务
// 2、没有提交的挂起的任务
// 3、没有提交的可能被挂起的任务
// 我们选择不追踪每个单独的阻塞登记,为了兼顾性能
// The earliest and latest priority levels that are suspended from committing.
// 提交时候被挂起的最老和最新的任务
earliestSuspendedTime: ExpirationTime,
latestSuspendedTime: ExpirationTime,
// The earliest and latest priority levels that are not known to be suspended.
// 在提交时候可能会被挂起的最老和最新的任务(所有任务进来都是这个状态)
earliestPendingTime: ExpirationTime,
latestPendingTime: ExpirationTime,
// The latest priority level that was pinged by a resolved promise and can
// be retried.
// 最新的通过一个promise被resolve并且可以重新尝试的优先级
latestPingedTime: ExpirationTime,
// 如果有错误被抛出并且没有更多的更新存在,我们尝试在处理错误前同步重新从头渲染
// 在`renderRoot`出现无法处理的错误时会被设置为`true`
didError: boolean,
// 正在等待提交的任务的`expirationTime`
pendingCommitExpirationTime: ExpirationTime,
// 已经完成的任务的FiberRoot对象,如果你只有一个Root,那他永远只可能是这个Root对应的Fiber,或者是null
// 在commit阶段只会处理这个值对应的任务
finishedWork: Fiber | null,
// 在任务被挂起的时候通过setTimeouth函数的返回值
// 用来清理下一次如果有新的任务挂起时还没触发的timeout
timeoutHandle: TimeoutHandle | NoTimeout,
// 顶层context对象,只有主动调用`renderSubtreeIntoContainer`时才会有用
context: Object | null,
pendingContext: Object | null,
// 用来确定第一次渲染的时候是否需要融合
+hydrate: boolean,
// 当前root上剩余的过期时间
// TODO: 提到renderer里面区处理
nextExpirationTimeToWorkOn: ExpirationTime,
// 当前更新对应的过期时间
expirationTime: ExpirationTime,
// List of top-level batches. This list indicates whether a commit should be
// deferred. Also contains completion callbacks.
// TODO: Lift this into the renderer
// 顶层批次(批处理任务?)这个变量指明一个commit是否应该被推迟
// 同时包括完成之后的回调
// 貌似用在测试的时候?
firstBatch: Batch | null,
// Linked-list of roots
// next
nextScheduledRoot: FiberRoot | null,
|};
Fiber
// Fiber对应一个组件需要被处理或者已经处理了,一个组件可以有一个或者多个Fiber
type Fiber = {|
// Fiber的tag,用来标记不同的组件类型
tag: WorkTag,
// 就是组件的那个key
key: null | string,
// 我们调用`createElement`的第一个参数 div/p/func/class
elementType: any,
// 异步组件resolved之后返回的内容,一般是`function`或者`class`
type: any,
// 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
stateNode: any,
// 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
return: Fiber | null,
// 单链表树结构
// 指向自己的第一个子节点
child: Fiber | null,
// 指向自己的兄弟结构
// 兄弟节点的return指向同一个父节点
sibling: Fiber | null,
index: number,
// ref
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
// 新的变动带来的新的props,就是nextProps
pendingProps: any,
// 上一次渲染完成之后的propP,就是当前props
memoizedProps: any,
// 该Fiber对应的组件产生的Update会存放在这个队列里面
updateQueue: UpdateQueue<any> | null,
// 上一次渲染的时候的state
memoizedState: any,
// 一个列表,存放这个Fiber依赖的context
firstContextDependency: ContextDependency<mixed> | null,
// 用来描述当前Fiber和他子树的`Bitfield`
// 共存的模式表示这个子树是否默认是异步渲染的
// Fiber被创建的时候他会继承父Fiber
// 其他的标识也可以在创建的时候被设置
// 但是在创建之后不应该再被修改,特别是他的子Fiber创建之前
mode: TypeOfMode,
// Effect
// 用来记录Side Effect
effectTag: SideEffectTag,
// 单链表用来快速查找下一个side effect
nextEffect: Fiber | null,
// 子树中第一个side effect
firstEffect: Fiber | null,
// 子树中最后一个side effect
lastEffect: Fiber | null,
// 代表任务在未来的哪个时间点应该被完成
// 不包括他的子树产生的任务
expirationTime: ExpirationTime,
// 快速确定子树中是否有不在等待的变化
childExpirationTime: ExpirationTime,
// 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber
// 我们称他为`current <==> workInProgress`
// 在渲染完成之后他们会交换位置
alternate: Fiber | null,
// 下面是调试相关的,收集每个Fiber和子树渲染时间的
actualDuration?: number,
// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,
// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,
// Sum of base times for all descedents of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,
// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only
_debugID?: number,
_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
|};