当前位置: 首页 > news >正文

React 18 的核心设计理念:并发渲染

React16在16.8版本中推出了Fiber架构设计,极大的解决了大型渲染任务的卡顿问题。但此时的Fiber架构依然不够完善,依然存在一定的问题。React18的到来,相比以前,又有了新的突破

React 18 的核心设计理念:并发渲染

在 React 18 之前,React 的渲染模式是 “同步” 和 “不可中断” 的。
请注意:同步和不可中断我都打了引号,表明这不是真正意义上的同步和不可中断,而仅仅是针对react渲染任务的同步和不可中断(后面会说明)

Fiber的引入

Fiber自16.8版本时便已经引入,引入Filber后的react,终结了传统 React 同步递归处理整个组件树,导致处理时间过长时,阻塞其他任务从而导致卡顿的问题。

如何解决的:
react将整颗渲染树,拆分成一个一个的节点,这个节点就是Fiber,它是一个对象,记录着每个react组件的相关信息。这样一样,react可以在每一帧的时间线内,一次只处理一个Fiber单元的工作(这些工作包括 调用渲染方法收集子元素、diff,标记副作用,内存中生成dom,收集副作用等等),处理完成后,会去查看当前线程是否空闲,如果空闲,才会继续执行下一个Fiber,否则就去做其他要做的事情。通过这种增加细粒度的方式,减少了每个渲染任务的执行时间,从而避免一个大的渲染任务阻塞其他任务(如用户输入等)导致明显卡顿的问题

诶,这就有个疑问了,这不是可中断的吗?为啥说是不可中断的呢,为啥还说它是同步的呢

传统Fiber的弊端

虽然Fiber已经可以将一个大任务切多个小任务了,但是它依然存在一定的弊端。
Fiber的每个任务一旦开始执行,将无法停止,这也意味着,Fiber的所有任务,依然是按照顺序一个一个执行的,前一个任务没有结束之前,下一个Fiber任务将不会开始执行。只是每个Fiber任务执行的间隙之间,可以穿插执行一些其他任务而已。

故,对于每个Fiber任务单元来说,它本质上还是一个同步的过程,并且每个Fiber任务一旦开始执行,将不可中断。

并发渲染

并发渲染的核心思想:将渲染工作分解成小的单元,在浏览器的每一帧的空闲时期去执行这些工作单元,并且可以被更高优先级的任务(如用户输入)所中断,这就是所谓的抢占式调度

这里有个关键的点,就是Fiber任务添加了一个优先级的概念,高优先级的任务可以中断低优先级任务的执行。且被中断的低优先级任务,不会再缓存,在等待高优先级任务执行完成后,会重新开始执行这个任务,而不会接着之前的进度继续执行。

这里涉及到渲染树的丢弃与重建:

  • React 16.8:中断后,保留WorkInProgress Tree的状态,恢复时继续构建

  • React 18:中断后,可以完全丢弃当前的WorkInProgress Tree,基于最新状态重新开始构建

// React 18的抢占式中断
function prepareFreshStack(root, nextLanes) {// 丢弃当前的workInProgress树!root.workInProgress = null;workInProgressRoot = null;// 基于最新的props和state重新开始workInProgress = createWorkInProgress(root.current, nextLanes);
}

有人又要问了WorkInProgress Tree是什么。

WorkInProgress Tree是react双缓存架构中,缓存在内存中的一颗渲染树

  • Current Tree: 当前屏幕上显示内容对应的 Fiber 树。

  • WorkInProgress Tree: 正在内存中构建的、下一次要渲染的 Fiber 树。
    当开始一次更新时,React 会从 Current Tree 的根节点开始,为每个需要更新的 Fiber 节点创建一个 “替身” (在 alternate 上),这些替身共同构成了 WorkInProgress Tree。所有的工作(如调用 render、diff)都在 WorkInProgress Tree 上进行。完成后,通过简单的指针交换,WorkInProgress Tree 就变成了新的 Current Tree。

react18为了实现并发渲染这一根本特性,对Fiber架构做了一系列的深化

1. Fiber对象新增属性:lanes(车道概念,一个表示更新优先级的位掩码。这是实现调度和中断的基础。), 用来标识任务的优先级

// React内部的车道定义(简化)
export const SyncLane = 0b0000000000000000000000000000001; // 最高优先级
export const InputContinuousLane = 0b0000000000000000000000000000100; // 用户输入
export const DefaultLane = 0b0000000000000000000000000010000; // 普通更新
export const TransitionLane = 0b0000000000000000000000000100000; // 过渡更新
export const IdleLane = 0b0100000000000000000000000000000; // 最低优先级

2. 自动批处理
在 React 17 及以前,只有在 React 的事件处理函数(如 onClick)中的多次 setState 会被批处理,合并成一次重新渲染。而在 setTimeout, Promise, 原生事件监听器等中的 setState 不会批处理,导致多次渲染。
React18在任何地方(事件处理函数、setTimeout、Promise 等)的更新都会被自动批处理。这减少了不必要的渲染,提升了性能。
实现原理:React 18 有一个统一的“更新上下文”机制。在执行任何用户代码时,React 会先设置一个“批处理上下文”,在这个上下文中触发的所有更新都会被收集起来,在上下文退出时统一处理。如果需要强制同步更新,可以使用 ReactDOM.flushSync() 退出批处理。

让我们来看一段代码:

// 这是一个React17的demo
import React, { useEffect, useState } from 'react';
import './App.css';function App() {console.log("React17 is Rerender=====")const [count, setCount] = useState(0);const [name, setName] = useState("");const [inputValue, setInputValue] = useState("123");useEffect(() => { setTimeout(() => {setCount(count + 1);setName(`${name}+`);setInputValue(`${inputValue}-`);}, 0)}, [])return (<div className="App">demo</div>);
}export default App;

此时,刷新页面,控制台输出为:
在这里插入图片描述
可以看到,除了首次的页面渲染,setTimout中的3次状态更新都触发了一次render

再看下react18上的表现

// 这是React 18的demo
import { useState } from 'react'
import './App.css'
import { useEffect } from 'react'function App() {console.log('React18 is rerender====')const [count, setCount] = useState(0);const [name, setName] = useState("");const [inputValue, setInputValue] = useState("123");useEffect(() => { setTimeout(() => {setCount(count + 1);setName(`${name}+`);setInputValue(`${inputValue}-`);}, 0)}, [])return (<><div>demo</div></>)
}export default App

此时,控制台输出:
在这里插入图片描述
可以看出,除了首次渲染外,setTimout内的3次状态更新仅触发了一次render

3. startTransition - 非紧急更新
react18新增一个api startTransition,它能将状态的更新标记为非紧急更新(优先级lanes会更低),而react18在执行Fiber任务时,会优先执行优先级高的任务,再执行优先级低的任务。

import { startTransition } from 'react'
import { useState } from 'react'
import './App.css'
import { useEffect } from 'react'function App() {const [count, setCount] = useState(0)const [input, setInput] = useState('')const handleClick = () => {// startTransition(() => {//   setInput(`${input}+`)// })setInput(`${input}+`)setCount(count + 1)}useEffect(() => {console.log("input: 变化了")}, [input])useEffect(() => {console.log("count: 改变了")}, [count])return (<><div><buttononClick={handleClick}>更新数据</button></div></>)
}export default App

这是未使用startTransition的代码,正常情况下,很明显,useEffect会依次输出
在这里插入图片描述
此时,我们使用startTransition去改变input

import { startTransition } from 'react'
import { useState } from 'react'
import './App.css'
import { useEffect } from 'react'function App() {const [count, setCount] = useState(0)const [input, setInput] = useState('')const handleClick = () => {startTransition(() => {setInput(`${input}+`)})// setInput(`${input}+`)setCount(count + 1)}useEffect(() => {console.log("input: 变化了")}, [input])useEffect(() => {console.log("count: 改变了")}, [count])return (<><div><buttononClick={handleClick}>更新数据</button></div></>)
}export default App

此时控制台输出:
在这里插入图片描述

4. useDeferredValue - 延迟值
react还提供了useDeferredValue用于延迟一个值的更新,本质就是降低需要延迟的值的变化导致的渲染优先级。

我们先来看个例子,场景:输入框输入

// 场景:输入框输入
import { useDeferredValue, useState, useMemo } from 'react';function SearchBox() {const [query, setQuery] = useState('');const deferredQuery = useDeferredValue(query);// 基于延迟的查询值进行计算const results = useMemo(() => {return searchAPI(deferredQuery);}, [deferredQuery]);return (<div><input value={query} onChange={(e) => setQuery(e.target.value)} /><ResultsList results={results} /></div>);
}

原理:
这段代码中,当用户不断快速的输入值时,query会快速的不断变化,页面会快速不断的渲染,但是deferredQuery的值的更新,却是延迟的,因为它的更新优先级很低。

这可以避免什么问题呢
当searchAPI是一个昂贵的操作时,如果直接依赖query,那么每次query的改变,都将触发searchAPI这个昂贵操作的时候,可能导致卡顿的出现。而依赖deferredQuery时,deferredQuery的改变是延迟的,当query不断快速变化时,query的变化是高优先级的,会不断的中断deferredQuery这个低优先级的变化,导致deferredQuery会等到query不停的变化完成后(用户可能停止输入),deferredQuery才发生改变,从而使searchAPI只执行一次。

假设用户快速输入hello
当依赖query时:

用户输入: h -> 搜索: h
用户输入: he -> 搜索: he  
用户输入: hel -> 搜索: hel
用户输入: hell -> 搜索: hell
用户输入: hello -> 搜索: hello

当依赖deferredQuery时

用户输入: h -> deferredQuery: "" (可能还是空,取决于用户是否快速输入)
用户输入: he -> deferredQuery: """h"
用户输入: hel -> deferredQuery: "h""he"  
用户输入: hell -> deferredQuery: "he""hel"
用户输入: hello -> deferredQuery: "hel""hell"
用户停止输入后 -> deferredQuery: "hello"

5. Concurrent Features:并发特性的开关
React 18 并没有默认开启完全的并发模式,而是提供了三个“模式”入口,让你可以渐进式地使用并发特性:

  • ReactDOM.createRoot: 这是开启并发特性的唯一方式。使用这个 API 创建的根节点,其内部更新才有可能以并发方式执行。

  • ReactDOM.render: 传统模式,所有更新仍然是同步的。

  • ReactDOM.createBlockingRoot: 遗留模式,介于两者之间。

关键点: 即使使用了 createRoot,也不是所有更新都是并发的。它只是为并发提供了可能。更新的具体行为取决于你使用的 API。比如上面说的startTransition 或者 useDeferredValue

总结:

React 16的Fiber架构提供了可中断的渲染的基础,但是并没有根据优先级来调度更新,而是按照顺序处理更新,因此一旦开始渲染,就会一直直到完成,中间不会因为更高优先级的更新而中断。
React 18则实现了完整的并发渲染,它可以根据优先级来中断和重新调度更新,从而让用户交互等紧急更新能够立即生效。

在React 18中,并发(Concurrency)并不是指同时执行多个任务(并行),而是指在单个JavaScript线程中,通过任务调度和中断机制,让多个更新任务可以“同时”进行(即交替执行),从而能够优先处理高优先级的更新,提升用户体验。

React 18的并发渲染核心在于:更新可以有优先级,并且渲染工作可以被中断和恢复,而且可以根据优先级进行调度。

http://www.dtcms.com/a/491828.html

相关文章:

  • 昆明 网站建设兼职北京石景山网站建设
  • 中小型网站建设资讯网站及数据库怎么做后门
  • 建设网站财务分析wordpress中修改链接
  • 网站模板 实验室西安国际网站设计
  • 建设实验室网站的意义湖南长沙房价
  • 行业热点丨仿真驱动设计:兼顾性能、可持续性与效益
  • 番禺建设网站策划南充房产管理网
  • 网站建设xywlcn网站云主机
  • 网站建设静态部分报告总结长沙旅游必去的八个景点
  • 5种方法解决:安装一个或多个角色、角色服务或功能失败。找不到源文件
  • 虚幻引擎5 GAS开发俯视角RPG游戏 P05-11 消息小部件
  • 网站seo综合诊断福田服务商app软件安装
  • 无缝开发通道:IT-Tools+CPolar让远程协作像坐在一起编码
  • 网站普查建设背景网页qq属于
  • 网站建设 局部放大镜功能wordpress建英文网站
  • 武冈网站建设怎么买网站空间
  • 从0开始掌握企业知识库构建:核心概念与实践入门
  • 伦教网站设计宣传广告牌图片
  • 文案撰写网站wordpress登录密码错误
  • NewStarCTF2025-Week1-Misc
  • 有什么做ppt的网站分销渠道的三种模式
  • 什么企业需要做网站wordpress删掉不需要的
  • node.js上传图片接口
  • 静态网站建设的技术运用德阳如何做百度的网站
  • 深圳光明网站建设知名网站建设制作
  • h5游戏免费下载:球跑者
  • 使用Grafana监控K8S中的异常Pod
  • C语言基础之:指针、结构体、链表
  • 王国保卫战全集下载 1~5部全系列MOD DLC修版 安卓+ios+PC电脑版
  • wordpress邮件发验证码网站站内结构优化