宇宙厂:聊聊Reactv18并发机制理论与双重渲染?

前有科技后进阶 2024-06-09 17:36:56

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

1.了解 React 中的并发渲染

在 React 领域,并发渲染是一种新的渲染方式,可以帮助应用程序在重负载(Heavy Load)下保持响应,并让开发者更好地控制更新方式。

React 并发的前提是重新处理渲染过程,以便在渲染下一个视图时,当前视图能保持响应,即将渲染过程分解为可中断的工作单元。在底层实现上,React 是通过将组件渲染包装在 requestIdleCallback 调用中实现。

// 阻塞模式function renderBlocking(Component) { for (let Child of Component) { renderBlocking(Child); }}// 并发模式// React 最初使用 requestIdleCallback 后切换到 requestAnimationFrame,后来又切换到用户空间计时器function renderConcurrent(Component) { // 如果状态过时则取消渲染 if (isCancelled) return; for (let Child of Component) { // 等待浏览器不繁忙,比如:无输入操作 requestIdleCallback(() => renderConcurrent(Child)); }}

总之,并发渲染使 React 能够同时处理多个任务,而不会阻塞主线程。 这与传统的同步渲染模型形成鲜明对比,在传统的同步渲染模型中,React 会阻塞主线程,直到完成组件树的渲染。

并发渲染是为了创造流畅的用户体验,允许 React 中断正在进行的渲染过程来处理更紧急的任务,例如:响应用户输入。 这样,即使应用程序正在进行大型渲染任务,仍然可以立即响应用户交互。

import { unstable_createRoot } from 'react-dom';const root = unstable_createRoot(document.getElementById('root'));root.render(<App />);

在上面代码片段中,使用 react-dom 的 unstable_createRoot 函数来创建根节点,此功能可以实现整个应用程序的并发渲染。

并发渲染的美妙之处在于,允许 React 在内存中准备多个版本的 UI,然后根据用户交互和网络条件快速切换,从而带来更灵敏、更流畅的用户体验,特别是对于计算量或网络请求量较大的复杂应用程序。

但是值得一提的是,并发模式目前是在 React Experimental 中引入的。 是一项激进的新功能,需要进行广泛的测试和反馈才能包含在稳定版本中。

2.并发模式与 React 16 有何不同

随着 React 18 的出现,React 团队引入了一种新的并发渲染器,其与 React 16 及之前版本中使用的同步渲染器有根本的不同。

在 React 16 中,渲染始终是同步的。这意味着一旦 React 开始渲染组件树就不会停止,直到渲染完成整个树。 如果大型渲染任务阻塞主线程,这可能会导致 UI 冻结并变得无响应。

另一方面,React 18 引入了并发渲染。 这种新模式使 React 能够中断渲染过程来处理更紧急的任务,例如:响应用户输入。

比如下面的示例:

// React 16ReactDOM.render(<App />, document.getElementById('root'));// React 18const root = ReactDOM.createRoot(document.getElementById('root'));root.render(<App />);

通过并发模式,React 18 还引入了许多新功能,例如:自动批处理多个状态更新、流式服务器渲染(Streaming Server Rendering)和 React Suspense,使开发者可以为组件创建平滑的加载状态。

从本质上讲,React 18 中的并发模式是一种范式转变,它改变了我们对 React 渲染的看法。 不仅让应用程序更快,而且其响应更快且用户友好。

3.React 18 中的并发特征

React 18 包含大量并发功能,旨在使应用程序更具响应性和用户友好性。相反,React 团队转向并发功能,这是一组有选择地启用并发渲染的新 API。到目前为止,React 引入了两个新的钩子来选择并发渲染。

3.1 自动批处理(Automatic Batching)

在 React 的早期版本中,事件之外触发的状态更新不会批量处理,从而可能会导致不必要的重新渲染和应用程序效率降低。 React 18 引入了自动批处理,它将多个状态更新一起批处理,从而产生一次重新渲染。

const [count, setCount] = useState(0);// In React 16, these would cause two re-renderssetCount(count + 1);setCount(count + 1);// In React 18, these are batched together and cause a single re-render3.2 Streaming Server Rendering

React 18 引入了新的服务器渲染 API,允许开发者将 HTML 从服务器流式传输到客户端。 这意味着浏览器可以在收到第一个数据块后立即开始渲染 HTML,而无需等待整个响应,从而显著提高应用程序的感知加载时间。

3.3 React Suspense

Suspense 是 React 18 中的一项新功能,可让开发者为组件创建平滑的加载状态。

使用 Suspense,开发者可以“等待”某些代码加载并以声明方式指定加载状态,这在处理代码分割或数据获取时特别有用。

<Suspense fallback={<div>Loading...</div>}> {/*fallback默认占位组件*/} <MyComponent /></Suspense>

React 18 还有 startTransition,其允许将某些更新标记为“transitions”并给予较低的优先级,还有 useDeferredValue 允许推迟较慢的状态更新,直到更快的状态更新完成。

这些并发功能使 React 能够更有效地处理复杂的渲染任务,并提供更流畅、响应更灵敏的用户体验。

4.并发渲染示例示例 1:确定用户输入的优先级

并发模式的主要优点之一是能够将某些任务标记为优先于其他任务。 假设应用程序中有一个搜索栏, 当用户在搜索栏中输入内容时,应用程序会根据用户的输入过滤项目列表。

在传统的同步渲染模型中,如果项目列表很大,过滤列表可能会阻塞主线程并导致输入栏中内容滞后于用户输入,从而影响用户体验。

通过并发模式,React 可以中断过滤过程来处理用户的输入,确保输入始终保持响应。

示例 2:具有 Suspense 的平滑加载状态

并发模式的另一个强大功能是 React Suspense,它允许开发者为组件创建平滑的加载状态。

假设有一个从 API 获取数据的组件。 在传统模型中必须手动管理加载状态,如果操作不当,可能会导致用户体验不佳。

使用 Suspense,可以声明性地指定加载状态,并让 React 处理其余的事情,从而带来更流畅的用户体验,因为 React 可以“等待”数据加载并同时显示加载状态。

<Suspense fallback={<div>Loading...</div>}> <DataFetchingComponent /></Suspense>

在上面示例中,如果 DataFetchingComponent 仍在获取数据,React 将显示 fallback 属性。

5.React 18 处理并发选项

React 18 的并发模式彻底改变了应用程序处理并发的方式,其引入了一个新的并发渲染器,可以同时处理多个任务,而不会阻塞主线程。 与之前的同步渲染器相比,这是一个重大改进,之前的同步渲染器一次只能处理一个任务。

但是 React 如何处理并发呢? 秘密在于 React 安排更新的方式。 在并发模式下,React 可以中断正在进行的渲染过程来处理更紧急的任务,例如:响应用户输入,可以确保应用程序始终保持响应,即使在繁重的计算或网络请求期间也是如此。

React 团队目前提供了一组选择启用并发渲染的新 API,比如 useTransition、useDeferredValue。

useTransition

useTransition Hook 返回值包括:

布尔标志位 isPending:如果正在进行并发渲染则为 true函数 startTransition:调度一个新的并发渲染,要使用它请将 setState 调用包装在 startTransition 回调中。function MyCounter() { const [isPending, startTransition] = useTransition(); const [count, setCount] = useState(0); // 依赖项为空 const increment = useCallback(() => { startTransition(() => { // 并发渲染 setCount((count) => count + 1); }); }, []); return ( <> <button onClick={increment}>Count {count}</button> <span>{isPending ? 'Pending' : 'Not Pending'}</span> {/*需要并发渲染的组件*/} <ManySlowComponents count={count} /> </> );}

从概念上讲,状态更新会检测是否包含在 startTransition 中,以决定是阻塞渲染还是并发渲染。

function startTransition(stateUpdates) { isInsideTransition = true; stateUpdates(); isInsideTransition = false;}function setState() { if (isInsideTransition) { // 并发渲染 } else { // 阻塞渲染 }}

提醒一下,useTransition 不能用于受控 input,可以用 useDeferredValue 替代。这是因为 Transition 是非阻塞的,但响应 onchange 事件而更新 input 应该同步发生。 如果想用 Transition 来响应输入有两种选择:

声明两个单独的状态变量:一个用于输入状态(始终同步更新),另一个在 Transition 中更新,从而开发者可以使用同步状态控制输入,并将 Transition 状态变量传递给渲染逻辑的其余部分。用一个状态变量,并添加 useDeferredValue:但是会“落后于”实际值,但是将触发非阻塞重新渲染以自动“赶上”新值。useDeferredValue

seDeferredValue Hook 适用于无法在 startTransition 中包装状态更新但仍希望并发运行更新的情况。

发生这种情况的一个示例是子组件从父组件接收新值。从概念上讲,useDeferredValue 是一种去抖动 Effect:

function useDeferredValue<T>(value: T) { const [isPending, startTransition] = useTransition(); const [state, setState] = useState(value); useEffect(() => { // 当input发生变化的时候进行并发更新 startTransition(() => { setState(value); }); }, [value]); return state;}

使用方式与 input 防抖钩子相同:

function Child({ value }) { const deferredValue = useDeferredValue(value); // ...}6.React 18 双重渲染之谜

React 18 并发模式最有趣的现象之一是所谓的“双重渲染(Double Render)”。 如果有尝试过 React 18,可能已经注意到某些组件似乎渲染了两次,即使确实应该只渲染一次。

双重渲染实际上是一个功能,而不是一个错误,其是新的并发渲染器工作方式的结果。 在并发模式下,React 在更新 DOM 之前在内存中准备新视图(组件更新后的状态),称为“内存中渲染”或“离屏渲染”。

在此阶段,React 可能会渲染某些组件一次或多次,即使最终对用户不可见,这就是导致“双重渲染”的原因。 但是,这些额外的渲染通常速度很快并且不会阻塞主线程,因此不会影响应用程序的性能。

function MyComponent() { const [state, setState] = useState(initialState); console.log('Render'); // ...}

以上示例如果在并发模式下运行,可能会看到“Render”在控制台中打印两次,即使该组件仅更新一次。

因此,如果看到组件在并发模式下渲染两次无需惊讶, React 只是在内存中准备新视图以提供更流畅的用户体验。

参考资料

https://www.dhiwise.com/post/deep-dive-into-react-concurrent-mode-exploring-key-features-and-use-cases

https://www.bbss.dev/posts/react-concurrency/

https://codesandbox.io/s/custom-usedeferredvalue-whv71y?file=/AppDeferred.js:550-566

https://react.dev/reference/react/useTransition

0 阅读:1

前有科技后进阶

简介:感谢大家的关注