react18 并发模式中的一些小细节

什么是并发模式?

在React之前,更新UI的过程是同步的,这意味着当React执行更新时,它会阻塞浏览器线程,直到更新完成为止。这可能导致用户界面在处理大量工作或复杂交互时出现卡顿或无响应的情况。

并发模式改变了这种方式。它允许React同时处理多个任务,而不会阻塞主线程。这意味着即使有耗时的任务正在进行,用户界面也可以保持响应,并且可以在后台进行渲染和其他工作。

React官方文档的解释比较详细了 不再赘述:

并发模式的用途是什么?

并发模式有几个重要的用途:

  1. 提高用户体验:通过使React能够在后台处理渲染和其他任务,而不会阻塞用户界面,提高了用户体验。这意味着即使在处理复杂的任务时,用户仍然可以流畅地与应用程序进行交互。
  2. 优化性能:并发模式使React能够更有效地管理任务和资源,从而提高应用程序的性能。通过并发处理渲染和其他任务,React可以更好地利用计算资源,并减少不必要的等待时间。
  3. 改善渲染优先级:并发模式还引入了渲染优先级的概念,这意味着React可以根据任务的重要性对它们进行排序。这样,React可以优先处理对用户体验最重要的任务,例如用户交互或动画。

核心实现原理

React的并发模式基于Fiber架构实现。Fiber是一种新的协调引擎,它允许React以非阻塞和可中断的方式处理渲染过程。在并发模式中,React可以将任务分解为多个小任务,并根据其优先级动态调整执行顺序。

具体而言,React使用一个优先级调度器来管理任务队列。每个任务都有一个优先级,React会根据任务的优先级以及当前系统资源的可用情况来决定任务的执行顺序。这样,React可以确保优先处理对用户体验最重要的任务,例如用户交互或动画。

js 复制代码
/* 理解 React Concurrent 模式的实现原理涉及到 React Fiber 架构的复杂性,以下是简化版的伪代码,演示其实现的核心思想 */

let nextUnitOfWork = null;

function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }
  requestIdleCallback(workLoop);
}

function performUnitOfWork(workInProgress) {
  // 执行当前 Fiber 节点的任务
  // 返回下一个 Fiber 节点
}

function scheduleWork(component, priority) {
  const newFiber = createFiber(component);
  newFiber.priority = priority;
  if (!nextUnitOfWork || priority < nextUnitOfWork.priority) {
    nextUnitOfWork = newFiber;
  }
}

function createFiber(component) {
  return {
    component,
    child: null,
    sibling: null,
    return: null,
    priority: 0,
    // 其他 Fiber 属性
  };
}

function requestIdleCallback(callback) {
  // 模拟浏览器的 requestIdleCallback 函数
  setTimeout(callback, 0);
}

// 初始化渲染
function render(rootComponent) {
  const rootFiber = createFiber(rootComponent);
  scheduleWork(rootFiber, 0); // 优先级为0,表示立即执行
}

// 启动任务调度循环
requestIdleCallback(workLoop);

// 示例代码
const App = () => {
  return <div>Hello, world!</div>;
};

render(App);

示例

让我们通过一个简单的示例来了解React的并发模式解决什么问题,假设我们有一个需要渲染大量数据的列表,并且用户可以通过搜索框来过滤列表中的项。在传统的同步渲染中,当用户在搜索框中输入时,整个列表会重新渲染,导致界面卡顿。现在让我们看看如何使用并发模式来改善这个问题:

jsx 复制代码
import React, { useState, useEffect } from 'react';

const List = ({ items }) => (
  <ul>
    {items.map(item => (
      <li key={item.id}>{item.name}</li>
    ))}
  </ul>
);

const App = () => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    // 模拟异步数据获取
    setTimeout(() => {
      const data = Array.from({ length: 1000 }, (_, index) => ({
        id: index,
        name: `Item ${index}`,
      }));
      setItems(data);
    }, 1000);
  }, []);

  const handleSearch = (e) => {
    // 处理搜索逻辑
  };

  return (
    <div>
      <input type="text" onChange={handleSearch} />
      <List items={items} />
    </div>
  );
};

export default App;

Fiber的设计模式

Fiber中没有使用普通JS堆栈,不能是递归的 , 如果不能使用递归,那么如何遍历树?使用单链表遍历树的算法:优先遍历父节点,深度优先

Andrew Clark: Fiber是调用栈的重新实现, 专为 React 组件设计. 你可以将一个 fiber 看作一个虚拟的栈帧

js 复制代码
let root = fiber;
let node = fiber;
while (true) {
  // Do something with node
  if (node.child) {
    node = node.child;
    continue;
  }
  if (node === root) {
    return;
  }
  while (!node.sibling) {
    if (!node.return || node.return === root) {
      return;
    }
    node = node.return;
  }
  node = node.sibling;
}

React 为何要使用链表遍历 Fiber 树: angularindepth.com/posts/1007/...)

React为啥要自己实现requestIdleCallback

  • 原生requestIdleCallback调用次数有限制
  • 浏览器调度的不够激进

并发模式调度优先级

React Concurrent 模式的调度优先级是指在处理任务时,React 根据任务的重要性和紧急程度来决定任务执行的顺序

在 React Concurrent 模式中,任务被分配不同的优先级,通常被分为以下几个大类:

  1. 最高优先级:用户交互任务,如点击、滚动等。这些任务对用户体验至关重要,需要尽快响应。
  2. 高优先级:动画任务和视觉效果的更新。虽然不像用户交互任务那样紧急,但仍然需要及时处理,以确保界面的流畅性和视觉效果的正确性。
  3. 普通优先级:常规的数据更新和渲染任务。这些任务通常是应用程序的核心逻辑,但不像用户交互或动画那样紧急。
  4. 低优先级:后台任务和较低优先级的更新。这些任务不需要立即处理,可以在系统空闲时进行。

react中通过以下3种优先级系统来实现优先级控制

  1. 调度器优先级:用于在调度器中对任务进行优先级排序。

    ini 复制代码
    export const NoPriority = 0;
    export const ImmediatePriority = 1;
    export const UserBlockingPriority = 2;
    export const NormalPriority = 3;
    export const LowPriority = 4;
    export const IdlePriority = 5;
  2. 事件优先级:用于标记用户事件的优先级。

    javascript 复制代码
    import {
      DiscreteEventPriority,
      ContinuousEventPriority,
      DefaultEventPriority,
    } from 'react-reconciler/constants';
  3. Lane 优先级:用于标记工作的优先级。

    js 复制代码
        export const SyncLanePriority: LanePriority = 15;
        export const SyncBatchedLanePriority: LanePriority = 14;
    
        const InputDiscreteHydrationLanePriority: LanePriority = 13;
        export const InputDiscreteLanePriority: LanePriority = 12;
    
        const InputContinuousHydrationLanePriority: LanePriority = 11;
        export const InputContinuousLanePriority: LanePriority = 10;
    
        const DefaultHydrationLanePriority: LanePriority = 9;
        export const DefaultLanePriority: LanePriority = 8;
    
        const TransitionHydrationPriority: LanePriority = 7;
        export const TransitionPriority: LanePriority = 6;
    
        const RetryLanePriority: LanePriority = 5;
    
        const SelectiveHydrationLanePriority: LanePriority = 4;
    
        const IdleHydrationLanePriority: LanePriority = 3;
        const IdleLanePriority: LanePriority = 2;
    
        const OffscreenLanePriority: LanePriority = 1;
    
        export const NoLanePriority: LanePriority = 0;

React 使用优先级调度器来管理任务队列,并根据任务的优先级动态调整执行顺序。例如,如果有一个高优先级的任务进入队列,React 将暂停当前任务的执行,立即处理高优先级任务,然后再回到之前的任务。

通过优先级调度,React 能够更好地响应用户操作,提高页面的交互性和流畅性。这种调度优先级的机制是 React Concurrent 模式的一个重要特征,使得 React 应用程序能够更有效地利用系统资源,提供更好的用户体验。

并发模式下的渲染撕裂问题

由于并发模式下react的compiliation是根据任务优先级来渲染的,这样会导致不同的子节点渲染根据同一个状态来渲染时,状态如果在渲染中间发生变化则最终渲染结果会导致不一致,就是所谓的状态撕裂: github.com/reactwg/rea...

解决办法: 使用useSyncExternalStore 或者 flushSync将并发模式任务改为同步渲染。。

ps:zustand使用useSyncExternalStore解决这个问题的issule

新版版的react-redux也使用了【使用useSyncExternalStore】 来解决类似问题,请参看我的状态专栏文章: zustand

撕裂问题示例代码: codesandbox.io/p/sandbox/r...

总结

React的并发模式是一个重要的改进,旨在提高应用程序的性能和用户体验。通过允许React在后台并发处理渲染和其他任务,它可以确保应用程序始终保持响应,并且可以更好地优化任务的执行顺序,从而提高性能和用户满意度,但也存在一些坑,比如上述撕裂问题,需要用户主动关注,


参考文章

相关推荐
放逐者-保持本心,方可放逐几秒前
vue3 中那些常用 靠copy 的内置函数
前端·javascript·vue.js·前端框架
IT古董几秒前
【前端】vue 如何完全销毁一个组件
前端·javascript·vue.js
Henry_Wu0013 分钟前
从swagger直接转 vue的api
前端·javascript·vue.js
SameX12 分钟前
初识 HarmonyOS Next 的分布式管理:设备发现与认证
前端·harmonyos
M_emory_39 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito42 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
fighting ~1 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录1 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
mon_star°2 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel