react18 新变化
总的来说,由于新的并发特性是渐进适配并按需启用的,React 18 中的重大更改仅限于几个简单的 API 更改,以及对 React 中多个行为的稳定性和一致性的一些改进,比较重要的一点是,不再支持 IE 浏览器。包括 createRoot、hydrateRoot 等 API。它还新增了 useId、useTransition、useDeferredValue、useSyncExternalStore 和 useInsertionEffect 等 hooks,以及 Strict Mode 的更新。
1. 加载根组件的方式变化
-
如果你继续使用 React 17 中的 ReactDOM.render() API,你将在控制台看到警告信息。
import ReactDOM from "react-dom"; import { BrowserRouter as RootRouter } from "react-router-dom"; ReactDOM.render( <RootRouter> <App /> </RootRouter>, document.getElementById("root") ); // 控制台警告信息: Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17.
-
需要替换成 React 18 的 createRoot() API。
import ReactDOM from "react-dom/client"; import { BrowserRouter as RootRouter } from "react-router-dom"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <RootRouter> <App /> </RootRouter> );
2. 自动批处理
createRoot() API 新增了 this.setState 的自动批处理功能。从 React 18 开始,状态批量更新也将应用在比如在 Promise、setTimeout 回调和原生事件处理程序中。而在 React 18 之前的版本中,这些原生事件处理程序中 this.setState 都是同步更新数据,是不会自动批处理的。也就是你调用几次 setState ,组件就会渲染几次。
-
React 17 中 setState 的特点
class App extends React.Component { state = { a: 1, }; componentDidMount() { this.setState({ a: 100, // 异步 }); setTimeout(() => { this.setState({ a: this.state.a + 1 }); // 同步 渲染一次 this.setState({ a: this.state.a + 1 }); // 同步 再渲染一次 }, 1000); } render() { console.log("render", this.state.a); return <div>test</div>; } } ReactDOM.render(<App />, document.getElementById("app"));
-
React 18 中 setState 的特点
class App extends React.Component { state = { a: 1, }; componentDidMount() { this.setState({ a: 100, // 异步 }); setTimeout(() => { this.setState({ a: this.state.a + 1 }); // 异步 this.setState({ a: this.state.a + 1 }); // 异步,合并更新 }, 1000); } render() { console.log("render", this.state.a); return <div>test</div>; } } ReactDOM.render(<App />, document.getElementById("app"));
-
如果需要让 this.setState 同步执行,可以通过 flushSync 函数进行强制同步执行
import React from "react"; import { flushSync } from "react-dom"; class Home extends React.Component { state = { a: 1, }; setA = () => { flushSync(() => { this.setState({ a: 100 }); }); flushSync(() => { this.setState({ a: 200 }); }); }; render() { return ( <div> <h5>Home</h5> <button onClick={this.setA}>setA</button> </div> ); } } export default Home;
3. 并发渲染
concurrent 不算是个新鲜概念,react 很早之前就开始为其铺路,早在 v16/v17 就引入了 fiber 架构和实验性的 concurrent Mode,开启后整个应用会开启并发渲染模式,但这将带来较大的破坏性改变。因此 React 18 提出了 Concurrent Rendering 的概念,即没有并发模式,只有并发特性,也就是说并发特性只是个可选项。默认情况下整个应用仍使用同步更新(legacy 模式),在使用了并发特性后相关的更新再开启并发更新,不用的话和之前就没有任何变化。
-
优先级分类
- 高优先级的更新/渲染:包括鼠标点击、输入框、拖拽、移动等对实时交互性要求很高的更新场景,卡顿时会影响用户的交互行为,使用户明显感到整个页面卡顿。一般这种会使用防抖或节流来减少高频率的操作。
- 非高优先级的更新/渲染:普通的 UI 更新,不与用户的交互相关,一些对更新实时性要求没那么高的场景。比如接口的请求。
- React 18 为我们提供了并发渲染方式来解决这个问题:使用 starTransition 和 useTransition,只有用到这些 API 时才会开启并发更新。
-
startTransition
用于标记非紧急的更新,用 starTransition 包裹起来就是告诉 React,这部分代码渲染的优先级不高,可以优先处理其它更重要的渲染。
-
useTransition
除了能提供 starTransition 以外,还能提供一个变量来跟踪当前渲染的执行状态。
import React, { useTransition, useState } from "react"; function Home() { // 可以使用 useTransition() 钩子来创建一个 transition 过渡任务。这个钩子返回一个函数来启动一个 transition,还有一个挂起的指示器来通知你 transition 的进度。 const [isPending, startTransition] = useTransition(); const [value, setValue] = useState(0); function handleClick() { startTransition(() => { setValue((value) => value + 1); }); } return ( <div> <h5>Home</h5> {isPending && <div>loading...</div>} <p>value: {value}</p> <button onClick={handleClick}>handleClick</button> </div> ); } export default Home;
4. 不常用的 hooks
以下的新 hook 主要用于解决 SSR 相关的问题或者是为第三方库的开发设计的,对于普通 React 应用开发者来说几乎用不到:
- useId 用于解决 SSR 时客户端与服务端难以生成统一的 ID 的问题。
- useSyncExternalStore 是一个为第三方库编写提供的新 hook,主要用于支持 React 18 在 concurrent rendering 下与第三方 store 的数据同步问题。
- useInsertionEffect 主要用于提高第三方 CSS in JS 库渲染过程中样式注入的性能。