这里介绍一下React18的4个新特性:
在 React 中使用setState来进行dispatch组件的State变化,当setState在组件中被调用后,并不会立即触发重新渲染。React 会执行全部事件处理函数,然后触发一个单独的re-render,合并所有的更新。
比如在点击+1的例子中,如果方法里连续触发三次setState,最终React会将更新函数放到一个队列里,然后合并队列触发setState的re-render,这就是batching的含义。
这样既可以减少程序数据状态存在中间值导致的不稳定性,也可以提高渲染性能。
在React 18 之前,如果在回调函数的异步调用中,执行setState,由于丢失上下文,无法做合并处理,所以每次setState调用都会触发一次re-render。
function handleClick() {
// React 18 之前的版本
(/*...*/).then(() => {
setCount(c => c + 1); // 立刻重新渲染
setShow(show => !show); // 立刻重新渲染
});
}
而 React 18中,任何情况下都可以合并渲染!
如果仍然希望setState之后立即重新渲染,只需要使用flushSync
包裹。
function handleClick() {
// React 18
fecth(/*...*/).then(() => {
ReactDOM.flushSync(() => {
setCount(c => c + 1); // 立刻重新渲染
setFlag(f => !f);
})
})
}
Automatic batching 机制让我们有能力对渲染顺序和节奏进行一些基础的把控。例如在Canvas画布编辑场景中,我们可以加载完主节点框架之后立刻进行渲染,而每个节点的内容则可以进行合并渲染,尽可能加快用户看到可编辑页面的时间,同时避免http异步函数引起的频繁渲染的性能开销。
官方明确指出了React 18 中并不存在 Concurrent Mode,只有用于并发渲染的并发新特性,开发者希望能够在Web Platform引入并发渲染,来实现多个渲染任务的并行渲染,其中Suspense就是基于此诞生的。
React18支持并发特性的三个API
import { startTransition } from 'react';
// 紧急更新
setInputValue(input)
// 标记回调函数内的更新为 非紧急更新
startTransition(() => {
setSearchQuery(input)
})
所以,startTransition的作用就是:被startTransition包裹的setState触发的渲染被标记为不紧急渲染,意味着它们可以被其他紧急渲染所抢占,这种渲染优先级的调整手段可以帮助我们解决各种性能伪瓶颈,提升用户体验。
这个hook适用于设置延迟值
function Page() {
const [filters, mergeFilter] = useMergeState(defaultFilters);
const deferedFilters = React.useDeferredValue(filters);
return (
<>
<Filters filters={filters} />
<List filters={deferedFilters}>
</>
)
}
useDeferredValue() 会将List组件的渲染变得更加平滑,深层次看来是 defered value 引起的渲染会被标记为不紧急渲染,会被filters引起的渲染进行抢占,进而达到用户快速输入搜索等场景下页面抖动或卡顿问题。
早在2018年,React就推出了Suspense的基础版本。可以在客户端配合React.lazy动态加载代码,实现数据拉取和状态控制的关注点分离(即当子组件未加载完成时,父组件填充fallback声明的组件),但是不能在服务器端进行加载。
<Suspense fallback={<Loading />}>
<Header />
<Suspense fallback={<ListPlaceholder />}>
<ListLayout />
</Suspense>
</Suspense>
在React18中,Suspense可以运行在服务器端,Server Rendering的性能不再受制于性能最差的组件(木桶效应)。
在React 18 之前,Server Rendering的流程就是服务端请求所有数据,然后发送HTML到客户端或者说浏览器,然后由客户端的hydrate内容(可以搜索同构渲染进行学习),每个环节必须按部就班的执行。当Suspense可以在服务器端使用之后,一旦某个组件加载慢,就可以将fallback的内容传输到客户端(例如loading态),保证用户可以尽可能早的可进行交互。
更加优秀的是,hydrate可以通过用户的行为来调整优先级。例如Profile组件和正在Loading的组件同时处于Suspense的流程中,此时用户点击评论组件,React将会优先hydrate评论组件,尽可能优先满足用户交互体验。
回归到代码实现细节,整体框架上服务器和客户端的连接必然趋向于持续性的长连接,因此res.send需要变为res.socket,pipeToNodeWritable替换renderToString并配合Suspense即可。
新的更友好的语义化render方式。
const container = document.getElementById('app');
// 旧的render API
ReactDOM.render(<App />, container);
// 新的 createRoot API
const root = ReactDOM.createRoot(container);
root.render(<App />);
Client 端提供了 新 水合 Hydrate API
const root = ReactDOM.createRoot(container, <App tab="home" />)
以及新的 useId() API来为组件生成唯一ID。
由于Suspense和并发渲染在React 18的大规模使用,一些具有External stores的API比如全局变量、document对象如何在并发场景下保持一致性呢?
React 18 提供了useSyncExternalStore这个hook,来保证External stores的一致性。
useSyncExternalStore(
// 注册回调函数
subscribe: (callback) => Unsubscribe,
// 获取快照函数
getSnapshot: () => state
) => state
具体使用方式例子:
const store = {
state: { count: 0 },
setState: (fn) => {
store.state = fn(store.state); // 需要不可变的更新
store.listeners.forEach(listener => listender());
},
listeners: new Set(),
subscribe: (callback) => {
store.listeners.add(callback);
return () => store.listeners.delete(callback);
},
getSnapshot: () => {
const snap = Object.freeze(store.state);
return snap;
}
}
// use external store
const Component = () => {
const snap = useSyncExternalStore(store.subscribe, store.getSnapshot);
return <div>{snap.count}</div>
}