React 的迭代过程
React 从 v16 到 v18 主打的特性包括三个变化:
- v16: Async Mode (异步模式)
- v17: Concurrent Mode (并发模式)
- v18: Concurrent Render (并发更新)
今天,我们就从开发者的角度来探索下React18的一些新特性。
新特性
一、Render API
创建一个初次渲染或者更新,以前我们用的是ReactDOM.render,现在改用react-dom/client中的createRoot,新的 root API 还支持 new concurrent render(并发模式的渲染),它允许你进入concurrent model(并发模式)。 同时,在卸载组件时,我们也需要将 unmountCompentAtNode升级为 root.unmount
React 17版本
二、setState 自动批处理
React18 通过在默认情况下执行批处理来实现了开箱即用的性能改进。
批处理是指为了获得更好的性能,在数据层,将多个状态更新批量处理,合并成一次更新(在视图层,将多个渲染合并成一次渲染)。
1. 在 React 18 之前:
在React18之前,我们只在React事件处理函数中行批处理更新。默认情况下,在promise、setTimeout、原生事件处理函数中或任务其他事件内的更新都不会进行批处理:
情况一:React 事件处理函数
可以看到,渲染次数和更新次数是一样的,即使我们更新了两个状态,每次更新组件也只渲染一次。
但是,如果我们把状态的更新放在 promise 或者 setTimeout里面:
情况二:setTimeout 或者 promise
点击一次会触发两次事件,不会进行批量更新
情况二:原生js事件
可以看到依旧是触发了两次。
2. 在 React 18 中:
可以看出,虽然有多次状态变更,但所有的变更都将自动批处理。无疑是很好的提高了应用整体的性能。
但是也有个特殊情况
总结:
- 在 18 之前,只有在react事件处理函数中,才会自动执行批处理,其它情况会多次更新
- 在 18 之后,任何情况都会自动执行批处理,多次更新始终合并为一次
三、 flushSync
允许你强制 React 在提供的回调函数内同步刷新任何更新,这将确保 DOM 立即更新。需要注意的是,如果flushSync函数内部有多个异步更新操作时仍然为批量更新 。
flushSync(callback)
官网上也说了flushSync 可能会严重影响性能,并且可能会意外地强制挂起的 Suspense 边界显示其 fallback 状态。大多数时候都不需要使用 flushSync,请将其作为最后的手段使用。
四、 transtion
React把update分成两种:
-
- Urgent updates 紧急更新,指直接交互,通常指的用户交互。如点击、输入等。这种更新一旦不及时,用户就会觉得哪里不对。
- Transition updates 过渡更新,如UI从一个视图向另一个视图的更新。通常这种更新用户并不着急看到。
1.startTransition
startTransition可以让你在不阻塞 UI 的情况下更新 state。
startTransition(scope)
函数可以将 state 更新标记为 transition。简单来说,就是被 startTransition 回调包裹的 setSate 触发的渲染被标记为不紧急渲染,这些渲染可能被其他紧急渲染所抢占。
import { startTransition } from 'react';
function TabContainer() {
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
2.useTransition
const [isPending, startTransition] = useTransition()
在使用startTransition更新状态的时候,用户可能想要知道transition的实时情况,这个时候就可以 使用这个api。
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
如果transition未完成,isPending值为true,否则为false。
-
useDeferredValue
返回一个延迟响应的值,可以让一个state延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValue和 startTransition一样,都是标记了一次非紧急更新。
从介绍上来看 useDeferredValue与 useTransition的区别
- 相同:useDeferredValue 本质上和内部实现与 useTransition一样,都是标记成了
延迟更新任务。 - 不同:useTransition是把更新任务变成了延迟更新任务,而 useDeferredValue是产生一个新的值,这个值作为延时状态。(一个用来包装方法,一个用来包装值)
新的API
一、useId
可以生成传递给无障碍属性的唯一 ID。
const id = useId()
支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不兼容,这解决了在 React 17 及 17 以下版本中已经存在的问题。因为我们的服务器渲染时提供的HTML 是无序的,useId的原理就是每个 id代表该组件在组件树中的层级结构
二、useSyncExternalStore
是一个让你订阅外部 store 的 React Hook。
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
三、useInsertionEffect
可以在布局副作用触发之前将元素插入到 DOM 中。
useInsertionEffect是为 CSS-in-JS 库的作者特意打造的。除非你正在使用 CSS-in-JS 库并且需要注入样式,否则你应该使用 useEffect 或者 useLayoutEffect。
并发更新的特性
- 并发更新的意义就是
交替执行不同的任务,当预留的时间不够用时,React将线程控制权交还给浏览器,等待下一帧时间到来,然后继续被中断的工作 并发模式是实现并发更新的基本前提时间切片是实现并发更新的具体手段- 上面所有的东西都是基于
fiber架构实现的,fiber为状态更新提供了可中断的能力
关于fiber,有三层具体含义
- 作为
架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为Stack Reconciler,stack 就是调用栈;在新的架构中,Reconciler(协调器)是基于fiber实现的,节点数据保存在fiber中,所以被称为fiber Reconciler。 - 作为静态
数据结构来说,每个fiber对应一个组件,保存了这个组件的类型对应的dom节点信息,这个时候,fiber节点就是我们所说的虚拟DOM。 - 作为动态
工作单元来说,fiber节点保存了该节点需要更新的状态,以及需要执行的副作用。
其他值得注意的变化
- 组件现在可以渲染
undefined:如果你从组件返回undefined,React 不会再发出告警。这使得允许的组件返回值与组件树中间允许的值能够保持一致。 - 在测试中,
act告警现在是可选的:如果你正在运行端对端的测试,act告警是非必要的。现已经引入了一个 可选 机制,这样你就可以只在有用且有益的单元测试开启它们。 - 未加载的组件取消了关于
setState的告警:之前每当你在未加载的组件中调用setState,React 就会发出内存泄漏告警。这个告警是为订阅添加的,但是人们经常在设置状态完好遇见它并且解决方法会让代码变得更加糟糕。所以 移除 了这个告警。 - 不抑制控制台打印:当你使用 Strict Mode 时,React 会将每个组件渲染两次来帮助你找到不符合预期的副作用。在 React 17 中,抑制了两次渲染之一的控制台打印让其更容易阅读。为了响应关于这会令人难以理解的 社区反馈,移除了这个抑制。如果安装了 React DevTool,第二次记录的渲染将会以灰色的文字展示并且会有一个选项(默认关闭)来抑制它们。
- 改进了内存使用:React 现在在卸载的时候会清理更多内部区域,这使得可能存在于应用代码中的未修复内存泄露的影响不那么严重。
- 不再支持IE 在本次发布中,React 正在放弃对 Internet Explorer 的支持,最终会在 2022 年 6 月 15 日完全放弃。我们现在正在做这一变更,因为 React 18 中引入的新特性是使用现代浏览器特性构建的,例如在 IE 中不能 polyfill 的微任务。如果你需要支持 Internet Explorer,我们推荐你保持在 React 17。
升级
现有项目的依赖版本号升级到最新版本,然后重装安装依赖。
npm install react react-dom
yarn add react react-dom
文档