React.js学习-一文梳理 React 18 新特性(转载)
转载自: https://blog.csdn.net/qq_41257129/article/details/123371328
React 的迭代过程
React 从 v16 到 v18 主打的特性包括三个变化:
- v16: Async Mode (异步模式)
- v17: Concurrent Mode (并发模式)
- v18: Concurrent Render (并发更新)
React 中 Fiber
树的更新流程分为两个阶段 render
阶段和 commit
阶段。组件的 render
函数执行时称为 render
(本次更新需要做哪些变更),纯 js 计算;而将 render
的结果渲染到页面的过程称为 commit
(变更到真实的宿主环境中,在浏览器中就是操作 DOM)。
在 Sync 模式下, render
阶段是一次性执行完成;而在 Concurrent
模式下 render
阶段可以被拆解,每个时间片内执行一部分,直到执行完毕。由于 commit
阶段有 DOM 的更新,不可能让 DOM 更新到一半中断,必须一次性执行完毕。
- Async Mode: 让 render 变为异步、可中断的。
- Concurrent Mode : 让 commit 在用户的感知上是并发的。
- Concurrent Render : Concurrent Mode 中包含 breaking change,比如很多库不兼容(mobx 等),所以 v18 提出了 Concurrent Render ,减少了开发者的迁移成本。
React 并发新特性
并发渲染机制(concurrent rendering)的目的:根据用户的设备性能和网速对渲染过程进行适当的调整, 保证 React 应用在长时间的渲染过程中依旧保持可交互性,避免页面出现卡顿或无响应的情况,从而提升用户体验。
v18 正式引入了的并发渲染机制,并基于此给我们带来了很多新特性。这些新特性都是可选的并发功能,使用了这些新特性的组件并能触发并发渲染,并且与其整个子树都将自动开启 strictMode
。
新 root API
v18 之前 root 节点对用户不透明。
import * as ReactDOM from 'react-dom'
import App from './App'
const root = document.getElementById('app')
// v18 之前的方法
ReactDOM.render(<App/>,root)
v18 中我们可以通过 createRoot Api 手动创建 root 节点。
import * as ReactDOM from 'react-dom'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('app'))
// v18 的新方法
root.render(<App/>,root)
想要使用 v18 中其他新特性 API, 前提是要使用新的 Root API 来创建根节点。
Automatic batching 自动批处理优化
批处理: React 将多个状态更新分组到一个重新渲染中以获得更好的性能。(将多次 setState
事件合并)
在 v18 之前只在事件处理函数中实现了批处理,在 v18 中所有更新都将自动批处理,包括 promise
链、 setTimeout
等异步代码以及原生事件处理函数。
// v18 之前
function handleClick () {
fetchSomething().then(() => {
// React 17 及之前的版本不会批处理以下的 state:
setCount((c) => c + 1) // 重新渲染
setFlag((f) => !f) // 二次重新渲染
})
}
// v18下
// 1、promise链中
function handleClick () {
fetchSomething().then(() => {
setCount((c) => c + 1)
setFlag((f) => !f) // 合并为一次重新渲染
})
}
// 2、setTimeout等异步代码中
setTimeout(() => {
setCount((c) => c + 1)
setFlag((f) => !f) // 合并为一次重新渲染
}, 5000)
// 3、原生事件中
element.addEventListener("click", () => {
setCount((c) => c + 1)
setFlag((f) => !f) // 合并为一次重新渲染
})
如果想退出自动批处理立即更新的话,可以使用 ReactDOM.flushSync()
进行包裹。
import * as ReactDOM from 'react-dom'
function handleClick () {
// 立即更新
ReactDOM.flushSync(() => {
setCounter(c => c + 1)
})
// 立即更新
ReactDOM.flushSync(() => {
setFlag(f => !f)
})
}
startTransition
可以用来降低渲染优先级。分别用来包裹计算量大的 function
和 value
,降低优先级,减少重复渲染次数。
举个例子:搜索引擎的关键词联想。一般来说,对于用户在输入框中输入都希望是实时更新的,如果此时联想词比较多同时也要实时更新的话,这就可能会导致用户的输入会卡顿。这样一来用户的体验会变差,这并不是我们想要的结果。
我们将这个场景的状态更新提取出来:一个是用户输入的更新;一个是联想词的更新。这个两个更新紧急程度显然前者大于后者。
以前我们可以使用防抖的操作来过滤不必要的更新,但防抖有一个弊端,当我们长时间的持续输入(时间间隔小于防抖设置的时间),页面就会长时间都不到响应。而 startTransition
可以指定 UI 的渲染优先级,哪些需要实时更新,哪些需要延迟更新。即使用户长时间输入最迟 5s 也会更新一次,官方还提供了 hook 版本的 useTransition
,接受传入一个毫秒的参数用来修改最迟更新时间,返回一个过渡期的 pending
状态和 startTransition
函数。
import * as React from "react";
import "./styles.css";
export default function App() {
const [value, setValue] = React.useState();
const [searchQuery, setSearchQuery] = React.useState([]);
const [loading, startTransition] = React.useTransition(2000);
const handleChange = (e) => {
setValue(e.target.value);
// 延迟更新
startTransition(() => {
setSearchQuery(Array(20000).fill(e.target.value));
});
};
return (
<div className="App">
<input value={
value} onChange={
handleChange} />
{
loading ? (
<p>loading...</p>
) : (
searchQuery.map((item, index) => <p key={
index}>{
item}</p>)
)}
</div>
);
}
所有在 startTransition
回调中更新的都会被认为是非紧急处理,如果一旦出现更紧急的处理(比如这里的用户输入), startTransition
就会中断之前的更新,只会渲染最新一次的状态更新。
startTransition
的原理就是利用了 React 底层的优先级调度模型。
更多例子: 真实世界示例:为慢速渲染添加 startTransition
SSR 下的 Suspense 组件
Suspense
的作用: 划分页面中需要并发渲染的部分。
hydration
[水化]:ssr 时服务器输出的是字符串(html),客户端(一般是浏览器)根据这些字符串并结合加载的 JavaScript 来完成 React 的初始化工作这一阶段为水化。
React v18 之前的 SSR, 客户端必须一次性的等待 HTML 数据加载到服务器上并且等待所有 JavaScript 加载完毕之后再开始 hydration
, 等待所有组件 hydration
后,才能进行交互。即整个过程需要完成从获取数据(服务器)→ 渲染到 HTML(服务器)→ 加载代码(客户端)→ 水合物(客户端)这一套流程。这样的 SSR 并不能使我们的完全可交互变快,只是提高了用户的感知静态页面内容的速度。
React v18 在 SSR 下支持了 Suspense
,最大的区别是什么呢?
服务器不需要等待被 Suspense 包裹组件是否加载到完毕,即可发送 HTML,而代替 suspense 包裹的组件是 fallback 中的内容,一般是一个占位符(spinner),以最小内联