async function handleSubmit() {
setPending(true)
await post('/someapi') // component might unmount while we're waiting
setPending(false)
}
在上面的代码中,如果发送请求时,组件卸载了,会抛出警告。
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
* React mounts the component.
* Layout effects are created.
* Effects are created.
* React simulates unmounting the component.
* Layout effects are destroyed.
* Effects are destroyed.
* React simulates mounting the component with the previous state.
* Layout effects are created.
* Effects are created.
在 2021 年 6 月 8 号,React 公布了 v18 版本的发布计划,并发布了 alpha 版本。经过将近一年的发布前准备,在 2022 年 3 月 29 日,React 18 正式版终于和大家见面了。
React 18 应该是最近几年的一个重磅版本,React 官方对它寄予了厚望。不然也不会将 React 17 作为一个过渡版本,也不会光发布准备工作就做了一年。
在过去一年,我们已经或多或少了解到一些 React 18 的新功能。这篇文章我会通过丰富的示例,向大家系统的介绍 React 18 带来的改变。当然本文融入了很多个人理解,如有不对,烦请指正。
Concurrent Mode
Concurrent Mode(以下简称 CM)翻译叫并发模式,这个概念我已经听了好多年了,并且一度非常担忧
随着对 CM 的了解,我发现它其实是人畜无害的。
CM 本身并不是一个功能,而是一个底层设计,它使 React 能够同时准备多个版本的 UI。
在以前,React 在状态变更后,会开始准备虚拟 DOM,然后渲染真实 DOM,整个流程是串行的。一旦开始触发更新,只能等流程完全结束,期间是无法中断的。
在 CM 模式下,React 在执行过程中,每执行一个 Fiber,都会看看有没有更高优先级的更新,如果有,则当前低优先级的的更新会被暂停,待高优先级任务执行完之后,再继续执行或重新执行。
CM 模式有点类似计算机的多任务处理,处理器在同时进行的应用程序之间快速切换,也许 React 应该改名叫 ReactOS 了。
这里举个例子:我们正在看电影,这时候门铃响了,我们要去开门拿快递。 在 React 18 以前,一旦我们开始看电影,就不能被终止,必须等电影看完之后,才会去开门。 而在 React 18 CM 模式之后,我们就可以暂停电影,等开门拿完快递之后,再重新继续看电影。
不过对于普通开发者来说,我们一般是不会感知到 CM 的存在的,在升级到 React 18 之后,我们的项目不会有任何变化。
我们需要关注的是基于 CM 实现的上层功能,比如 Suspense、Transitions、streaming server rendering(流式服务端渲染), 等等。
React 18 的大部分功能都是基于 CM 架构实现出来的,并且这这是一个开始,未来会有更多基于 CM 实现的高级能力。
startTransition
我们如果要主动发挥 CM 的优势,那就离不开 startTransition。
React 的状态更新可以分为两类:
我以前会认为,CM 模式会自动帮我们区分不同优先级的更新,一键无忧享受。很遗憾的是,CM 只是提供了可中断的能力,默认情况下,所有的更新都是紧急更新。
这是因为 React 并不能自动识别哪些更新是优先级更高的。
比如以上示例,用户的键盘输入操作后,
setInputValue
会立即更新用户的输入到界面上,是紧急更新。而setSearchQuery
是根据用户输入,查询相应的内容,是非紧急的。但是 React 确实没有能力自动识别。所以它提供了
startTransition
让我们手动指定哪些更新是紧急的,哪些是非紧急的。如上代码,我们通过
startTransition
来标记一个非紧急更新,让该状态触发的变更变成低优先级的。光用文字描述大家可能没有体验,接下来我们通过一个示例来认识下可中断渲染对性能的爆炸提升。
示例页面:https://react-fractals-git-react-18-swizec.vercel.app/
如下图,我们需要画一个毕达哥拉斯树,通过一个 Slider 来控制树的倾斜。
那我们的代码会很简单,如下所示,我们只需要一个
treeLean
state 来管理状态。在每次 Slider 拖动后,React 执行流程大致如下:
每一次用户拖动 Slider,都会同步执行上述三步。但当树的节点足够多的时候,Pythagoras 渲染一次就非常慢,就会导致 Slider 的 value 回填变慢,用户感觉到严重的卡顿。如下图。
当数的节点足够大时,已经卡到爆炸了。在 React 18 以前,我们是没有什么好的办法来解决这个问题的。但基于 React 18 CM 的可中断渲染机制,我们可以将树的更新渲染标记为低优先级的,就不会感觉到卡顿了。
以上代码,我们通过 startTransition 标记了非紧急更新,让树的更新变成低优先级的,可以被随时中止,保证了高优先级的 Slider 的体验。
此时更新流程变为了
React 会在高优先级更新渲染完成之后,才会启动低优先级更新渲染,并且低优先级渲染随时可被其它高优先级更新中断。
当然,在低优先状态等待更新过程中,如果能有一个 Loading 状态,那就更好了。React 18 提供了
useTransition
来跟踪 transition 状态。自动批处理 Automatic Batching
批处理是指 React 将多个状态更新,聚合到一次 render 中执行,以提升性能。比如
在 React 18 之前,React 只会在事件回调中使用批处理,而在 Promise、setTimeout、原生事件等场景下,是不能使用批处理的。
而在 React 18 中,所有的状态更新,都会自动使用批处理,不关心场景。
如果你在某种场景下不想使用批处理,你可以通过
flushSync
来强制同步执行(比如:你需要在状态更新后,立刻读取新 DOM 上的数据等。)React 18 的批处理在绝大部分场景下是没有影响,但在 Class 组件中,如果你在两次 setState 中间读取了 state 值,会出现不兼容的情况,如下示例。
当然你可以通过
flushSync
来修正它。流式 SSR
SSR 一次页面渲染的流程大概为:
在传统的 SSR 模式中,上述流程是串行执行的,如果其中有一步比较慢,都会影响整体的渲染速度。
而在 React 18 中,基于全新的 Suspense,支持了流式 SSR,也就是允许服务端一点一点的返回页面。
假设我们有一个页面,包含了 NavBar、Sidebar、Post、Comments 等几个部分,在传统的 SSR 模式下,我们必须请求到 Post 数据,请求到 Comments 数据后,才能返回完整的 HTML。
但如果 Comments 数据请求很慢,会拖慢整个流程。
在 React 18 中,我们通过
Suspense
包裹,可以告诉 React,我们不需要等这个组件,可以先返回其它内容,等这个组件准备好之后,单独返回。如上,我们通过
Suspense
包裹了Comments
组件,那服务器首次返回的 HTML 是下面这样的,<Comments />
组件处通过loading
进行了占位。当
<Comments />
组件准备好之后,React 会通过同一个流(stream)发送给浏览器(res.send 替换成 res.socket),并替换到相应位置。更多关于流式 SSR 的讲解可见:https://github.com/reactwg/react-18/discussions/37
Server Component
Server Component 叫服务端组件,目前还在开发过程中,没有正式发布,不过应该很快就会和我们见面的。 Server Component 的本质就是由服务端生成 React 组件,返回一个 DSL 给客户端,客户端解析 DSL 并渲染该组件。
Server Component 带来的优势有:
假设我们有一个 markdown 渲染组件,以前我们需要将依赖
marked
和sanitize-html
打包到 JS 中。如果该组件在服务端运行,则最终返回给客户端的是转换完成的文本。当然说了这么多好处,Server Component 肯定也是有一些局限性的:
OffScreen
OffScreen 目前也在开发中,会在未来某个版本中发布。但我们非常有必要提前认识下它,因为你现在的代码很可能已经有问题了。
OffScreen 支持只保存组件的状态,而删除组件的 UI 部分。可以很方便的实现预渲染,或者 Keep Alive。比如我们在从 tabA 切换到 tabB,再返回 tabA 时,React 会使用之前保存的状态恢复组件。
为了支持这个能力,React 要求我们的组件对多次安装和销毁具有弹性。那什么样的代码不符合弹性要求呢?其实不符合要求的代码很常见。
在上面的代码中,如果发送请求时,组件卸载了,会抛出警告。
所以我们一般都会通过一个
unmountRef
来标记当前组件是否卸载,以避免所谓的「内存泄漏」。我们来模拟执行一次组件,看看组件的变化状态:
在 OffScreen 中,React 会保存住最后的状态,下次会用这些状态重新渲染组件。惨了,此时我们发现重新渲染组件一直在 loading。
怎么解决?解决办法很简单,就是回归最初的代码,删掉
unmountRef
的逻辑。至于「内存泄漏」的警告,React 18 删除了,因为这里不存在内存泄漏(参考:https://mp.weixin.qq.com/s/fgT7Kxs_0feRx4TkBe6G5Q)。为了方便排查这类问题,在 React 18 的 Strict Mode 中,新增了 double effect,在开发模式下,每次组件初始化时,会自动执行一次卸载,重载。
这里还是要再提示下:开发环境,在 React 18 的严格模式下,组件初始化的 useEffect 会执行两次,也就是可能 useEffect 里面的请求被执行了两次等。
新 Hooks
useDeferredValue
useDeferredValue 可以让一个 state 延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValue 和 startTransition 一样,都是标记了一次非紧急更新。
之前 startTransition 的例子,就可以用
useDeferredValue
来实现。useId
支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免
hydration
的不兼容。原理是每个 id 代表该组件在组件树中的层级结构。useSyncExternalStore
useSyncExternalStore 能够让 React 组件在 Concurrent Mode 下安全地有效地读取外接数据源。 在 Concurrent Mode 下,React 一次渲染会分片执行(以 fiber 为单位),中间可能穿插优先级更高的更新。假如在高优先级的更新中改变了公共数据(比如 redux 中的数据),那之前低优先的渲染必须要重新开始执行,否则就会出现前后状态不一致的情况。 useSyncExternalStore 一般是三方状态管理库使用,一般我们不需要关注。
useInsertionEffect
这个 Hooks 只建议
css-in-js
库来使用。 这个 Hooks 执行时机在 DOM 生成之后,useLayoutEffect 生效之前,一般用于提前注入<style>
脚本。如何升级到 React 18
参考:https://mp.weixin.qq.com/s/2QYEmFlIIMQkXR-Q9DVG2Q