React源码解析25-schedule工作原理

1.主要功能是时间切片和优先级调度

1.时间切片

时间切片的本质是模拟实现requestIdleCallback

浏览器一帧执行的任务,除去浏览器重排/重绘还有四个是可以用来执行我们的js的

只有requestAnimationFrame 是确定时机在 浏览器重排/重绘 之前执行的

一个task(宏任务) — 队列中全部job(微任务) — requestAnimationFrame — 浏览器重排/重绘 — requestIdleCallback

我们模拟实现的requestIdleCallback也没法在确定时机被调用

所以,退而求其次,Scheduler时间切片功能是通过task(宏任务)实现的。

如果有messagechannel就用messagechannel实现 ,没有就用settimeout实现

所以Scheduler将需要被执行的回调函数作为MessageChannel的回调执行。如果当前宿主环境不支持MessageChannel,则使用setTimeout

所以时间片执行的回调函数在messagechannel执行 否则在settimeout回调执行

所以每个任务在每一帧宏任务里面就执行5ms,超过5ms shouldYieldToHost就会变为false,就会中断workloop,等到下一帧的宏任务执行时间在执行

2.优先级调度

schedule是独立于react包,他的优先级是独立于react的优先级的,那么schedule是怎么和react优先级联系上的呢?

1.优先级调度

因为schedule往外暴露啦一个unstable_runwithpriority方法, 这个函数

unstable_runWithPriority(priorityLevel, eventHandler) 作用 让eventHandler这个函数以priorityLevel优先级执行

schedule有五个优先级

case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
复制代码

commitRoot:以同步的方式执行commitRootImpl这个函数

runWithPriority(
    ImmediateSchedulerPriority,//优先级
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
复制代码

2.优先级意义

优先级对应过期时间的长短,以时间大小排序即优先级排序

var timeout;//!不同优先级不同 优先级越低时间timeout越大
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;//-1
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }

  var expirationTime = startTime + timeout;//!开始时间+timeout = 过期时间
复制代码

暴露scheduleCallback函数 该方法用于以某个优先级注册回调函数。

if (!rootDoesHavePassiveEffects) {
  rootDoesHavePassiveEffects = true;
  scheduleCallback(NormalSchedulerPriority, () => {
    flushPassiveEffects();//useEffect函数执行
    return null;//将flushPassiveEffects以NormalSchedulerPriority注册这个函数 即创建task 加入最小堆中 加入调度 等到执行时机到来
  });
}
复制代码

执行流程

newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
复制代码

以schedulerPriorityLevel优先级注册这个函数 ,当这个函数执行的时候如果中断返回值则依然是这个函数 那么这个函数依然是新任务的callback继续执行 知道结束返回null 才弹出这个task

这就是为什么render阶段可中断恢复

3.react内部lane模型的工作原理

满足三个特性:1.可以表示不同的优先级 2.可以表示批处理 3.可以方便计算

//!不同的赛道 越下的赛道优先级越低 lanes就是批次
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = /*                 */ 0b0000000000000000000000000000010;

export const InputDiscreteHydrationLane: Lane = /*      */ 0b0000000000000000000000000000100;
const InputDiscreteLanes: Lanes = /*                    */ 0b0000000000000000000000000011000;

const InputContinuousHydrationLane: Lane = /*           */ 0b0000000000000000000000000100000;
const InputContinuousLanes: Lanes = /*                  */ 0b0000000000000000000000011000000;

export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000100000000; 
export const DefaultLanes: Lanes = /*                   */ 0b0000000000000000000111000000000;//!这三个赛道有一样的优先级 批的概念

const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000001000000000000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111110000000000000;//!优先级越低的赛道越批处理越多 因为他们容易被打断被留下来

const RetryLanes: Lanes = /*                            */ 0b0000011110000000000000000000000;

export const SomeRetryLane: Lanes = /*                  */ 0b0000010000000000000000000000000;

export const SelectiveHydrationLane: Lane = /*          */ 0b0000100000000000000000000000000;

const NonIdleLanes = /*                                 */ 0b0000111111111111111111111111111;

export const IdleHydrationLane: Lane = /*               */ 0b0001000000000000000000000000000;
const IdleLanes: Lanes = /*                             */ 0b0110000000000000000000000000000;
复制代码

4.异步可中断更新

react内部的lane和schedule结合流程

1.根据模式找到schedule的优先级

2.转成lane的优先级

3.根据lane的优先级找到lanes 然后找到优先级最高的没被占用的lane(赛道)

4.lane赋值给update.lane

5.然后给当前fiber.lanes合并这个lane 然后所有父节点的childlanes合并这个lane

6.将root的pendinglanes(root上需要执行还没执行的lane)加上这个lane

7.获取root的callbacknode属性即root当前正在执行的task的回调函数,如果有那么当前应用正在被执行任务

8.取到root上的pendinglanes,给上面所有的lane加上timestamp(过期时间),timestamp是怎么计算的呢?先获取这个lane对应的优先级,优先级越低 他的timestamp越高,所以所有pendinglanes都有一个过期时间,如果当前时间超过timestamp+starttime(即有任务过期),那么就把这个lane加入到root.expiredLanes里面(root上过期的lane), 这一步解决饿饥问题

9.然后获取root上最高lane对应的任务,包括expiredLanes和pendinglanes,如果有过期任务会先被获取,我们这次执行的任务可能是在后面,这次不会被执行,我们这次拿到最优先级的任务,过期任务优先,记为nextlanes为root下优先级最高的lanes

10.如果callbacknode(即root正在被执行的callback)存在,那么我们判断nextlanes和callbacknode对应的lanes谁的优先级高,如果nextlanes更高那么之前的任务就会被取消

11.把lane的优先级转回为schedule优先级,然后调用schedulecallback传入回调函数为performConcurrentworkOnroot,这和函数如果被时间到达中断啦则会返回这次函数,在schedule内部判断如果返回的是个函数 那么这个函数作为newtask的callback继续执行 这就是可中断恢复

 if (root.callbackNode === originalCallbackNode) {//包过时间过啦和其他高优先级的performConcurrentworkOnroot都会走这 因为函数不变 task的callback是这个,优先级在schedule里面保存啦这个函数的优先级
    // 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;
复制代码

返回函数的情况:继续把这个函数=currentTask.callback这里面保存啦优先级。

 currentTask.callback = continuationCallback;//!继续把这个函数当作新任务
        markTaskYield(currentTask, currentTime);
复制代码

root.callbackNode 是当前root进行任务的回调函数,originalCallbackNode就是performConcurrentworkOnroot,这里就是判断如果新进来的函数不是originalCallbackNode,那么就是更高优先级任务进来啦,那么返回Null,不让performConcurrentworkOnroot作为下一个task,这个任务会pop除去。如果相等则是时间片到了的更新因为函数没变,所以就返回函数让下一个task继续。

12.时间片是根据当前时间-任务开始时间>5ms来判断是否超时?yieldInterval = Math.floor(1000 / fps);

5ms是根据fps来定的

13.commitRoot阶段:root.finishedLanes(是指已经完成工作的lanes)赋值为lanes,合并root的lanes和childlanes即为该root即子树中剩下的lanes记为remaininglanes,将remaininglanes和之前的penginglanes比对,少的就是完成的lanes,我们给他初始化为空方便下次使用,更新penginglanes为remaininglanes

5.batchupdates

之前的版本batchedUpdates

export function batchedUpdates<A, R>(fn: A => R, a: A): R {
  const prevExecutionContext = executionContext;
  executionContext |= BatchedContext;//!这个函数类似runwithpriority 执行fn的时候上下文包括BatchedContext 执行后又重置上下文
  try {
    return fn(a);//当fn里面检测到BatchedContext 就不会立即执行flushSyncCallbackQueue 而是在回调里面执行flushSyncCallbackQueue
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      resetRenderTimer();
      flushSyncCallbackQueue();
    }
  }
}
复制代码

缺点就是:如果onclick是setState异步的,那么就没有BatchedContext这个上下文,那么就还是要执行那么多render次数。

现在版本的batchedUpdates

lane模型解决这个缺点:开启concurrentmode

//举例
 handleClick(){
    setTimeout(() => {
      this.setState({
        number:1
      })
      this.setState({
        number:2
      })
    }, 0);
  }
复制代码

每一次setState都会走获取lane这个函数requestUpdateLane,

这个函数有两个关键地方

if (currentEventWipLanes === NoLanes) {
    currentEventWipLanes = workInProgressRootIncludedLanes;
  }
复制代码

第一次setState的时候currentEventWipLanes,后面的setState时候currentEventWipLanes都有值且都等于workInProgressRootIncludedLanes

 lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
复制代码

这个是根据schedulerLanePriority和currentEventWipLanes获取lane,显然同一个onclick下schedulerLanePriority和currentEventWipLanes都一样,最后取得lane也一样,这个函数就是取去除currentEventWipLanes后的schedulerLanePriority对应lanes的最高优先的lane.

最后有一个判断执行不执行render的判断

 if (existingCallbackNode !== null) {
    var existingCallbackPriority = root.callbackPriority;

    if (existingCallbackPriority === newCallbackPriority) {
      // The priority hasn't changed. We can reuse the existing task. Exit.
      return;
    } 
复制代码

第一次setState,existingCallbackNode是为null的

第二次setState existingCallbackNode不为null,且这两个的lane一样,所以existingCallbackPriority === newCallbackPriority两个任务的优先级也一样,所以后面setState不会将render阶段进行调度,所以只执行第一个setState 实现批处理。

原理:

只有开始进入render阶段后,workInProgressRootIncludedLanes = lanes; workInProgressRootIncludedLanes 才会被赋值为这次render的lanes。所以同一次的workInProgressRootIncludedLanes相同,经过啦render阶段workInProgressRootIncludedLanes才会被赋值为其他值。实现啦一次render阶段的批处理。

所以整体逻辑:

每次进入render阶段,workInProgressRootIncludedLanes 会被赋值为这次的lanes,所以同一个onclick的settimeout函数里面,只有第一个setState进入render阶段的调度,而同步的setState会因为

workInProgressRootIncludedLanes相同(因为都是用上次render阶段workInProgressRootIncludedLanes ,导致lane相同,而不会将render阶段加入调度。所以等这次render执行后workInProgressRootIncludedLanes赋值为这次的lane,下次执行setState就能有不同的workInProgressRootIncludedLanes ,就能进入render的调度。虽然render阶段调度只进入了一次,但是update还是都创建加入队列啦的,所以render只进入一次会依次执行两个update,所以值就是最后的新值。

主要是workInProgressRootIncludedLanes 这个变量。

6.高优先级更新如何插队

低优先级的lane为10去执行schecallback调度render阶段

高优先级(点击事件)获取的lane为8 会判断当前exitcallback是否存在(是否有任务在调度) 发现有则判断两个优先级,如果新的高那么旧的任务就会被取消,即低优先级的被取消。然后以lane=8的优先级调度render阶段 实现插队。

怎么清除低优先级更新已经造成的影响?进入render阶段有一个函数prepareFreshStak:

进入这个函数的条件是本次新的render的lane和已经在执行的lane不一致,即优先级不同的render阶段在执行(高优先级的执行render阶段),就会进入prepareFreshStack,这个函数清除上次的workingfiber树和

重置一些属性,以及workInProgressRootIncludedLanes=新的lanes。主要是清除之前创建好的fiber。重新去创建新的workingfiber去diff。

  }
  workInProgressRoot = root;
  workInProgress = createWorkInProgress(root.current, null);//清除之前的workingfiber树
  workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
  workInProgressRootExitStatus = RootIncomplete;
  workInProgressRootFatalError = null;
  workInProgressRootSkippedLanes = NoLanes;
  workInProgressRootUpdatedLanes = NoLanes;
  workInProgressRootPingedLanes = NoLanes;
复制代码

与第五点区别: 如果lane相同则和batchupdates一样的逻辑 直接return 第二次就不调度

总之主要这个优先级是在render开始阶段以什么优先级去执行render,所有组件的update还是都存在fiber里面的,和render阶段以什么优先级无关,只是说要不要执行这个update,不管执行不执行都存在。

7.suspense的实现

io的瓶颈的解决方案就是suspense,解决异步请求数据的时间问题,cpu的问题就是用异步可中断更新解决。

import React, {
  Suspense,
  useState,
  unstable_useTransition as useTransition
} from "react";

import { wrapPromise } from "./utils";

function fetchTime() {
  return wrapPromise(
    new Promise((resolve) => {
      setTimeout(() => {
        resolve({ time: new Date().toLocaleString() });
      }, 1000);
    })
  );
}

function Clock({ resource }) {
  const { time } = resource.read();
  return <h3>{time}</h3>;
}

function Button({ onClick, children }) {
  const [startTransition, isPending] = useTransition({
    timeoutMs: 2000
  });

  const btnOnClick = () => {
    startTransition(() => {
      onClick();//还有2000ms延迟的lane 和setpending:false 一致的优先级 
    });
  };

  return (
    <>
      <button disabled={isPending} onClick={btnOnClick}>
        {children}
      </button>
      <span>{isPending && " loading"}</span>
    </>
  );
}

export default function App() {
  const [time, setTime] = useState(fetchTime());

  const load = () => {
    setTime(fetchTime());
  };

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Button onClick={load}>加载</Button>
      <Clock resource={time} />
    </Suspense>
  );
}

复制代码

useTransition实现

image-20210524114318874会触发lian两次更新 一次是高优先级的更新回调函数为pending=true,表示我们正在执行延迟任务执行,等待pending中,第二次是低优先级更新,回调函数为setpending=false.callback(即我们传入useTransition的函数),低优先级执行pending=false和我们想推迟的回调。 这就实现了回调函数会延迟执行。

updatesuspense实现

先保持suspense两个子组件为nextchildren 此例为button 和clock 但是最终返回的是primarychild,怎么得到呢?

创建一个offsetfiber 他的children是两个子组件,mode:”visible”根据模式是不是可见来判断要不要展示,最终返回的是offsetFiber,即我们suspense和children之间有一层offsetFiber子组件,通过offsetFiber判断两个子组件是否可见。

react在workloop函数下面会catch(error),我们看这个error是不是promise对象(发生在clock子组件上,因为他直接同步执行promise,同步方法执行异步,会抛出错误),是的话会一直向上抛出这个错误的error promise 找到最近的suspense组件,将这promise加入到suspense的fiber的updatequeue中,然后执行这个promise的resolve方法,触发root的更新使得视图重新渲染。(不大懂)

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享