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在后台并发处理渲染和其他任务,它可以确保应用程序始终保持响应,并且可以更好地优化任务的执行顺序,从而提高性能和用户满意度,但也存在一些坑,比如上述撕裂问题,需要用户主动关注,


参考文章

相关推荐
浮华似水10 分钟前
简洁之道 - React Hook Form
前端
正小安2 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch4 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光4 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   4 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   4 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web4 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常4 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇5 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr5 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui