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模式才会开启。

第二阶段