# React 理念

# react 理念

React 被设计用于 JavaScript 构建快速响应的大型 Web 应用程序。

由此可见, 快速响应 是 react 框架的关键

# 难点限制

需要解决快速响应的难点就要解决以下两种限制:

  • CPU 的瓶颈

    遇到大量计算操作时,页面掉帧导致卡顿

    • function App() {
        const len = 3000;
        return (
            <ul>
            	{
      new Map(len).fill(0).map((item, index) => <li>index</li>);
                }
            </ul>
        )
      }
    • 这样当页面执行 js 代码时,由于渲染线程和 js 脚本执行互斥,所以页面迟迟没有渲染,造成卡顿

    • 解决方案:

      • 在浏览器每一帧的时间中,预留一些时间给 JS 线程,React 利用这部分时间更新组件(预留的初始时间是 5ms)。

      • 预留的时间不够用时,react 将线程控制权交还给浏览器,使其有时间渲染 UI 界面。

      • 这种将长任务拆分到每一帧中,每次执行一小段任务的操作成为时间切片

      • // 通过使用 ReactDOM.unstable_createRoot 开启 Concurrent Mode, 启用时间切片
        // ReactDOM.render(<App/>, rootEl);
        ReactDOM.unstable_createRoot(rootEl).render(<App />);
      • 解决 CPU 瓶颈的关键在于实现时间切片,而时间切片的关键是: 将同步的更新变为可中断的异步更新 。

  • IO 的瓶颈

    发送网络请求后,需要等待数据返回才能进一步操作

    • 网络延迟是无法解决的,如何在网络延迟客观存在的情况下,减少用于对网络延迟的感知?

    • 如果请求数据和显示页面并行执行,就会造成页面的卡顿,而一旦先显示页面,后面请求数据再更新就会流畅很多。

    • react 采用的方案是:将界面 UI 的同步更新变为可中断的异步更新

# React15 架构

React15 的架构分为协调器和渲染器

# 协调器

  • Reconciler 协调器负责找出变化的组件。
  • React 中通过 this.setState 、 this.forceUpdate 和 ReactDOM.render 等 API 进行更新。每当有更新发生,协调器就会做如下工作:
    • 调用函数组件或者类组件的 render 方法,将返回的 JSX 转化为虚拟 DOM
    • 将虚拟 DOM 和上次更新时的虚拟 DOM 比对
    • 通过比对找出本次更新中变化的虚拟 DOM
    • 通知渲染器将变化的虚拟 DOM 渲染到页面上

# 渲染器

  • React 支持跨平台,所以不同平台有不同的 Renderer 渲染器,浏览器环境是 ReactDOM
  • Renderer 渲染器负责将变化的组件渲染到页面上

# 缺点

  • mount 的组件会同步递归更新子组件,造成 UI 卡顿。

    在协调器 Reconciler 中,mount 的组件会调用 mountComponent,update 的组件会调用 updateComponent 方法,这两个方法都会递归更新子组件,而递归更新子组件是非常消耗性能的。

  • 同步更新 UI 视图,界面卡顿

    视图同步更新,并且 React15 不会中断进行中的更新

# React16 架构

React16 架构可以分为三层:

  • Scheduler 调度器:负责调度任务的优先级,高优先级的任务先进入 Reconciler 进行协调
  • Reconciler 协调器:找出变化的组件
  • Renderer 渲染器:将变化的组件渲染到页面上

React16 架构相比 React15 添加了 Scheduler 调度器

# 调度器

  • 首先以浏览器是否有剩余时间作为任务终端的标准,那么就需要一种机制,当浏览器有剩余时间时通知我们。可以使用 requestIdleCallback 这个 API,但是因为下面的原因被弃用了:

    • 浏览器兼容性(跨平台所不能容忍的)
    • 触发频率不稳定,会受很多因素影响
  • Scueduler 调度器库由此诞生,并且独立于 React

# 协调器

  • React15 中的 Reconciler 协调器是同步递归处理虚拟 DOM 的,这样会造成 UI 视图层的卡顿。在 React16 中将更新工作从递归变成了可以中断的循环过程,每次循环都会调用 shouldYield 判断当前是否有剩余时间

  • React16 解决中断更新时,DOM 渲染不完全的问题

    React16 中,调度器和协调器不再是交替工作,当调度器将任务交给协调器之后,协调器会为变化的虚拟 DOM 打上增 / 删 / 改 的标记

    整个工作都在内存中进行,只有当所有组件都完成协调的工作,才会统一交给渲染器进行渲染。

# 渲染器

Renderer 渲染器根据 Reconciler 为虚拟 DOM 打的标记,同步执行对应的 DOM 操作。

# 流程

  • 例如:点击 button 按钮,触发更新
  • 调度器受到更新,判断是否有凄然优先级更高的任务需要执行,如果没有就将组件状态改变,交给协调器
  • 协调器接收到更新,将变化的虚拟 DOM 打上标记,交给渲染器
  • 渲染器对打标记的 DOM 元素执行对应的操作

中间两个任务(调度器和协调器)随时都可能由于以下原因被中断:

  • 有其他更高优先级的任务需要执行

  • 当前帧没有剩余时间

    由于调度器和协调器的工作都在内存中进行,不会更新到页面上,所以即使反复中断,对用户来说也无感

# Fiber 介绍

Fiber 其实是一个数据结构,需要记录节点和节点之间的关系,在 Fiber 架构中的任务是可以中断执行的,继续执行时会丢弃原来完成的工作从头再来(数据不可变的原因),并且即使重新执行中断的任务,对用户侧来说也是无感的,因为 Fiber 架构的视图更新是后缓冲区视图 替换 前缓冲区视图的过程。
顺便理解一下 componentWillMount、componentWillReceiveProps、componentWillUpdate 这三个钩子被废弃的原因:因为 render 阶段是异步执行、可被中断的,再次回来的时候有可能会丢弃已经完成的工作从头再来,这样就可能会导致 Fiber 节点在 render 阶段重复调用 componentWillMount 这几个钩子,导致不符合开发者预期,并且当这几个钩子中存在副作用时,就会出现问题。

// Fiber 中关键字段
{
  type, //DOM 类型
  key, //reconciler 在协调过程中,决定该 fiber 是否要复用
  child, // 组件 render 函数返回值
  sibling,
  return, //return 属性指向父节点
  pendingProps,
  memoizedProps,
  pendingWorkPriority,
  alternate,
  ...
}

# Fiber 架构

Fiber 架构出现的原因:

由于 JS 单线程执行,并且 React15 版本的 reconciler 协调阶段只能一次执行完,不能被中途打断,这就导致:当更新渲染的执行任务较多时,协调阶段一次执行的时间花费就很长,这时就会出现掉帧、卡顿的现象。由此推出 Fiber 架构
Fiber 架构实现逻辑:
Fiber 架构借鉴生成器 (或者说协程) 的概念,将任务分给一个个的迭代器执行,迭代器完成执行任务的过程发生在浏览器空闲阶段,这样就不会阻塞 UI 的渲染。(由于 RequestIdleCallback 存在兼容性问题、并且在大量用户操作的时候不够稳定,因此使用 RequestAnimationFrame 和 disPatchEvent 消息队列来完成)

# 双缓冲更新机制

双缓冲更新机制就是:在视图需要进行更新时,通过在后台更新新的 fiber 树,如果遇到优先级更高的任务,就暂停当前的执行 (conscrent 模型) 任务,此时,即使更新到一半,对于用户侧来说也是无影响的,当整个新的 fiber 树构建完成之后,才会去用新的 fiber 树替换掉旧的 fiber 树.

# Fiber 架构心智模型

# 代数效应

  • 代数效应是函数式编程中的概念,用于 将副作用和函数相分离 。
  • 代数效应能够将副作用从函数逻辑中分离,使函数关注点保持纯粹
  • 代数效应最明显的应用就是 Hooks
  • 代数效应和 Fiber 架构:
    • Fiber(纤程)和 Generator(协程)可以理解为代数效应思想在 JS 中的体现。

# Fiber

  • React Fiber 可以理解为:
  • React 内部实现了一套状态更新机制,支持任务的不同优先级,可以中断和恢复,并且恢复后可以复用之前的中间状态。
  • 其中每个任务更新单元就是 React Element 对应的 Fiber 节点。

# Fiber 原理

# 原因

  • 在 React15 及以前,协调器采用递归的方式创建虚拟 DOM,递归过程是不能中断的,如果组件树嵌套很深就会占用线程很多时间,造成 UI 卡顿。
  • React16 将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归虚拟 DOM 数据结构无法满足需要了,就有了全新的 Fiber 架构。

# 概念

  • React16 之后,虚拟 DOM 在 React 中有个正式的称呼:Fiber
  • Fiber 包含三层含义:
    • Fiber 架构,React16 的 Reconciler 协调器基于 Fiber 节点实现,被称为 Fiber Reconciler
    • Fiber 数据结构,作为静态的数据结构,每一个 Fiber 节点对应一个 React 组件,保存了该组件的类型、对应的 DOM 节点等信息
    • Fiber 动态工作单元,每一个 Fiber 节点保存了本次更新中该组件改变的状态和要执行的工作
  • Fiber 节点对应 DOM 节点、Fiber 节点构成的 Fiber 树对应 DOM 树。
    • 组件 mount 时, Reconciler协调器 根据 JSX 描述的组件内容生成组件对应的 Fiber节点 。
    • 组件 update 时, Reconciler协调器 将 JSX 与 Fiber节点 保存的数据对比(diff 算法),生成组件对应的 Fiber节点 ,并根据对比结果为 Fiber节点 打上 标记 。

# 原理

  • 双缓存
    • 在内存中构建并直接替换的技术叫双缓存,目的是不会出现白屏闪烁情况。
    • React 使用 “双缓存” 来完成 Fiber树 的构建与替换 —— 对应着 DOM树 的创建与更新
    • 在 React 中最多会同时存在两棵 Fiber树 。当前屏幕上显示内容对应的 Fiber树 称为 current Fiber树 ,正在内存中构建的 Fiber树 称为 workInProgress Fiber树 。

# diff 算法

由于 Diff 操作本身也会带来性能损耗,React 文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O (n 3),其中 n 是树中元素的数量。

如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。

为了降低算法复杂度, React 的 diff 会预设三个限制:

  • 只对同级元素进行 Diff 。如果一个 DOM节点 在前后两次更新中跨越了层级,那么 React 不会尝试复用他。

  • 两个不同类型的元素会产生出不同的树。如果元素由 div 变为 p ,React 会销毁 div 及其子孙节点,并新建 p 及其子孙节点。

  • 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:

# 单节点 diff

  • 当 key相同 且 type不同 时,代表我们已经找到本次更新的 p 对应的上次的 fiber ,但是 p 与 li type 不同,不能复用。既然唯一的可能性已经不能复用,则剩下的 fiber 都没有机会了,所以都需要标记删除。
  • 当 key不同 时只代表遍历到的该 fiber 不能被 p 复用,后面还有兄弟 fiber 还没有遍历到。所以仅仅标记该 fiber 删除。

# 多节点 diff

Diff算法 的整体逻辑会经历两轮遍历:(对 Children 和 Fiber 进行遍历)

  • 第一轮遍历:处理 更新 的节点。

    • 如果 DOM 节点可以复用,比较下一个节点
    • 不可复用分两种情况
      • key 不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。
      • key 相同 type 不同导致不可复用,会将 oldFiber 标记为 DELETION ,并继续遍历
    • 遍历完,跳出第一轮遍历。
  • 第二轮遍历:处理剩下的不属于 更新 的节点,需要对第一轮遍历的结果分别讨论:

    • newChildren 与 oldFiber 同时遍历完

      那就是最理想的情况:只需在第一轮遍历进行组件 更新 。此时 Diff 结束。

    • # newChildren 没遍历完, oldFiber 遍历完

      已有的 DOM节点 都复用了,这时还有新加入的节点,意味着本次更新有新节点插入,我们只需要遍历剩下的 newChildren 为生成的 workInProgress fiber 依次标记 Placement 。

    • # newChildren 遍历完, oldFiber 没遍历完

      意味着本次更新比之前的节点数量少,有节点被删除了。所以需要遍历剩下的 oldFiber ,依次标记 Deletion 。

    • # newChildren 与 oldFiber 都没遍历完

      这意味着有节点在这次更新中改变了位置。接下来需要处理移动的节点并标记节点的移动进行处理。

# 实现调度系统

// 手写一个最简版的 React 调度系统
// 核心概念:lane 优先级、任务队列、时间切片、调度循环
// ==================== 优先级定义 ====================
const ImmediatePriority = 1;   // 同步优先级(最高)
const UserBlockingPriority = 2; // 用户阻塞优先级
const NormalPriority = 3;        // 正常优先级
const LowPriority = 4;          // 低优先级
const IdlePriority = 5;         // 空闲优先级(最低)
const priorityTimeout = {
  [ImmediatePriority]: -1,
  [UserBlockingPriority]: 250,
  [NormalPriority]: 5000,
  [LowPriority]: 10000,
  [IdlePriority]: 1000 * 60 * 60, // 1 小时
};
// ==================== 任务节点 ====================
class SchedulerTask {
  constructor(callback, priority) {
    this.callback = callback;           // 实际要执行的函数
    this.priority = priority;            // 优先级
    this.expirationTime = 0;           // 过期时间
    this.next = null;                   // 链表指针
    this.sortIndex = Infinity;         // 排序优先级
  }
}
// ==================== 任务队列(最小堆) ====================
class SchedulerMinHeap {
  constructor() {
    this.heap = [];
  }
  push(task) {
    const heap = this.heap;
    heap.push(task);
    this._siftUp(heap.length - 1);
  }
  pop() {
    const heap = this.heap;
    if (heap.length === 0) return null;
    const first = heap[0];
    const last = heap.pop();
    if (heap.length > 0) {
      heap[0] = last;
      this._siftDown(0);
    }
    return first;
  }
  peek() {
    return this.heap[0] || null;
  }
  get size() {
    return this.heap.length;
  }
  _siftUp(idx) {
    const heap = this.heap;
    while (idx > 0) {
      const parent = (idx - 1) >> 1;
      if (heap[parent].expirationTime > heap[idx].expirationTime) {
        [heap[parent], heap[idx]] = [heap[idx], heap[parent]];
        idx = parent;
      } else {
        break;
      }
    }
  }
  _siftDown(idx) {
    const heap = this.heap;
    const length = heap.length;
    while (true) {
      const left = 2 * idx + 1;
      const right = 2 * idx + 2;
      let smallest = idx;
      if (left < length && heap[left].expirationTime < heap[smallest].expirationTime) {
        smallest = left;
      }
      if (right < length && heap[right].expirationTime < heap[smallest].expirationTime) {
        smallest = right;
      }
      if (smallest !== idx) {
        [heap[smallest], heap[idx]] = [heap[idx], heap[smallest]];
        idx = smallest;
      } else {
        break;
      }
    }
  }
}
// ==================== 调度器核心 ====================
class Scheduler {
  constructor() {
    // 任务队列(按过期时间排序的小顶堆)
    this.taskQueue = new SchedulerMinHeap();
    // 当前正在执行的任务
    this.currentTask = null;
    // 是否正在调度
    this.isScheduling = false;
    // MessageChannel 用于实现时间切片(兼容 requestIdleCallback)
    this._channel = null;
    this._initChannel();
    // 帧预算(5ms,用于时间切片)
    this.frameDeadline = 0;
    this._frameInterval = 5; // 5ms
    // 当前时间(虚拟,用于计算过期时间)
    this.currentEventTime = 0;
  }
  // 初始化 MessageChannel
  _initChannel() {
    this._channel = new MessageChannel();
    this._channel.port2.onmessage = () => this._flushWork();
  }
  // 获取当前时间(毫秒)
  _getCurrentTime() {
    return performance.now();
  }
  // 计算过期时间
  _computeExpirationTime(priority) {
    const startTime = this._getCurrentTime();
    const timeout = priorityTimeout[priority];
    if (timeout === -1) {
      return -1; // 同步任务立即过期
    }
    return startTime + timeout;
  }
  // 检查是否应该让出主线程
  _shouldYield() {
    const currentTime = this._getCurrentTime();
    return currentTime >= this.frameDeadline;
  }
  // 请求调度
  _request调度() {
    // 使用 MessageChannel 发起调度
    this._channel.port1.postMessage(null);
  }
  // 执行单个任务
  _executeTask(task) {
    const callback = task.callback;
    if (!callback) {
      // 任务已被取消,跳过
      return;
    }
    this.currentTask = task;
    const finished = callback();
    if (typeof finished === 'function') {
      // 返回了新 callback,继续调度
      task.callback = finished;
      this.taskQueue.push(task);
    } else {
      // 任务完成,释放
      this.currentTask = null;
    }
  }
  // 调度循环(核心)
  _flushWork() {
    this.isScheduling = true;
    // 如果有 requestIdleCallback,用它来计算帧预算
    if (typeof requestIdleCallback !== 'undefined') {
      requestIdleCallback((deadline) => {
        this.frameDeadline = deadline.timeRemaining();
        this._workLoop();
        this.isScheduling = false;
        this._checkForMoreWork();
      });
    } else {
      // 兜底:手动计算帧预算
      this.frameDeadline = this._getCurrentTime() + this._frameInterval;
      this._workLoop();
      this.isScheduling = false;
      this._checkForMoreWork();
    }
  }
  // 工作循环
  _workLoop() {
    while (true) {
      // 获取下一个要执行的任务
      const task = this.taskQueue.peek();
      if (!task) break;
      // 检查是否过期
      const currentTime = this._getCurrentTime();
      if (task.expirationTime <= currentTime && task.priority !== ImmediatePriority) {
        // 任务过期,弹出并丢弃(实际 React 中会直接执行,这里简化处理)
        this.taskQueue.pop();
        this._executeTask(task);
        continue;
      }
      // 检查是否应该让出
      if (this._shouldYield()) {
        // 让出主线程,等下一帧继续
        this._request调度();
        break;
      }
      // 执行任务
      this.taskQueue.pop();
      this._executeTask(task);
    }
  }
  // 检查是否还有待处理的任务
  _checkForMoreWork() {
    if (this.taskQueue.size > 0 && !this.isScheduling) {
      this._request调度();
    }
  }
  // ==================== 对外 API ====================
  // 调度一个回调函数
  scheduleCallback(callback, priority = NormalPriority) {
    const currentTime = this._getCurrentTime();
    this.currentEventTime = currentTime;
    const task = new SchedulerTask(callback, priority);
    task.expirationTime = this._computeExpirationTime(priority);
    this.taskQueue.push(task);
    // 如果是同步优先级,立即执行
    if (priority === ImmediatePriority) {
      this._flushWork();
    } else {
      // 请求调度
      this._request调度();
    }
    return task;
  }
  // 同步刷新(React 的 flushSync)
  flushSync(callback) {
    // 临时提高优先级
    const prevScheduling = this.isScheduling;
    this.isScheduling = true;
    try {
      return this.scheduleCallback(callback, ImmediatePriority);
    } finally {
      this.isScheduling = prevScheduling;
    }
  }
  // 让出检查(React 的 shouldYield)
  shouldYield() {
    return this._shouldYield();
  }
  // 取消任务
  cancelCallback(task) {
    task.callback = null;
  }
  // 获取当前任务
  getCurrentTask() {
    return this.currentTask;
  }
}
// ==================== 创建全局调度器实例 ====================
const scheduler = new Scheduler();
// ==================== 辅助函数 ====================
function getNextPriority() {
  const task = scheduler.taskQueue.peek();
  return task ? task.priority : null;
}
// ==================== 示例 & 测试 ====================
console.log('=== React 调度系统 测试 ===');
// 测试 1: 正常优先级任务
scheduler.scheduleCallback(() => {
  console.log('[Normal] 任务1执行');
}, NormalPriority);
// 测试 2: 低优先级任务
scheduler.scheduleCallback(() => {
  console.log('[Low] 任务2执行');
}, LowPriority);
// 测试 3: 同步任务
scheduler.scheduleCallback(() => {
  console.log('[Immediate] 同步任务执行');
}, ImmediatePriority);
// 测试 4: 模拟长时间任务(带时间切片)
let count = 0;
scheduler.scheduleCallback(() => {
  function doWork() {
    while (count < 10000000) {
      count++;
      if (count % 1000000 === 0) {
        console.log(`[Normal] 进度: ${count / 1000000}/10`);
      }
      // 模拟时间切片检查
      if (scheduler.shouldYield()) {
        // 让出,等下一帧继续
        scheduler.scheduleCallback(doWork, NormalPriority);
        return;
      }
    }
    console.log('[Normal] 长时间任务完成!');
  }
  doWork();
}, NormalPriority);
// 测试 5: flushSync
scheduler.flushSync(() => {
  console.log('[flushSync] 同步执行的任务');
});
// 测试 6: 任务取消
const taskToCancel = scheduler.scheduleCallback(() => {
  console.log('这不应该被执行');
}, LowPriority);
scheduler.cancelCallback(taskToCancel);
console.log('[Cancel] 已取消任务');
// 测试 7: 优先级队列验证
console.log('\n=== 队列顺序测试 ===');
scheduler.scheduleCallback(() => console.log('[Immediate] A'), ImmediatePriority);
scheduler.scheduleCallback(() => console.log('[Normal] B'), NormalPriority);
scheduler.scheduleCallback(() => console.log('[Low] C'), LowPriority);
scheduler.scheduleCallback(() => console.log('[UserBlocking] D'), UserBlockingPriority);

# 实现 useState

let workInProgressHook;
let isMount = true;
const fiber = {
  memoizedState: null,
  stateNode: App,
};
function schedule() {
  workInProgressHook = fiber.memoizedState;
  const app = fiber.stateNode();
  isMount = false;
  return app;
}
function dispatchAction(queue, action) {
  const update = {
    action,
    next: null,
  };
  if (queue.pending === null) {
    update.next = update;
  } else {
    update.next = queue.pending.next;
    queue.pending.next = update;
  }
  queue.pending = update;
  schedule();
}
function useState(initialState) {
  let hook;
  if (isMount) {
    hook = {
      queue: {
        pending: null,
      },
      memoizedState: initialState,
      next: null,
    };
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook;
    } else {
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;
  } else {
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }
  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    let firstUpdate = hook.queue.pending.next;
    do {
      const action = firstUpdate.action;
      baseState = action(baseState);
      firstUpdate = firstUpdate.next;
    } while (firstUpdate !== hook.queue.pending);
    hook.queue.pending = null;
  }
  hook.memoizedState = baseState;
  return [baseState, dispatchAction.bind(null, hook.queue)];
}
function App() {
  const [num, updateNum] = useState(0);
  console.log(`${isMount ? "mount" : "update"} num: `, num);
  return {
    click() {
      updateNum((num) => num + 1);
    },
  };
}
window.app = schedule();

# 资源文档

React 技术揭秘

更新于 阅读次数 次

请我喝[茶]~( ̄▽ ̄)~*

dmq 微信支付

微信支付

dmq 支付宝

支付宝