React 并发渲染笔记

React 并发渲染(Concurrent Rendering)是 React 18 引入的一项重要特性,旨在提高应用的响应性和用户体验。它允许 React 在渲染过程中暂停、中断和恢复工作,从而优先处理用户交互等高优先级任务,避免长时间的渲染阻塞导致页面卡顿。 [1][2]

为什么需要并发渲染?

在 React 16 之前的版本中,更新过程是同步且不可中断的。这意味着一旦 React 开始渲染组件树,它会一气呵成地完成所有工作,直到更新真实 DOM。 [3][4] 如果组件树非常庞大,或者更新操作涉及大量计算,JavaScript 主线程会被长时间占用,导致浏览器无法及时响应用户输入(如点击、输入)或动画,从而出现卡顿现象,影响用户体验。 [1][2]

并发渲染的出现正是为了解决这个问题。它将渲染过程分解成多个可中断的小任务,并引入了优先级调度机制,使得 React 能够更灵活地利用浏览器的空闲时间,优先处理紧急任务。 [1][3]

并发渲染的核心原理

React 并发渲染的实现依赖于以下几个核心概念:

  1. Fiber 架构 [1][2]

    • 可中断性与可恢复性: Fiber 是 React 16 引入的一种全新的数据结构,它将组件树的每个节点抽象为一个 Fiber 节点。与传统的 VDOM 树不同,Fiber 节点之间通过 childsiblingreturn 等指针相互连接,形成一个链表结构。 [1][2] 这种链表结构使得 React 能够将渲染工作分解成一个个独立的工作单元(unit of work),每个工作单元对应一个 Fiber 节点。 [3][4] 这样,React 可以在处理完一个 Fiber 节点后,将控制权交还给浏览器,让浏览器有机会处理其他任务,然后在下一个空闲时间恢复之前中断的工作。 [3][5]
    • 双缓冲(Double Buffering): React 维护两棵 Fiber 树:current 树(当前屏幕上渲染的 UI 对应的 Fiber 树)和 workInProgress 树(正在构建的、代表下一次更新的 Fiber 树)。 [6] 当新的更新触发时,React 会在 workInProgress 树上进行计算和构建。 [6][7] 一旦 workInProgress 树构建完成,并且所有更新都准备就绪,React 会通过一个简单的指针切换,将 workInProgress 树变为 current 树,从而一次性地将所有更新呈现在屏幕上。 [6] 这种机制保证了 UI 更新的原子性,避免了中间状态的显示。 [6]
  2. 工作循环与阶段(Work Loop & Phases) [1][2]

    React 的渲染过程分为两个主要阶段:

    • 渲染/协调阶段 (Render / Reconciliation Phase): 这个阶段是可中断的。 [1][2] React 会遍历 Fiber 树,执行组件的 render 方法,对比新旧 Fiber 节点(Diff 算法),计算出需要进行的更新(如增、删、改),并在这个阶段为 Fiber 节点打上对应的副作用标记(flags)。 [1][2] 这个阶段不会操作真实 DOM。 [8]
    • 提交阶段 (Commit Phase): 这个阶段是同步且不可中断的。 [1][2] 在渲染阶段完成后,React 会进入提交阶段,根据渲染阶段打上的副作用标记,一次性地将所有更新应用到真实 DOM 上。 [1][6] 这个阶段还包括执行生命周期方法(如 componentDidMount/Update)、useLayoutEffect 回调、更新 ref 等操作。 [1][6]
  3. 优先级调度 (Priority Scheduling) - Lane 模型 [1][9]

    React 引入了优先级机制来管理不同更新的重要性。

    • Lane 模型: React 内部使用 Lane 模型来表示更新的优先级。 [1][9] 每个更新会被分配一个或多个 Lane,Lane 是一种位掩码(bitmask),通过位运算进行优先级的合并和判断。 [9][10] 优先级越高的 Lane,其对应的更新会越早被处理。 [11][12]

    • 优先级分类: React 将更新分为不同的优先级,例如:

      • 紧急更新 (Urgent Updates): 如用户输入、点击等,需要立即响应以保证流畅的用户体验。 [1][2]
      • 过渡更新 (Transition Updates): 如数据加载、UI 状态切换等,可以在后台进行,不阻塞用户交互。 [1][2]
    • Scheduler (调度器): React 内部使用一个独立的 scheduler 包来管理任务的调度。 [1][2] scheduler 模块类似于操作系统的任务调度器,它根据任务的优先级,利用 requestAnimationFrameMessageChannel 等浏览器 API,在浏览器空闲时执行 React 的渲染任务。 [5][13] 如果当前任务执行时间过长或有更高优先级的任务到来,scheduler 会暂停当前任务,将控制权交还给浏览器,并在适当的时机恢复。 [5][13]

如何实现并发渲染?

并发渲染的实现体现在 React 内部的调度逻辑和提供给开发者的 API 上。

内部实现概览:

  1. 更新触发:setState 或其他更新函数被调用时,React 会为该更新分配一个优先级(Lane)。 [1][2]
  2. 调度任务: React 将更新任务提交给 schedulerscheduler 会根据任务的优先级将其放入优先级队列中。 [5][13]
  3. 时间切片 (Time Slicing): scheduler 在浏览器每一帧的空闲时间(通常是 5ms)内执行优先级最高的任务。 [14] 在执行任务时,React 会不断检查是否还有剩余时间(通过 shouldYield 方法)或是否有更高优先级的任务。 [14][15]
  4. 暂停与恢复: 如果时间片用完,或者有更高优先级的任务需要处理,当前正在进行的渲染任务会被暂停,并将控制权交还给浏览器。 [5][13] 浏览器可以利用这段时间进行重绘或处理用户输入。当浏览器再次空闲时,scheduler 会恢复之前暂停的任务,从中断的地方继续执行。 [5][13]
  5. 提交: 一旦渲染阶段的所有工作(包括暂停和恢复)都完成,React 会进入同步的提交阶段,将更新一次性应用到真实 DOM。 [1][6]

开发者 API:

React 18 提供了 startTransitionuseDeferredValue 等 API,让开发者可以标记哪些更新是"过渡更新",从而利用并发渲染的优势。 [1][2]

  1. startTransition [1][2]
    startTransition 函数用于将一个状态更新标记为"过渡更新"(非紧急更新)。 [1][2]startTransition 包裹的更新优先级较低,React 会在后台渲染这部分 UI,而不会阻塞用户交互。 [7][16]

    代码示例:

    jsx 复制代码
    import React, { useState, startTransition } from 'react';
    
    function SearchPage() {
      const [inputValue, setInputValue] = useState('');
      const [searchQuery, setSearchQuery] = useState('');
      const [isPending, setIsPending] = useState(false); // useTransition 才能获取 isPending
    
      const handleChange = (e) => {
        // 紧急更新:立即更新输入框的值
        setInputValue(e.target.value);
    
        // 过渡更新:将搜索结果的更新标记为非紧急
        // startTransition 不直接提供 isPending 状态,这里仅为示意
        // 如果需要 isPending,通常会使用 useTransition
        // setIsPending(true); // 仅为示意,实际 startTransition 不提供此功能
        startTransition(() => {
          setSearchQuery(e.target.value);
          // setIsPending(false); // 仅为示意
        });
      };
    
      return (
        <div>
          <input
            type="text"
            value={inputValue}
            onChange={handleChange}
            placeholder="输入搜索内容"
          />
          {/* {isPending && <p>正在搜索...</p>} */} {/* useTransition 才能获取 isPending */}
          <SearchResults query={searchQuery} />
        </div>
      );
    }
    
    // 假设 SearchResults 是一个会进行耗时计算或数据获取的组件
    function SearchResults({ query }) {
      const items = React.useMemo(() => {
        // 模拟耗时计算
        const startTime = performance.now();
        while (performance.now() - startTime < 100) {
          // 模拟阻塞
        }
        return Array.from({ length: 500 }).map((_, i) => `${query} Item ${i}`);
      }, [query]);
    
      return (
        <ul>
          {items.map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>
      );
    }
    
    export default SearchPage;

    原理:startTransition 被调用时,它会改变当前上下文的优先级。 [17]startTransition 回调函数内部触发的所有状态更新,都会被标记为低优先级的"过渡更新"。 [1][2] React 的调度器会优先处理高优先级的更新(如 setInputValue),而将低优先级的 setSearchQuery 更新推迟到浏览器有空闲时间时再进行渲染,并且这个渲染过程是可中断的。 [1][2]

  2. useTransition [1][2]
    useTransition 是一个 Hook,它返回一个包含 isPending 状态和 startTransition 函数的数组。 [18][19] isPending 可以用来在过渡更新进行时显示加载指示器。 [18][19]

    代码示例:

    jsx 复制代码
    import React, { useState, useTransition } from 'react';
    
    function SearchPageWithTransition() {
      const [inputValue, setInputValue] = useState('');
      const [searchQuery, setSearchQuery] = useState('');
      const [isPending, startTransition] = useTransition(); // 使用 useTransition
    
      const handleChange = (e) => {
        setInputValue(e.target.value); // 紧急更新
    
        startTransition(() => {
          setSearchQuery(e.target.value); // 过渡更新
        });
      };
    
      return (
        <div>
          <input
            type="text"
            value={inputValue}
            onChange={handleChange}
            placeholder="输入搜索内容"
          />
          {isPending && <p>正在搜索...</p>} {/* 显示加载状态 */}
          <SearchResults query={searchQuery} />
        </div>
      );
    }
    
    // SearchResults 组件同上
    function SearchResults({ query }) {
      const items = React.useMemo(() => {
        const startTime = performance.now();
        while (performance.now() - startTime < 100) {
          // 模拟阻塞
        }
        return Array.from({ length: 500 }).map((_, i) => `${query} Item ${i}`);
      }, [query]);
    
      return (
        <ul>
          {items.map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>
      );
    }
    
    export default SearchPageWithTransition;
  3. useDeferredValue [1][2]
    useDeferredValue Hook 允许你延迟更新 UI 的某个部分的值。 [1][2] 它返回一个"延迟"版本的值,这个延迟值会"滞后于"最新的值。 [20] 当原始值发生变化时,useDeferredValue 会立即返回旧值,同时在后台尝试使用新值进行渲染。 [20] 这种后台渲染是可中断的。 [20]

    代码示例:

    jsx 复制代码
    import React, { useState, useDeferredValue } from 'react';
    
    function SearchPageWithDeferredValue() {
      const [inputValue, setInputValue] = useState('');
      // deferredQuery 是 inputValue 的延迟版本
      const deferredQuery = useDeferredValue(inputValue); // [16, 24, 34]
    
      const handleChange = (e) => {
        setInputValue(e.target.value); // 立即更新输入框
      };
    
      return (
        <div>
          <input
            type="text"
            value={inputValue}
            onChange={handleChange}
            placeholder="输入搜索内容"
          />
          {/* SearchResults 使用延迟的值,它的更新会是非阻塞的 */}
          <SearchResults query={deferredQuery} />
        </div>
      );
    }
    
    // SearchResults 组件同上
    // 注意:为了 useDeferredValue 的优化效果,
    // SearchResults 组件通常需要被 React.memo 包裹,
    // 以便在 deferredQuery 没有变化时跳过不必要的渲染。 [16]
    const SearchResults = React.memo(({ query }) => {
      const items = React.useMemo(() => {
        const startTime = performance.now();
        while (performance.now() - startTime < 100) {
          // 模拟阻塞
        }
        return Array.from({ length: 500 }).map((_, i) => `${query} Item ${i}`);
      }, [query]);
    
      return (
        <ul>
          {items.map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>
      );
    });
    
    export default SearchPageWithDeferredValue;

    原理: useDeferredValue 的内部实现与 useTransition 类似,它也是通过标记更新为过渡任务来实现的。 [18][21]inputValue 改变时,setInputValue 会立即更新,而 deferredQuery 暂时保持旧值。React 会在后台启动一个低优先级的渲染,使用新的 inputValue 来计算 deferredQuery 的新值,并渲染 SearchResults[20] 这个后台渲染是可中断的,如果用户继续输入,新的高优先级更新会打断当前的后台渲染,并从最新的值重新开始。 [20] 这样,用户输入框始终保持响应,而耗时渲染的 SearchResults 则在后台悄悄更新。 [20]

总结来说,React 并发渲染通过 Fiber 架构、优先级调度和时间切片等机制,将耗时的渲染工作拆分成小块,并在浏览器空闲时或以低优先级执行,从而确保了用户界面的流畅性和响应性。 startTransitionuseTransitionuseDeferredValue 等 API 则是开发者利用这些底层能力,优化用户体验的利器。


好文:

  1. 彻底搞懂React 18 并发机制的原理 - 稀土掘金
  2. 彻底搞懂React 18 并发机制的原理 - 腾讯云
  3. 深入理解React 的Fiber 架构 - 稀土掘金
  4. 面试官:说说对Fiber架构的理解?解决了什么问题? | web前端面试 - Vue3
  5. React调度系统- Scheduler - 稀土掘金
  6. 【从0实现React18】 (六) 完成commit提交流程并初步实现react-dom包,完成首屏渲染测试
  7. React 18的并发渲染:颠覆传统的性能飞跃- 个人文章 - SegmentFault 思否
  8. React 中的并发渲染 - IoT技术专栏
  9. 干货满满,React设计原理(三):藏在源码里的排位赛,Lane模型
  10. 优先级管理 - 图解React
  11. React源码解析之优先级Lane模型上 - 稀土掘金
  12. 前端宝典之六:React源码解析之lane模型 - CSDN博客
  13. React 的调度系统Scheduler 原创 - CSDN博客
  14. Scheduler的原理与实现 - React技术揭秘
  15. React 调度原理(scheduler)
  16. startTransition -- React 中文文档
  17. 给女朋友讲React18新特性:startTransition-51CTO.COM
  18. V18 - Transition | 前端那些事儿
  19. 一文读懂React的Transition实现原理 - 稀土掘金
  20. useDeferredValue -- React 中文文档
  21. 第37节------useDeferredValue+useTransition 原创 - CSDN博客
相关推荐
zwjapple6 小时前
docker-compose一键部署全栈项目。springboot后端,react前端
前端·spring boot·docker
像风一样自由20208 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
aiprtem9 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊9 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术9 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
GISer_Jing9 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止9 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall9 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴9 小时前
简单入门Python装饰器
前端·python
袁煦丞10 小时前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作