面试备战录

0 阅读7分钟

1、React 为什么要引入异步渲染(Concurrent Rendering)?从哪个版本开始支持?对开发者有什么影响?

Stack 架构

  • React16 之前版本使用的是 Stack 架构,它渲染组件时,使用递归函数同步遍历虚拟 DOM 树,这会把每一层组件的函数压入JS调用栈中
  • 调用栈的本质就是浏览器JS引擎维护了一个栈(后进先出)【LIFO】的数据结构,用来管理函数调用。
  • 这种架构下一旦虚拟DOM很大,递归很深,调用栈就会被完全占满,又由于JS是单线同步执行的,意味着同一时间只能做一件事情,这时候其他JS操作(比如滚动、点击)等都会在等待状态,表现出来的就是页面卡顿
  • Stack架构:
    • 逻辑简单,每次需要更新节点时,只需要从根结点递归遍历虚拟DOM树,便于维护;
    • 行为可预测,由于每次都是同步执行的,顺序是固定的,要么一次完成更新,要么抛出异常,不会有中间状态;
    • 适应于小型组件树,当组件不复杂,更新频率低时,无需要额外调度系统(Scheduler),从而可以更快的渲染,性能开销更低;
    • 调试更简单,同步渲染不涉及异步调度、优先级等逻辑,一旦出现异常情况可以更快的定位到问题所在。

Fiber 架构

从React16开始React为了解决同步渲染问题,引入了Fiber架构;Fiber架构是一套基于“链表 + 树 + 调度机制”的架构,可实现中断、增量更新、优先级调度等能力异步渲染;

  • Fiber 节点(FiberNode)
    • React中每个组件都会转化成一个Fiber对象,保存该组件的所有信息;
    • 可以把他理解为:链式的虚拟DOM节点 + 调度执行信息的任务单元
    • 简单的举例:就相当于Stack架构下,定义一个变量 = 整棵虚拟DOM树(内部通过对象嵌套的);Fiber架构下,定义了很多个变量,每一个变量=一个组件虚拟DOM树(一个对象不嵌套子组件,使用其中一个 key 记录子组件/父组件/其他信息)
  • 双缓存机制(current & workInProgress),Fiber架构的核心之一是双缓冲树结构
    • current:当前已经渲染完成的Fiber树(旧树);
    • workInProgress:正在构建的新 Fiber 树
  • 每次更新其实都是构建一颗新的Fiber树,然后在合适的时间提交。
    • 合适时间:React会在确保,所有要更新的组件都准备好(新Fiber树构建完成),并且在浏览器允许的空闲时间内,才统一把这些更新“提交”(操作真实DOM)到真实DOM中。
  • 可中断调度机制(协程式调度),React的渲染过程被拆分为两大阶段:
    • Render(Diff):构建新的Fiber树,收集执行副作用;(可中断)
    • Commit:操作真实DOM(不可中断):通过requestIdleCallback/MessageChannel等调度器,React可以在空闲时可以渲染,避免长时间阻塞主线程。

2、React Fiber 调度器

答:React Fiber的核心是任务调度,它允许React把渲染工作拆分为小块,分批执行,以避免阻塞主线程,从而实现更流畅的交互。 为了实现这些调度,React需要一个机制:

  • 安排任务在空闲时间执行;
  • 能被打断和恢复;
  • 有较高优先级的任务能抢占低优先级任务;

调度器内部封装的机制

MessageChannel(主力调度机制)【16.4引入 MessageChannel + performance.now()】

  • MessageChannel 是由浏览器提供的api,用来实现微任务队列的消息传递。
  • 通过postMessage触发消息事件,能立即调度回调执行,属于宏任务接近微任务的调度方式。
  • 优点:响应快,执行时间确定,兼容性好;
  • 缺点:无法判断浏览器是否真正空闲;仍然可能阻塞主线程;不能跨线程使用。
  • React Fiber中的调度器使用MessageChannel来实现一种自定义的循环调度,它可以精细控制执行时间和任务优先级。
  • React 使用 MessageChannel来模拟一种抢占式调度,让更高优先级任务能尽快执行。
console.log(window.MessageChannel);
const channel = new MessageChannel();
channel.port1.onmessage = () => {
  console.log('在微任务中触发');
};
channel.port2.postMessage(null); // 触发port1端口中的回调任务

requestIdleCallback(补充机制)

  • requestIdleCallback 浏览器原生API,允许你注册一个函数,当浏览器空闲时执行;
  • 这个空闲时间是指浏览器完成了所有高优先级任务(例如用户输入、动画帧、网络请求等)之后剩余时间;
  • 优点:非常适合低优先级任务,不会阻塞浏览器;
  • 缺点:不是所有浏览器都支持,且回调不够精准(例如:空闲时间短或者没有空闲时间回调可能延迟)
  • React版本早期使用它来调度任务,但兼容性和精度的限制让React逐渐放弃单纯依赖它。
requestIdleCallback(deadline => {
    while (deadline.timeRemaining() > 0) {
      // 浏览器会在帧之间的空闲时间,执行你传入的回调函数;
    }
  });

requestAnimationFrame(用于同步渲染阶段相关任务)

  • 负责 DOM 提交,确保 UI 刷新贴合浏览器节奏

setTimeout/setInterval(兜底用)

  • 最低级的 fallback 机制(当其它 API 不可用时兜底用)

Web Workers(实验/预研阶段)

问题1:为什么说 MessageChannel 属于“宏任务”或接近“微任务”的调度方式?

答:MessageChannel 本质上属于宏任务,但它执行时机非常接近微任务,延迟小、调度速度快,所以被称为接近微任务

  • 宏任务:setTimeout、setInterval、setImmediate、MessageChannel、I/O 事件、UI 渲染,下一轮事件开始时执行;
  • Promise.then()/catch()/finally()、queueMicrotask 微任务队列会在每个宏任务执行完后立刻执行。
console.log('start');
setTimeout(() => {
  console.log('setTimeout');
}, 0);
Promise.resolve()
  .then(() => {
    console.log('promise1');
  })
  .then(() => {
    console.log('promise2');
  });
const channel = new MessageChannel();
channel.port1.onmessage = () => console.log('messageChannel');
channel.port2.postMessage(null);
console.log('end');
// 输出 start、end、promise1、promise2、setTimeout、messageChannel
// 这里要注意: messageChannel 属于虽然宏任务调度速度极快,但也会要一点时间的,所以setTimeout 0秒执行是快于
// messageChannel 的 经测试(setTimeout >= 2)时才会先执行 messageChannel

问题2:为什么说 MessageChannel 执行时间确定?

答:MessageChannel 所谓“执行时间确定”是指:它会在当前宏任务和所有微任务完成后、尽快地执行,没有延迟、不依赖浏览器的空闲调度。相较于 setTimeout 和 requestIdleCallback,它更稳定、更可预测。

问题3:MessageChannel 是如何知道浏览器空闲了

答:MessageChannel本身不能判断浏览器空闲,它只是用来安排下一轮任务执行,真正判断是否空闲的逻辑,是React自己通过performance.now()控制的时间片判断出来的。

问题4:MessageChannel 为什么可能仍然会阻塞主线程?

答:MessageChannel不会自动让出主线程,它调度的是“下一轮任务”,但如果本身任务就很重,依然会阻塞主线程。

  • MessageChannel 本身是宏任务机制,不能插入代码中间,如果当前同步任务很重(比如大量循环、布局计算等),它无法插进来,必须等主线程空闲才能触发;
  • MessageChannel 调度的任务本身仍然运行在主线程上,如果执行时间太长,就还是会阻塞渲染、动画、交互;
  • 任务本身太重,没有分片机制,照样会卡住。

问题5:requestIdleCallback 在 React 中现在还在干什么?是不是已经被 MessageChannel 完全替代?

答:React现在仍然会在一些非关键路径上使用requestIdleCallback,但它已经不再是核心调度机制,而是作为一种补充的,用于非紧急处理。React 中 requestIdleCallback 的典型用途:

  • 异步预加载、后台任务、非关键更新
    • 用户离开页面后的一些清理逻辑
    • 非关键数据的缓存写入
    • 异步初始化(如首次加载时不重要的模块)
  • 在调度器初始化失败时兜底(fallback),在调度器内部如果检测到当前浏览器不支持MessageChannel,可能会使用requestIdleCallback作为一个兼容方案。
    • 特殊环境(某些旧设备或 WebView)
    • SSR 环境下不小心引入了浏览器端代码
    • React Native 或 React DOM 的 polyfill 扩展中
  • 在调度器初始化阶段探测「空闲能力」,有时候,React 甚至会用 requestIdleCallback 来测试:
    • 当前浏览器的空闲时间有多长
    • 有没有 idleCallback 支持
    • 用来帮助调度器估算时间片大小