(写这篇文章的时候,由React18 还没正式发布太多的文档,很多概念和内容是我从多个来源拼凑而来,里面包含了很多我个人的理解,可能到 React18 正式发布的时候,会有些许错误,写这个文章仅仅是满足一下猎奇心理。所以如果你是在未来的某个时间内看到这篇文章,你记得去阅读官网的内容,并以官网的内容为主。)
其实 React18@alpha 已经发布有一段时间了,因为我最近分到一个调研-- “UMI 如何支持 react@18 alpha”。(要不然,我应该会继续蹲。)
所以就开始看了看相关的文档和新闻。
比较好的消息是,你可以非常平滑的升级到 React18。
比如在 umi 中,抛开测试和兼容之类的代码,仅仅只需要修改一行代码就可以支持。
- import { hydrate, render } from 'react-dom'; + import ReactDOM, { hydrate, render } from 'react-dom'; - if (window.g_useSSR) { - hydrate(rootContainer, rootElement, callback); - } else { - render(rootContainer, rootElement, callback); - } + reactRoot = (ReactDOM as any).createRoot(rootElement, { + hydrate: window.g_useSSR, + }); + reactRoot.render(rootContainer);
并且改完从业务侧,页面代码中都无需做任何修改项目就可以正常的运行。接着你就可以通过选择性的添加 React18 的新特性到你的某些新页面或者优化某些场景。
(看到这里不得不吐槽一下 React Native,发一个小版本都前后不兼容啊!)
开箱即用
当你简单的更改了类似上面的代码,你将直接享受到 React18 开箱即用的一些功能。
- 自动批处理以减少渲染
- Suspense 的 SSR 支持(全新的 SSR 架构)
自动批处理以减少渲染
批处理使 React 将多个状态更新分组到单个重新渲染中以获得更好的性能。这个特性在现在的 React17 中就已经有了,但是在不同的上下文中交互的时候,将不支持批处理。现在 React18 中,增加了对网络请求,promise、setTimeout等事件的自动批处理支持。
React17 - Updates inside of promises, setTimeout, native event handlers, or any other event were not batched in React by default.
function App() { const [count, setCount] = useState(0); const [flag, setFlag] = useState(false); function handleClick() { fetchSomething().then(() => { // React 18 and later DOES batch these: setCount(c => c + 1); setFlag(f => !f); // React will only re-render once at the end (that's batching!) }); } return ( <div> <button onClick={handleClick}>Next</button> <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1> </div> ); }
Suspense 的 SSR 支持
之前在服务端渲染上对 Suspense 的支持不是很好,现在你可以通过使用新的 API pipeToNodeWritable 来使用全新的 SSR 架构。
比如我们期望最终打开的页面如下:(绿色代表用户可交互)
(图片来自React18 工作组 discussions 37)
当你没有使用 SSR 功能时,我们的页面都会在页面启动的时候,经历短暂的白屏阶段,这是由于此时浏览器发起了对 js 的请求和加载,页面之后等待这些 js 文件被下载完成之后,才会被执行,渲染出页面。
当使用 SSR 时,React 会在服务端将组件渲染成 Html 并发送给用户,此时用户将会看到页面的基本框架,只能进行一些内置 Web 交互(如链接和表单输入)。
(此处,灰色说明屏幕的这些部分尚未完全交互)
之后浏览器照常加载 js 文件,当 js 文件加载完成之后,通过水合过程,将当前的 html 渲染成可交互的。
这个过程的专业名词是 “Hydrate”,在 vue 中被翻译成“激活”,React 社区更期望称之为“水合”或者“注水”来描述这个过程,就相当于将“水”注入到“干”的 Html 中。
这里也存在一个和不使用 SSR 一样的问题,当 js 被全部加载,页面组件被全部“水合”之前,页面研究是不可交互的。React18 就支持,让部分的页面优先加载完数据,优先执行水合。你的页面看起来大概是这样子的。
并发渲染(concurrent rendering)
当然你也可以选择性的使用 React18 的一些新功能,React18 加入了一个主要的可选机制,“并发渲染(concurrent rendering)”,这个是很多新功能的基础。值得注意的是这里的“并发”使用的依旧是单线程。但是这个单线程可以被中断的。因此渲染可以在多个渲染任务之间交错进行,如用户交互,网络请求,计时器,动画和浏览器布局/绘制等等。它的主要工作分配大致如下:
当渲染任务遭遇到更高级的渲染任务时将会被中断,然后优先执行优先级更高的任务,再任务完成之后,再回到原来的渲染任务上。
你可以通过一下一些新的 API 来告诉 React 哪些是优先级较高的任务。
- startTransition
- useDeferredValue
- SuspenseList
startTransition 过渡更新
这是一个比较好理解的 API,通过使用 startTransition 来包裹一些 setState,声明他们是比较不重要的渲染行为。比如官方的例子里面提到的搜索场景。
当用户在搜索框中输入时,搜索框需要实时的显示用户的输入字符,然后通过网络请求(或者本地过滤数据),获取到新的列表数据,再更新列表。这里面会有两个 setState
,一个是 input 的 value
绑定。一个是搜索之后的页面数据绑定。
import { startTransition } from 'react'; // 紧急:显示输入的内容 setInputValue(input); // 将内部的任何状态更新标记为转换 startTransition(() => { // Transition: 显示结果 setSearchQuery(input); });
如果你需要在等待过渡渲染的时候执行一些表现,如 loading 操作之类的。你可以使用 useTransition
import { useTransition } from 'react'; const [isPending , startTransition] = useTransition(); startTransition(() => { // Transition: 显示结果 setSearchQuery(input); }); { isPending && < Spinner /> }
useDeferredValue
推迟更新屏幕上不太重要的部分(这个还没有放出来文档)。
SuspenseList
协调加载指示器出现的顺序(这个还没有放出来文档)。
但是从 Suspense 的用法来看,预计与懒加载的优先级有关,可能是指定哪些加载优先执行。(我猜的,毕竟新的文档几乎都是这个概念。)
// 该组件是动态加载的 const OtherComponent = React.lazy(() => import('./OtherComponent')); function MyComponent() { return ( // 显示 <Spinner> 组件直至 OtherComponent 加载完成 <React.Suspense fallback={<Spinner />}> <div> <OtherComponent /> </div> </React.Suspense> ); }
其他
除了上面提到的这些和协作多任务、基于优先级的渲染、调度和中断等有关的功能之外,还有值得关注的特性。
StrictMode 严格模式
你的组件将会被多次调用加载-卸载,以确保他们的状态正确。
React intentionally double-invokes effects (mount -> unmount -> mount) for newly mounted components.
值得关注的是,这个特性是被默认开启的,其实现在的React中就已经有使用这个功能了。--- 快速刷新(Fast Refresh),开发时可以保持组件状态,同时编辑提供即时反馈。
Offscreen
新的 Offscreen API 允许 React 通过隐藏组件而不是卸载组件来保持这样的状态。为此,React 将调用与卸载时相同的生命周期钩子——但它也会保留 React 组件和 DOM 元素的状态。这就是 React 中的 keepalive 功能啊。
这是我所期待的一个能力,现在是使用
实现。
当然它还可以用作预渲染页面,提前渲染用户即将到达的页面,有点类似 Next 中用 Link 链接的页面,会被提前渲染。
在优先级方面,Offscreen 是最低的,理论上它会被任何其他的渲染任务中断。
it will not be in the initial 18.0 release, but may come in an 18.x minor.
感谢阅读,有任何疑问可以通过评论一起讨论。喜欢这个文章的朋友,请给一个赞,喜欢我的朋友,可以关注一下我。感谢三连。
参考链接
https://zh-hans.reactjs.org/blog/2021/06/08/the-plan-for-react-18.html
https://github.com/reactwg/react-18/discussions/4
https://github.com/reactwg/react-18/discussions/21
https://github.com/reactwg/react-18/discussions/22
https://github.com/reactwg/react-18/discussions/27
https://github.com/reactwg/react-18/discussions/37
https://www.youtube.com/watch?v=bpVRWrrfM1M