likes
comments
collection
share

循序渐进,彻底搞懂React调度器原理

作者站长头像
站长
· 阅读数 52

前言

  • 学完本篇文章你将对React不同优先级任务调度有一个全面的认识。

前置知识

学习本片文章前需要知道浏览器一帧中会做哪些事情

  • 三部分:执行js、渲染页面、空闲时间。

  • 在我们调用setState的时候会创建一个更新任务,当然react内部也会有其他等级的任务。为了避免任务执行时间超过当前帧空闲时间,造成页面卡顿的现象。React利用调度器实现了了在空闲时间内按优先级执行任务。

几个概念

  • work:一个任务,其count属性代表该任务包含count个子组件需要更新。

  • workList:任务队列,包含着React需要执行的所有任务。

  • perform:执行更新流程,当任务执行完毕后,执行schedule,开始下一轮调度。

  • schedule:调度器,从workList中获取一个高优先级任务,交给perform执行。

  • perform与schedule循环往复,实现整个任务调度。

初识任务调度

下面例子中,创建了三个不同等级的任务,每个任务里面都有100个组件需要更新。当然,更新组件的等级是相同的,这里只是作为区分。

html

<style>
  #app {
    word-break: break-all;
  }
</style>

<body>
  <div id="app"></div>
  <script src="./react优先级调度算法-基础版.js"></script>
</body>

javascript

const workList = [];
const contetnBox = document.querySelector("#app");

let list = [
  {
    value: "低",
  },
  {
    value: "中",
  },
  {
    value: "高",
  },
]

list.forEach(item => {
  const btn = document.createElement("button");
  btn.innerText = item.value;
  contetnBox?.appendChild(btn);

  btn.onclick = () => {
    // 添加任务
    workList.unshift({ ...item, count: 100 })
    schedule()
  }
})

// 执行任务
const renderComponent = (content) => {
  // 更好的观察任务调度,做的延迟效果
  let i = 10000000;
  while (i) {
    i--;
  }
  const ele = document.createElement("span");
  ele.innerText = `${content}`;
  contetnBox?.appendChild(ele)
}

// 调度器
function schedule() {
  // 获取一个任务,并弹出任务队列
  const curWork = workList.pop();
  if (curWork) {
    // 执行更新流程
    perform(curWork)
  }
}

// 更新流程
function perform(work) {
  while (work.count) {
    // count代表这个任务中有count个组件需要更新
    work.count -= 1;
    renderComponent(work.value)
  }
  // 当前任务中的组件全部更新完后,继续执行调度
  schedule()
}

效果图

在点击按钮后,会向任务队列workList中添加work。调度器开始工作,调度器从workList中获取一个任务,并弹出任务队列中。任务执行完后,调度器继续调度。schedule与perform循环调用,直至任务队列清空。

循序渐进,彻底搞懂React调度器原理

按优先级顺序执行任务

引入React调度器

引入该包的目的是使用一些方法辅助完成调度工作

项目结构

learn-schedule
├─ package-lock.json
├─ package.json
├─ public
│  └─ index.html
└─ src
   └─ index.js

导包

import {
  //空闲优先级
  unstable_IdlePriority as IdlePriority,
  //低优先级
  unstable_LowPriority as LowPriority,
  //用户阻塞优先级
  unstable_UserBlockingPriority as UserBlockingPriority,
  //普通优先级
  unstable_NormalPriority as NormalPriority,
  //立刻执行的优先级
  unstable_ImmediatePriority as ImmediatePrity,
  // 当某一个preform正在被调度,但是还没被执行时,可以使用该函数进行取消
  unstable_cancelCallback as cancelCallback,
  // 用于调度preform方法
  unstable_scheduleCallback as scheduleCallback,
  // 当前帧是否用尽了, 用尽了为true,此时需要中断任务
  unstable_shouldYield as shouldYield,
  // 返回当前正在调度的任务
  unstable_getFirstCallbackNode as getFirstCallbackNode,
  // unstable_scheduleCallback的返回值
  CallbackNode
} from "scheduler"

添加变量

// 本次schedule进行时,正在调度的任务的优先级
// 设置初始值为undefined
let prevPriority = undefined;

修改schedule方法

function schedule() {
  // 当前正在执行的调度任务
  const cbNode = getFirstCallbackNode();
  // 获取优先级最高的任务
  const curWork = workList.sort((node1, node2) => {
    return node1.priority - node2.priority;
  })[0]
  // 如果任务不存在,即任务队列为空
  if (!curWork) {
    return;
  }
  const { priority } = curWork;
  // 只有本次任务优先级 > 已经正在在执行的任务的优先级,才会中断正在执行的任务
  if (priority === prevPriority) {
    return;
  }
  // 此时本次的任务优先级 > 正在执行的任务优先级
  // 需要中断正在执行的任务
  if (cbNode) {
    cancelCallback(cbNode);
  }
  // 执行任务,以某个优先级来调度某个任务
  // 为什么要使用bind,因为scheduleCallback第二个参数是一个回调函数
  scheduleCallback(priority, perform.bind(null, curWork))
}

修改work

// 本次schedule进行时,正在调度的任务的优先级
let prevPriority = undefined;

const workList = [];
const contetnBox = document.querySelector("#app");

let list = [
  {
    priority: IdlePriority,
    value: "低",
  },
  {
    priority: LowPriority,
    value: "中",
  },
  {
    priority: NormalPriority,
    value: "高",
  },
]

list.forEach(item => {
  const btn = document.createElement("button");
  btn.innerText = item.value;
  contetnBox?.appendChild(btn);

  btn.onclick = () => {
    // 添加任务
    workList.unshift({ ...item, count: 100 })
    schedule()
  }
})

修改perform方法

任务执行完的时候,将prevPriority制空

// 更新流程
function perform(work) {
  if (work.count === 0) {
    const workIndex = workList.indexOf(work)
    workList.splice(workIndex, 1)
    // 任务执行完的时候,将prevPriority制空
    prevPriority = undefined;
  }
  while (work.count) {
    work.count -= 1;
    renderComponent(work.value)
  }
  schedule()
}

效果图

这个时候已经创建了三个不同优先级的任务,当点击多次低优先级任务后,再点击中优先级任务,会发现中优先级任务先被执行。此时已经完成了不同优先级的任务调度。

循序渐进,彻底搞懂React调度器原理

超出空闲时间时,任务可中断

上一个版本我们可以发现一个问题,当低优先级任务执行过程中,点击中优先级任务的时候,并没有马上中断低优先级的任务,而是等当前正在执行的低优先级任务执行完毕后,才执行中优先级任务。解决该问题的方法就是使perform可中断

修改perform

function perform(work) {
  // 当前任务是否是同步执行
  // ImmediatePrity是立即执行优先级,所以需要同步执行
  const isSync = work.priority === ImmediatePrity;
  // shouldYield判断浏览器当前帧是否剩余空闲时间
  while ((isSync || !shouldYield()) && work.count) {
    work.count -= 1;
    renderComponent(work.value)
  }
  if (work.count === 0) {
    const workIndex = workList.indexOf(work)
    workList.splice(workIndex, 1)
    // 任务执行完的时候,将prevPriority制空
    prevPriority = undefined;
  } else {
    prevPriority = work.priority;
  }
  //继续调度
  schedule()
}

效果图

当多次点击低优先级任务的时候,再点击中优先级任务,会发现调度器立刻切换到中优先级任务执行,当中优先级任务执行完毕后,会接着执行低优先级任务,并且页面流畅渲染。

循序渐进,彻底搞懂React调度器原理

优化调度

到这里的时候,其实已经实现了不同优先级的任务调度,但是还有可优化的余地。

观察发现,当任务正在执行时碰到没有空闲时间用完,需要中断执行,这时候会再次执行schedule方法重新进行一系列的调度工作。这个时候会浪费一些性能,其实当没有更高优先级的任务时,我们可以不进行调度,直接再下一个空闲时间内继续执行当前的work。

添加全局变量

// 当前被调度的回调函数
let curCallback = null;

修改schedule

在函数最后一行为curCallback赋值。curCallback是一个包裹了当前work的数据结构,后面会讲到。

// 调度器
function schedule() {
  // 当前正在执行的调度任务
  const cbNode = getFirstCallbackNode();
  // 获取优先级最高的任务
  const curWork = workList.sort((node1, node2) => {
    return node1.priority - node2.priority;
  })[0]
  // 如果任务不存在,即任务队列为空
  if (!curWork) {
    curCallback = null;
    return;
  }
  const { priority } = curWork;
  // 只有本次任务优先级 > 已经正在在执行的任务的优先级,才会中断正在执行的任务
  if (priority <= prevPriority) {
    return;
  }
  // 此时本次的任务优先级 > 正在执行的任务优先级
  // 需要中断正在执行的任务
  if (cbNode) {
    cancelCallback(cbNode);
  }
  // 执行任务,以某个优先级来调度某个任务
  // 为什么要使用bind,因为scheduleCallback第二个参数是一个回调函数
  curCallback = scheduleCallback(priority, perform.bind(null, curWork))
}

修改perform

在perform执行的最后,需要重新获取一遍curCallback,此时会触发schedule的priority === prevPriority判断,当没有更高优先级时,前后两个的curCallback值相等时,此时就可以直接循环perform函数即可,由因为perform是当作回调函数传递给scheduleCallback的,而且当perform返回一个函数时,scheduleCallback会直接执行这个函数,而不会去执行其他调度相关的工作,减少了部分性能开支。

// 更新流程
function perform(work) {
  // 当前任务是否是同步执行
  // ImmediatePrity是立即执行优先级,所以需要同步执行
  const isSync = work.priority === ImmediatePrity;
  // shouldYield判断浏览器当前帧是否剩余空闲时间
  while ((isSync || !shouldYield()) && work.count) {
    work.count -= 1;
    renderComponent(work.value)
  }
  if (work.count === 0) {
    const workIndex = workList.indexOf(work)
    workList.splice(workIndex, 1)
    prevPriority = undefined;
  } else {
    prevPriority = work.priority;
  }
  //存储当前回调
  const prevCallback = curCallback
  //继续调度
  schedule()
  //获取新的回调
  const newCallback = curCallback
  // 当没有更高优先级的时候,直接走perform
  if (prevCallback === newCallback) {
    return perform.bind(null, work)
  }
}

补充:curCallback是什么

其实就是对当前work信息的一个封装

在React源码源码中长这样

type Task = {
  id: number,
  callback: Callback | null,
  priorityLevel: PriorityLevel,
  startTime: number,
  expirationTime: number,
  sortIndex: number,
  isQueued?: boolean,
};

在项目中打印

循序渐进,彻底搞懂React调度器原理

最后

demo地址:https://github.com/jj19100/learn-schedule

转载自:https://juejin.cn/post/7150916189516988447
评论
请登录