react源码debugger-17,18版本的事件更新流程
Posted coderlin_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react源码debugger-17,18版本的事件更新流程相关的知识,希望对你有一定的参考价值。
17版本的批量更新
首先,在react的事件系统中,触发的更新,executionContext会置为EventContext,而通过ReactDOM.batchedUpdates执行的函数,executionContext会被置为BatchContext。其他状态下的executionContext为NoContext。
我们以类组件的更新为例子
如一下demo
this.state = number: 1
onClick = () =>
this.setState(number: this.state.number+1)
console.log(this.state.number);
this.setState(number: this.state.number+1)
console.log(this.state.number);
打印出来的结果应该都是1,而更新后的this.state.number变为了2。为什么打印的是1,结果是2呢?因为react内部有批量更新的处理。
上面说过,在react事件系统中,触发的更新executionContext会置为EventContext。然后setState会创建update,调用ScheduleUpdateOnFiber。
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane, //优先级
eventTime: number
): FiberRoot | null
/**
* 判断是否hi无限循环的update,如果是就报错。
* 比如在componentWillUpdate或者componentDidupdate生命周期中重复调用setState方法,就会发生这种情况。
* react限制了嵌套更新的数量防止无限制更新,限制的嵌套数量是50
*/
checkForNestedUpdates();
//----------------- 1阶段,找到rootFiber
// 递归找到rootFiber, 并且逐步根据当前的优先级,把当前fiber到RootFiber的父级链表上所有的优先级都更新了。
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null)
return null;
//-----------2阶段 创建任务注册到react事件系统中
ensureIsRootSchedule(root)
// ----------3 阶段,对于不可控的更新,react会立即执行
// 开始调度, 从rootFiber开始,是通过 childLanes 逐渐向下调和找到需要更新的组件的。
// react18以前,同步状态下
//非可控任务:如果在延时器(timer)队列或者是微任务队列(microtask),
//那么这种更新任务,React 是无法控制执行时机的,所以说这种任务就是非可控的任务。
// 比如 setTimeout 和 promise 里面的更新任务,那么 executionContext === NoContext ,
// 接下来会执行一次 flushSyncCallbackQueue 。
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode && //非concurrent模式
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
)
/* 进入调度,把任务放入调度中 */
// 当前的执行任务类型为 NoContext ,说明当前任务是非可控的,那么会调用 flushSyncCallbackQueue 方法。
resetRenderTimer();
// 只会在同步状态下执行
flushSyncCallbacksOnlyInLegacyMode();
return root
主要有三个阶段:
- 1阶段,找到rootFiber
- 2阶段 创建任务注册到react事件系统中
- 3 阶段,对于不可控的更新,react会立即执行
可以看到不可控的更新的判断,executionContext===NoConText,什么情况下会这样呢?在17的时候,就是异步的更新,比如微任务或者宏任务等。因为微任务宏仁无的代码不是在这一帧执行的,而executionContext的赋值操作大概是这样的。
const preExecutionContext = executionContext
executionContext = EventContext
fn() //函数执行,比如setState这些
executionContext = preExecutionContext //重新赋值
fn里面的js代码是异步的,导致执行的时候executionContext已经变成了NoContext了。
然后看看第二阶段ensureIsRootSchedule做的事情。
function ensureIsRootScheduled(root, currentTIme)
// --------- 1 阶段,判断当前调度的任务与即将调度任务的优先级,从而决定执行更高优先级的任务
//正在工作的任务
const existingCallbackNode = root.callbackNode;
//此次调度的任务的优先级
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
// 如果此次调度的任务优先级是NoLanes,不需要调度,直接刷新全局变量,并且取消当前的工作的任务
if (nextLanes === NoLanes)
if (existingCallbackNode !== null)
// 如果有存在的任务,则取消
cancelCallback(existingCallbackNode);
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
// 获取此次任务的Priority /* 计算一下执行更新的优先级 */
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// 获取当前正在执行的任务的优先级 /* 当前 root 上存在的更新优先级 */
const existingCallbackPriority = root.callbackPriority;
if(newCallbackPriority === existingCallbackPriority)
// 重点,批量更新的处理
return;
// --------- 2阶段注册任务调度 需要调度的任务优先级更高
if (existingCallbackNode != null)
cancelCallback(existingCallbackNode);
// 注册任务调度
if (newCallbackPriority === SyncLane)
// 注册同步任务,这是react自己模拟的调度任务,本质就是将任务存入一个数组之中
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
//注册flushSyncCallbacks,这个才是SCheduler模块提供的调度函数,flushSyncCallbacks会以立即优先级的形式调度。
// flushSyncCallbacks会执行刚才通过scheduleSyncCallback注册的任务数组
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
else
// 异步调度,scheduleCallback的返回值就是当前注册的任务newTask
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
/* 给当前 root 的更新优先级,绑定到最新的优先级 */
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
主要也可以看作两点
- 1 阶段,判断当前调度的任务与即将调度任务的优先级,从而决定执行更高优先级的任务
- 2阶段注册任务调度 需要调度的任务优先级更高
对于第一个setState,因为当前并没有正在调度的任务,而且17版本的任务都是同步的,所以会走到第二阶段,调用scheduleSyncCallback和scheduleCallback注册任务。
而对于第二个setState,他们的优先级是一样的,所以在第一阶段判断那里就直接return了,但是setState创建的update已经赋值到了fiber身上了。
这就是批量更新的原理,相同优先级的更新只有第一个会开启调度。
接着看如何注册任务的,主要是scheduleSyncCallback和scheduleCallback
export function scheduleSyncCallback(callback: SchedulerCallback)
if (syncQueue === null)
/* 如果队列为空 */
syncQueue = [callback];
else
/* 如果任务队列不为空,那么将任务放入队列中。 */
syncQueue.push(callback);
- scheduleSyncCallback是react自己维护的一个同步任务数组,然后将所有同步任务放进去。
接着又通过scheduleCallback以最高优先级注册了flushSyncCallbacks函数。scheduleCallback是Schedule模块提供的调度函数,可以注册任务,他会通过postMessage调用宏任务执行flushSyncCallbacks函数,而flushSyncCallbacks就会消费刚才react自己维护的同步任务的数组,消费任务。
自此,ensureIsScheduled结束。
然后到scheduleUpdateOnfIber的第三阶段
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode && //非concurrent模式
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
)
/* 进入调度,把任务放入调度中 */
// 当前的执行任务类型为 NoContext ,说明当前任务是非可控的,那么会调用 flushSyncCallbackQueue 方法。
resetRenderTimer();
// 只会在同步状态下执行
flushSyncCallbacksOnlyInLegacyMode();
如果此时是异步更新的话那么executionContext会等于NoContext,会执行flushSyncCallbacksOnlyInLegacyMode函数,而这个函数最终会调用flushSyncCallbacks,消费react的同步任务数组,注意,到这里为止,还属于同步的状态,也就是说,如果是异步的更新。
- 比如第一个setState,此时刚刚注册任务到同步数组里面去,到这里就会被消费了。
我们以一个例子来说明
异步任务
this.state = number: 1
setTimeout(()=>
this.setState(number: this.state.number+1)
console.log(this.state.number);
this.setState(number: this.state.number+1)
console.log(this.state.number);
此时打印出来的结果会是2,3,可以看到并没有被合并。原因就是因为:
- react17的时候,对于不可控的状态,executionContext === NoContext,this.setState创建update,调用ScheduleUpdateOnFiber之后,会调用ensureRootIsSchedule进行任务注册,对于17,所有任务都是同步的,react内部调用ScheduleSyncCallback,将performSyncWorkOnRoot存放同步数组,然后调用ScheduleCallback调度flushSyncCallbacks,然后继续往下执行。flushSyncCallback会执行同步数组里面所有的任务。
- 这里注意,当ensureRootIsSchedule调用完毕之后,因为executionContext === NoContext,所以这里还会执行flushSyncCallbacksOnlyInLegacyMode函数,他的任务也是跟flushSyncCallback,执行完同步数组里面所有的任务。所以这时候他会从同步数组里面取出任务直接执行,直接进入到第一个this.setState触发的更新的调度,到这里,都是同步的代码,fiber会经过render,commit阶段将this.state.number修改为了2。
- 等到第一个setState调用完毕,此时已经经过一轮调度了,所以这时候打印this.state.number就已经是最新的值了。然后再调用this.setState继续创建update,会重复上述的流程。
- 这就是异步任务打破批量更新的原因,每一个setState执行完后,已经经过一轮更新了,所以this.state.number已经被赋予新值了。
同步任务
而对于同步的任务,按照我们上述的说法。
- 每一个setState会创建update,调用shceduleUpdateOnfiber,然后调用ensureRootIsScheduled,而多个setState,只有第一个才会注册任务给同步数组,其他的因为优先级相同,不需要再注册任务。注册任务通过ScheduleSyncClallback将任务存入同步数组,通过scheduleCallback创建宏任务消费同步任务数组里面的任务。ensureRootIsScheduled执行完毕
- 同步状态下,shceduleUpdateOnFiber执行完ensureRootIsScheduled之后,不会调用flushSyncCallbacksOnlyInLegacyMode,因为此时executionContext不为NoContext。
- 那么就会继续执行下一个setState,此时会走上面的逻辑。
- 最后在合适的时机,scheduleCallback调用注册的flushSyncCallbacks,将同步任务数组的任务取出来执行,进入workLoop阶段,消费update,更新props,更新dom等,这就是同步批量更新的原理。
以一个demo来看
this.state = number: 1
setTimeout(()=>
ReactDOM.batchUpdates(()=>
this.setState(number: this.state.number+1)
console.log(this.state.number);
this.setState(number: this.state.number+1)
console.log(this.state.number);
)
ReactDOM.bacthUpdates是react提供的批量更新的方法,就是为了解决异步不能批量更新的处理。
export function batchedUpdates<A, R>(fn: (A) => R, a: A): R
const prevExecutionContext = executionContext;
executionContext |= BatchedContext; //将context置为BatchedContext
try
// 执行传入的事件
return fn(a);
finally
/* 重置状态 */
executionContext = prevExecutionContext;
if (
executionContext === NoContext &&
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
)
/* 批量更新流程,没有更新状态下,那么直接执行任务 */
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
可以看到处理也是类似的,通过executionContext置为BatchedContext,然后再执行函数,这样多个setState就会批量更新了。
- executionContext变为BatchContext,然后开启更新,此时到了ScheduleSyncCallback存入任务之后,scheduleUpdateOnFiber不会再继续执行flushSyncCallbacksOnlyInLegacyMode函数,那么就不会进入调和阶段。
- 然后直接打印,这时候的this.state.number还没变,值还是1,继续调用this.setState创建下一个任务,此时发现当前的优先级跟正在调度额优先级一样,所以不会执行ScheduleSyncCallback存入任务,而是直接return,然后最后取出任务执行的时候,创建的两个Update已经在fiber上了。达到批量更新的目的。
- 最后执行完事件之后,,如果浏览器没有调度更新任务,那么会立即执行flushSyncCallbacksOnlyInLegacyMode消费同步任务,而不用等到宏任务来执行
看图:同步更新的原理
这就是react17同步模式下的更新流程。讲述了为什么批量更新,就是通过executionContext来判断是否是批量更新的状态。
18版本的批量更新。
上面说过,17版本的更新大概是这样的。
function batchedEventUpdates()
var prevExecutionContext = executionContext;
executionContext |= EventContext; // 运算赋值
try
return fn(a); // 执行函数
finally
executionContext = prevExecutionContext; // 重置之前的状态
if (executionContext === NoContext)
flushSyncCallbacksOnlyInLegacyMode() // 同步执行更新队列中的任务,只有legacy才会执行了
这是18alpha 版本的代码,这里依然有同步异步之分,因为如果fn里面是这样的
setTimeout(()=>
this.setState(number: this.state.number+1)
console.log(this.state.number);
this.setState(number: this.state.number+1)
console.log(this.state.number);
那么还是会打破批量更新,因为setTimeout执行之前,executionContext又变成了NoContext了。
最新版本的有什么区别呢?
首先executionContext没有EventContext之说了,他的值只有四个
export const NoContext = /* */ 0b000;
const BatchedContext = /* */ 0b001;
const RenderContext = /* */ 0b010;
const CommitContext = /* */ 0b100;
RenderContext表示即将进入render阶段,它只会在renderRootConcurrent被赋值
function renderRootConcurrent// --将Context切换成RenderContext,表示进入了render阶段
executionContext |= RenderContext;
do
try
// 真正执行任务
workLoopConcurrent();
break;
catch (thrownValue)
handleError(root, thrownValue);
while (true);
// 执行完workLoopConcurrent,需要切换状态
executionContext = prevExecutionContext; // 重新赋值上一次的状态
而commitContext顾名思义就是表示进入commit阶段了。
可以看到,还保留了BatchedContext,批量更新,这可能是兼容17版本ReactDOM.batchedUpdates,因为只有通过ReactDOM.render开启的调度,才会进入立即执行同步函数。
从scheduleUpdateOnFiber的第三个阶段可以看出
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode && //非concurrent模式
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
)
/* 进入调度,把任务放入调度中 */
// 当前的执行任务类型为 NoContext ,说明当前任务是非可控的,那么会调用 flushSyncCallbackQueue 方法。
resetRenderTimer();
// 只会在同步状态下执行
flushSyncCallbacksOnlyInLegacyMode();
只有属于NoContext并且非Concurrent模式,我们可以看到调用的函数是flushSyncCallbacksOnlyInLegacyMode,从名字就可以看出来了,只有legacy模式才会调用。
export function flushSyncCallbacksOnlyInLegacyMode()
if (includesLegacySyncCallbacks)
// 只有通过legacy模式才会执行
flushSyncCallbacks();
可以看到,18版本已经丢弃了这种方式。那么18版本有什么区别呢?
首先看事件函数
export function batchedUpdates(fn, a, b)
if (isInsideEventHandler)
return fn(a, b);
isInsideEventHandler = true;
try
return batchedUpdatesImpl(fn, a, b);
finally
isInsideEventHandler = false;
finishEventHandler();
// batchedUpdatesImpl函数,他也是ReactDOM.batchedUpdates函数
export function batchedUpdatesImpl<A, R>(fn: (A) => R, a: A): R
const prevExecutionContext = executionContext;
executionContext |= BatchedContext; //将context置为BatchedContext
try
// 执行传入的事件
return fn(a);
finally
/* 重置状态 */
executionContext = prevExecutionContext;
// If there were legacy sync updates, flush them at the end of the outer
// most batchedUpdates-like method.
if (
executionContext === NoContext &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
)
/* 批量更新流程,没有更新状态下,那么直接执行任务 */
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
`可以看到,事件一开始执行还是会修改Context为BatchedContext,然后执行函数,唯一的不同就是,flushSyncCallbacksOnlyInLegacyMode现在只在legacy模式才会消费同步数组任务,而18版本的createRoot不再消费这些同步数组任务了。
以一个demo来说
this.state = number: 1
const onCLikc = () =>
setTimeout(()=>
this.setState(number: this.state.number+1)
console.log(this.state.number);
this.setState(number: this.state.number+1)
console.log(this.state.number);
还是这串代码
- 事件触发,executionContext变为BatchedContext,setTImeout执行,放入宏任务。
- 事件执行结束,executionContext变为上一次的Context,因为是concurrent模式,不会执行flushSyncCallbacks去消费同步任务数组。
- 宏任务执行,执行setState,创建update,调用scheduleUpdateOnFiber,调用ensureRootIsSchedule,此时任务不一定是同步的了。对于异步的任务
function ensureIsRootScheduled(root, currentTIme)
// --------- 1 阶段,判断当前调度的任务与即将调度任务的优先级,从而决定执行更高优先级的任务
// .....判断任务优先级,决定是否执行更高优先级任务
if(newCallbackPriority === existingCallbackPriority)
// 重点,批量更新的处理
return;
// --------- 2阶段注册任务调度 需要调度的任务优先级更高
if (existingCallbackNode != null)
cancelCallback(existingCallbackNode);
// 注册任务调度
if (newCallbackPriority === SyncLane)
// 注册同步任务,这是react自己模拟的调度任务,本质就是将任务存入一个数组之中
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
//注册flushSyncCallbacks,这个才是SCheduler模块提供的调度函数,flushSyncCallbacks会以立即优先级的形式调度。
// flushSyncCallbacks会执行刚才通过scheduleSyncCallback注册的任务数组
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
else
// 异步调度,scheduleCallback的返回值就是当前注册的任务newTask
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
/* 给当前 root 的更新优先级,绑定到最新的优先级 */
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
- 可以看到,对于异步任务,react会通过scheduleCallback注册performConcurrentWorkOnRoot函数,返回值是当前的任务,然后赋值到root上面去。ensureRootIsSchedule执行完毕。
- 到了scheduleUpdateOnFiber的第三阶段,不满足lagecy模式,直接退出。
- 然后第二个setState执行,一样的逻辑,到了ensureRootIsSchedule,发现优先级一样,退出调度。
- scheduleCallback在合适的时机执行performConcurrentWorkOnRoot函数,开始render-commit阶段。修改状态。
- 这就是18版本批量更新的原则。取消了NoContext条件下同步处理 同步任务数组。
那么对于18版本下优先级很高的任务,可能会等于SyncLane,那么会正常走scheduleSyncCallback,将任务放入react维护的同步数组,但是不同的是
if (newCallbackPriority === SyncLane)
// 同步状态下
// -----第一阶段,将任务存入同步数组
if (root.tag === LegacyRoot)
// 如果是legacy Root模式
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
else
// 注册同步任务,这是react自己模拟的调度任务,本质就是将任务存入一个数组之中
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
// 判断是否支持queueMicrotask创建微任务,支持的话scheduleMicrotask来调度任务,不用scheduleCallback
// ----第二阶段,调度异步任务执行同步任务数组的任务
if (supportsMicrotasks)
scheduleMicrotask(() =>
if (executionContext === NoContext)
flushSyncCallbacks();
);
else
else
// 注册flushSyncCallbacks,这个才是SCheduler模块提供的调度函数,flushSyncCallbacks会以立即优先级的形式调度。
// flushSyncCallbacks会执行刚才通过scheduleSyncCallback注册的任务数组
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
第一阶段
如果是leagcy开启的,那么会调用scheduleLegacySyncCallback函数注册任务,如果不是调用scheduleSyncCallback,
他们的区别就是
export function scheduleLegacySyncCallback(callback: SchedulerCallback)
includesLegacySyncCallbacks = true; //标识这是由ReactDOM.render调用开启的,这样批量更新才能被打破。
scheduleSyncCallback(callback);
scheduleLegacySyncCallback会开启一个开关,再执行scheduleSyncCallback。因为开启开关之后,批量更新就可以被打断了,如果是leagcy模式的话。
如果不是的话,那么直接调用scheduleSyncCallback将任务存入数组。
为什么有这个开关呢,其实对应这个
export function flushSyncCallbacksOnlyInLegacyMode()
if (includesLegacySyncCallbacks)
// 只有通过legacy模式才会执行
flushSyncCallbacks();
flushSyncCallbacks用来直接消费同步任务数组的任务,只有legacy模式才会开启。
第二阶段
- 判断是否支持微任务,支持的话调用scheduleMicrotask,本质就是queueMicortask,也就是Promise.resolve去调用,微
以上是关于react源码debugger-17,18版本的事件更新流程的主要内容,如果未能解决你的问题,请参考以下文章