React 18版本来了
近两个月,React 18 已经正式发布了,带来了许多新的特性。在这个版本中, React通过改进渲染系统带来了并发能力,并在此基础上构建了转化或自动批处理等性能增强特性。
React 18 版本到底有什么新的特性,这些新的特性对 React 开发人员到底有什么帮助,让我们先来看一下 React 18 版本的 更新日志。
18.1.0 (April 26, 2022)
18.0.0 (March 29, 2022)
主要更改
由于新的并发特性是渐进适配并按需启动的,React 18 中重大更改仅限于几个简单的API 更改,以及对跨 React 不同行为的稳定性和一致性的一些改进。
客户端渲染API
一个明显的更改是一个新的,带有 createRoot()
的 root API
。 它的目的是替换掉现有的 render()
函数,提供更好的人体工程学并启用新的并发渲染特性。
// 以前的写法
import React from "react";
import ReactDOM from "react-dom";
import App from "App";
const rootElement = document.getElementById("app");
ReactDOM.render(<App />, rootElement);
// 18 用createRoot()的写法
import { createRoot } from "react-dom/client";
import App from "App";
const rootElement = document.getElementById("app");
createRoot(rootElement).render(<App />);
请注意,这个新的 API 现在已从 react-dom/client 模块导出。当然我们依然可以从 react-dom 中引入它。
import ReactDom from "react-dom";
const rootElement = document.getElementById("root");
const root = ReactDom.createRoot(rootElement);
root.render(<App />);
unmounting
和 hydration
API 也发生了变化:
unmountComponentAtNode
改为了root.unmount
:
// 以前卸载节点
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
// 现在
root.unmount();
hydrate
的服务器渲染改成了hydrateRoot
;
// 以前
import { hydrate } from 'react-dom';
const container = document.getElementById('app');
hydrate(<App tab="home" />, container);
// 现在
import { hydrateRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = hydrateRoot(container, <App tab="home" />);
另外,react
还将之前 render
函数的回调给干掉了,因为通常它在配合 Suspense
一起使用的时候得不到预期的效果:
import React, { useEffect } from 'react';
import ReactDom from 'react-dom';
import { createRoot } from "react-dom/client";
import App from './App'
// 以前
const container = document.getElementById('app');
ReactDom.render(<App tab="container" />, container, () => {
console.log('rendered')
})
// 现在
const AppWithCB = () => {
useEffect(() => {
console.log('rendered callback')
});
return <App tab="container" />
}
const container = document.getElementById('app');
const root = createRoot('caontainer');
root.render(<AppWithCB />)
服务端渲染API
在这个版本中,React
为了完全支持服务端的 Suspense
和流式SSR,改进了 react-dom/server
的 API
,旧的 Node.js
流式 API
将会被完全弃用:
renderToNodeStream
弃用⛔️️,使用时将发出警告。- 对应新版
Node
环境的流式传输 API 为:renderToPipeableStream
。
另外,React 在这个版本还引入了新的 renderToReadableStream
来支持 Deno、Cloudflare worker
等其他环境的流式 SSR
和 Suspense
。
renderToString、renderToStaticMarkup
这两个 API 还可以继续用,但是对 Suspense
支持就不那么友好了。
自动批处理
React
中的批处理简单来说就是将多个状态更新合并为一次重新渲染,由于设计问题,在 React 18
之前,React
只能在组件的生命周期函数或者合成事件函数中进行批处理。默认情况下,Promise、setTimeout
以及其他异步回调是无法享受批处理的优化的。从 React 18 开始,状态更新也将被安排到其他地方——比如在 Promise、setTimeout
回调和原生事件处理程序中。
// React 18 之前
function handleClick() {
setCount(c => c + 1);
setName('Tom');
// 在合成事件中,享受批处理优化,只会重新渲染一次
}
setTimeout(() => {
setCount(c => c + 1);
setName('Tom');
// 不会进行批处理,会触发两次重新渲染
}, 1000)
从 React 18
开始,如果你使用了 createRoot
,所有的更新都会享受批处理的优化,包括Promise、setTimeout
以及其他异步回调函数中。
// React 18
function handleClick() {
setCount(c => c + 1);
setName('Tom');
// 只会重新渲染一次
}
setTimeout(() => {
setCount(c => c + 1);
setName('Tom');
// 只会重新渲染一次
}, 1000)
这个更改虽然一般来说符合人们期望,也挺有用,但可能是破坏性的。如果你的代码依赖于在分开的状态更新之间重渲染的组件,那么你必须使其适应新的批处理机制,或使用 flushSync() 函数来强制立即刷新更改。如果你有特殊的渲染需求,不想进行批处理,也可以使用 flushSync
手动退出:
import { flushSync } from "react-dom";
const handleClick = () => {
flushSync(() => {
setCounter(c => c + 1);
});
// 更新 DOM
flushSync(() => {
setFlag(f => !f);
});
// 更新 DOM
}
其他更改
react 18 将不再支持 Internet Explorer
, 因为 React 18 现在依赖很多现代浏览器特性,如 Promise 或 Object.assign。鉴于微软将在今年 6 月 15 日停止对该浏览器的支持,React 和其他 JS 库也将停止对它的支持是很自然的。如果你的业务在 IE 还有用户,只能继续使用 React 17
及以下的版本了。
新的API
startTransition()
当我们使用 startTransition
API 时,正在做的是把一些不太紧急的动作标记为“过渡”,然后告诉 React 让其他更紧急的动作在渲染时间线中优先。
在React18之前,所有更新都是紧急的。这意味着上述两种状态会同时更新,并且直到所有内容都呈现出来前会阻碍用户看到交互的反馈。我们需要一种方法,告诉React哪些是紧急,哪些不是紧急的更新。
startTransition 依赖于
concurrent Mode
渲染并发模式。也就是说在 React 18 中使用startTransition
,要先开启并发模式。也就是需要用createRoot
创建 Root。
例如,输入一个过滤数据列表的输入字段,第一个输入更新是紧急的,更新输入的值可能会更改周围的一些UI,但是显示搜索结果不是那么紧急,都是在之后触发,用户并不需要立即完成。正常情况下,在 input 中绑定 onChange 事件用来触发上述的两种类的更新。
const handleChange = (e) => {
/* 改变搜索条件 */
setInputValue(e.target.value);
/* 改变搜索过滤后列表状态 */
setSearchQuery(e.target.value)
}
上述这种写法,那么 setInputValue
和 setSearchQuery
带来的更新就是一个相同优先级的更新。而前面说道,输入框状态改变更新优先级要大于列表的更新的优先级。 ,这个时候我们的主角就登场了。用 startTransition
把两种更新区别开。
const handleChange=()=>{
/* 高优先级任务 —— 改变搜索条件 */
setInputValue(e.target.value)
/* 低优先级任务 —— 改变搜索过滤后列表状态 */
startTransition(()=>{
setSearchQuery(e.target.value)
})
}
useTransition()
可以用来降低渲染优先级。分别用来包裹计算量大的 function和 value,降低优先级,减少重复渲染次数。
举个栗子,搜索引擎关键字联想。一般来说对于用户输入我们都希望是实时更新的,如果联想词也是实时更新,但是联想词特别多的话就会导致用户的输入卡顿。这不是我们期望的结果。我们可以将这个场景状态提取出来:一是用户输入更新,二十联想词的更新,这两个紧急程度明显是前者高于后者。
我们之前用防抖操作来过滤不必要的操作,但是这有个弊端,当我们连续长时间不间断的输入时,如果间隔时间小于防抖设置的时间,那么页面就会长时间没有任何相应。startTransition
可以指定UI 的渲染优先级,哪些需要实时更新,哪些可以延迟更新,即使用户长时间持续输入最迟5s也会更新一次,官方还提供了 Hook
版本的 useTransition
,接收传入一个毫秒参数用来修改最迟更新的时间,返回一个过渡期的 pending 状态和 startTransition 函数。
代码如下:
import { useTransition, useState } from "react";
import { Input, Button, List, Spin } from "antd";
import "../styles.css";
const Transition = () => {
const [value, setValue] = useState("");
const [searchValue, setSearchValue] = useState("");
const [isPending, startTransition] = useTransition(2000);
const handleChange = (e) => {
const { value } = e.target;
setValue(value);
// 延迟更新
startTransition(() => {
if (!!value) {
const data = Array(20000).fill(e.target.value);
setSearchValue(data);
} else {
setSearchValue([]);
}
});
};
return (
<div className="test">
<Button className="test_btn" type="primary">
useTransition
</Button>
<Input value={value} onChange={handleChange} />
{isPending ? (
<Spin spinning={isPending} tip="Loading..." />
) : (
<List
bordered
dataSource={searchValue}
renderItem={(item) => <List.Item>{item}</List.Item>}
/>
)}
</div>
);
};
export default Transition;
效果如图:
效果就和我们之前写的防抖差不多。
useDeferredValue
返回一个延迟响应的值,,在useDeferredValue内部会调用useState并触发一次更新,但是此更新的优先级很低、可以看作是startTransition的语法糖。
新的useDeferredValue()
API 允许我们选择 UI 的特定部分并有意推迟更新它们,这样它们就不会减慢页面的其他部分。这样做有两个好处:
1.控制渲染顺序
2.显示以前或旧值的能力,而不仅仅是加载动画或灰色框。
如上所述,这是一个非常好的面向设计的更新。没有什么比充满加载动画的页面更糟糕的了,而且很多时候稍微旧的数据总比没有数据好。这让我们的组件永远不会觉得它们正在加载,即使是在加载。对用户来说,看到的只是数据更新。
下面是一个如何使用它的示例:假设我们value
从一个定期更新的数据源中获取,但它的内容很多,通常需要一些时间来加载。现在,useDeferredValue
我们可以允许在后台获取新数据,并通过让我们的组件使用 的旧内容value
长达 4000 毫秒来创造快速平稳更新的假象。
const deferredValue = useDeferredValue(value, { timeoutMs: 4000 });
return (
<div>
<MyComponent value={deferredValue} />
</div>
);
Suspense 更新
React 的一大优点是代码的可读性。开发人员可以很容易地打开文件并从上到下阅读代码,以快速了解该组件中发生的情况。之前,我们必须指定某种加载状态,然后根据它编写相应的 JSX 进行条件渲染。这意味着我们的 UI 元素总是与特定数据的加载状态相关联。
const [loading, setLoading] = useState(true);
if myData != null {
setLoading(true);
}
<>
{ !loading && <MyComponent />}
{ loading && <Loading /> }
<>
Suspense通过允许我们为尚未准备好显示的 UI 元素指定后备来解决该问题。
<Suspense fallback={<Loading/>}>
<MyComponent myData={myData}/>
</Suspense>
它的设计受到了设计原则的启发,特别是骨架布局的概念,做过骨架屏的同学应该很容易理解,UI 元素始终在适当的位置,并在内容准备好时填充。这种方法使重新设计页面的 UI 变得更容易,因为我们可以添加新<Suspense>
组件(甚至嵌套在其他<Suspense>
组件中)。或将其他元素移入或移出现有<Suspense>
组件快速重新排列页面布局。
因为<Suspense>
组件本身并没有与特定的数据块(我们过去使用的方式)固有地绑定,它以一种真正优先考虑设计体验的方式将 UI 代码与功能代码分开。
不过,不仅可以将 Suspense 用于数据——我们还可以将它用于流式服务器渲染。