面试官又问我是否了解 React 并发模式??

我们知道React V18中,提出的最重要的概念就是并发

这里我们要解释下并发,并发是具备处理多个任务的能力,但是不是同一时刻执行,而是交替执行。也就是说每次只能执行一个。

React 中的并发

我们知道js是单线程语言,JS引擎和渲染引擎在一个进程中,如果JS线程长时间占用了主线程,渲染进行就会等待,如果界面长时间不更新,用户的操作不能及时响应,看上去就会卡顿。

所以React想要能够优先的处理像高优先级的任务(如用户交互,点击,滚动等),同时中断延后低优先级的任务。

所以要想实现并发,React需要达到两个目标:

  • js暂停,将主线程还给浏览器,让浏览器能够有序的重新渲染页面
  • 需要执行暂停中的js任务。

浏览器的事件循环

我们知道浏览器的事件循环机制是从事件队列中取出一个任务执行,如果还没有到浏览器更新渲染的时间,就继续从任务队列中取出任务执行, 如果达到了浏览器渲染的时间,就会执行浏览器的渲染任务,依次循环。

这里提到了浏览器的渲染时间,解释一下:我们知道浏览器的页面也是一帧一帧画出来的,只要画的够快,就不会让人的视觉上感到卡顿。浏览器绘制一帧需要16.6ms。 这16.6ms中需要做很多事情:用户的输入,执行js脚本,处理窗口的变更,滚动,媒体查询,动画,然后执行requestAnimationFrame 的回调,最后布局绘制。如果还有时间就会执行requestIdleCallback 的回调函数。

所以我们要想让用户不感觉到卡顿,那就是让浏览器到底渲染时间的时候,就将主线程交给渲染进程。

这时候我们就会把目光放在了requestIdleCallback上了,这不正是我们想要的目的吗? 既要执行任务,又不耽误浏览器渲染。

requestIdleCallback

先看看MDN的解释:

window.requestIdleCallback() 方法插入一个函数,这个函数将在浏览器空闲时期 被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

看到这个解释,和React的理念十分相似,很可惜的是React并没有使用这个API,因为兼容太差了。

接下来就看看React是如何实现的呢?

MessageChannle

MessageChannle 接口本身是用来发送消息的, 允许我们创建一个消息通道, 通过两个MessagePort来进行消息的发送和接收。

我们现在再回顾以下React的目的:

  • js暂停,将主线程还给浏览器,让浏览器能够有序的重新渲染页面
  • 需要执行暂停中的js任务。

这里就会想到事件循环, 我们可以将没有执行完的js 放在任务队列, 下次事件循环再将事件循环中取出。

那如何将没有执行完的任务放在任务队列中呢?

这里需要产生一个宏任务, MessageChannel能够产生宏任务。这样就达到我们插队的目的了。

用法:

ini 复制代码
const channel = new MessageChannel();
const output = document.querySelector(".output");
const iframe = document.querySelector("iframe");

// 等待 iframe 加载
iframe.addEventListener("load", onLoad);

function onLoad() {
  // 在 port1 上监听消息
  channel.port1.onmessage = onMessage;

  // 将 port 2 传输到 iframe
  iframe.contentWindow.postMessage("来自主页的您好!", "*", [channel.port2]);
}

// 处理 port 1 收到的消息
function onMessage(e) {
  output.innerHTML = e.data;
}

为什么不使用setTimeOut(fn,0)这种方式呢?

因为setTimeout再层级超过5层之后, timeout小于4ms则会设置为10ms。 所以React没有选择setTimeout来产生宏任务。

为什么不使用requestAnimationFrame

因为这个只能在重新渲染之前才能执行一次。这样就会导致没开始渲染的时候, 就继续从任务队列中取消息。并且兼容性有问题, Safati和edge 是在页面渲染完成之后执行。

为什么不包装成微任务?

这是因为和微任务的执行机制有关系, 微任务队列会在清空整个队列之后才会结束。那么微任务会在页面更新前一直执行,达不到将主线程还给主线程目的。

Scheduler

上文中提到了React现在完成了如何对事件队列的插队,下一步就需要知道 React 怎么判断插队

任务优先级

首先Scheduler给任务定义了任务优先级(每个等级有延迟时间,计算任务的开始时间),延时时间最长,优先级最低,可以看看源码(V18):

可以看到React对任务分的很细(V16中只有几个)。

时间切片

为了避免单个任务占用主线程时间过长,引入了时间片的概念, 每个任务会被分配一个时间片,在时间片内可以持续执行, 一旦时间片用完,任务会主动让出主线程。

React 创建两个任务队列, 普通任务队列和延时队列。 当有任务需要调度时, 会创建一个包含回调函数,优先级,开始事件,过期时间的任务对象, 如果任务时间还未开始,会被加到延时队列, 如果已经开始会被加到普通任务队列。

Scheduler依赖浏览器的事件循环机制 来触发任务的调度, 当主线程空闲时,会启动调度循环,循环开始时,会先判断延时队列中查看是否已经到达开始时间的任务,如果有就会将这个任务放在普通任务队列中。

Scheduler会在普通任务队列中选择优先级最高(也就是过期时间最早)的任务进行执行,在执行任务的回调函数中会检查是否有时间片的限制(5ms:源码中的常量), 如果时间片用完,任务会被暂停,放回普通任务队列中。

执行完毕之后 会将任务从任务队列中移除,依次循环。

React 实际源码简化(Scheduler 核心逻辑):

scss 复制代码
// React 源码中的时间片管理(简化)
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
  if (workInProgress !== null) {
    // 时间片用尽,重新调度
    scheduleCallback();
  }
}

// 检查是否应让出主线程
function shouldYield() {
  return performance.now() >= deadline;
}

React 时间切片与任务优先级模拟

scss 复制代码
// 简化版任务队列(React Scheduler 核心逻辑)
const taskQueue = [];
const frameInterval = 5; // 5ms时间片

function workLoop(deadline) {
  while (taskQueue.length > 0 && deadline.timeRemaining() > 0) {
    const task = taskQueue.pop();
    task.execute(); // 执行任务
  }
  if (taskQueue.length > 0) {
    // 继续调度剩余任务
    requestIdleCallback(workLoop); 
  }
}

// 添加任务(带优先级)
function scheduleTask(task, priority) {
  taskQueue.push({ ...task, priority });
  taskQueue.sort((a, b) => a.priority - b.priority); // 按优先级排序
  requestIdleCallback(workLoop);
}

// 示例:高优先级任务(如用户输入)
scheduleTask({
  execute: () => console.log('High priority task'),
}, 1);

// 低优先级任务(如数据分析)
scheduleTask({
  execute: () => console.log('Low priority task'),
}, 2);

总结

React 将更新的操作包装成一个个任务,放在队列里维护,如果直接同步遍历,在这个期间,浏览器就没有其他时间响应用户的输入等高优先级的任务。 于是借助了MessageChannel 包装成异步任务,依然是遍历任务,只是每次执行任务的时候,判断是否过了被分配的事件(5ms),如果时间没有了,就在调用postMessage 让出主线程,等浏览器完成高优先级任务后,重新执行 onMessage 再推入,继续遍历任务列表。

参考文章:

相关推荐
小桥风满袖几秒前
Three.js-硬要自学系列7 (查看几何体顶点位置和索引、旋转,缩放,平移几何体)
前端·css·three.js
kim__jin3 分钟前
Vue3 使用项目内嵌iFrame
前端
独立开阀者_FwtCoder15 分钟前
# 一天 Star 破万的开源项目「GitHub 热点速览」
前端·javascript·面试
天天扭码26 分钟前
前端进阶 | 面试必考—— JavaScript手写定时器
前端·javascript·面试
梦雨生生43 分钟前
拖拉拽效果加点击事件
前端·javascript·css
前端Hardy1 小时前
第2课:变量与数据类型——JS的“记忆盒子”
前端·javascript
冴羽1 小时前
SvelteKit 最新中文文档教程(23)—— CLI 使用指南
前端·javascript·svelte
jstart千语1 小时前
【SpringBoot】HttpServletRequest获取使用及失效问题(包含@Async异步执行方案)
java·前端·spring boot·后端·spring
徐小夕1 小时前
花了2个月时间,写了一款3D可视化编辑器3D-Tony
前端·javascript·react.js
凕雨1 小时前
Cesium学习笔记——dem/tif地形的分块与加载
前端·javascript·笔记·学习·arcgis·vue