react18新特性及实践总结
react18新特性
一、 Render API
三种入口模式
legacy 模式: ReactDOM.render(, rootNode)。没有开启新功能,这是react17采用的默认模式。
blocking 模式: ReactDOM.createBlockingRoot(rootNode).render()。作为迁移到concurrent 模式的过渡模式。
concurrent 模式: ReactDOM.createRoot(rootNode).render()。这个模式开启了所有的新功能。
react18正式迁移到了concurrent 模式,同时,用户也可以继续使用react17下的旧API(但是会有警告提示)。
// React 17
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
const root = document.getElementById('root')!;
ReactDOM.render(<App />, root);
// React 18
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = document.getElementById('root')!;
ReactDOM.createRoot(root).render(<App />);
二、 Automatic Batching
批处理是 react 将多个状态更新分组到一个渲染中以获得更好的性能。react18 之前只能在react 事件处理程序中批处理更新。默认情况下,Promise、setTimeout、本机事件处理程序或任何其他事件内部的更新不会在 React 中批处理。使用自动批处理,这些更新将自动批处理:
//示例一:react17会render两次,react18只需要render一次
const handleClick = async () => {
Promise.resolve().then(() => {
setC1((c) => c + 1);
});
setC2((c) => c + 1);
};
//示例二:react18需要render两次
const handleClick = async () => { await setC1((c) => c + 1); //提升到同步优先级,类似flushSync
setC2((c) => c + 1);
};
react17-demo:react17-demo - CodeSandbox
react18-demo:react18-demo - CodeSandbox
那么,如果我不想要批处理呢?
flushSync
官方提供了一个 API flushSync
用于退出批处理
function handleClick() {
flushSync(() => {
setC1((c) => c + 1);
}); setC2((c) => c + 1);
}
flushSync
会以函数为作用域,函数内部的多个 setState
仍然为批量更新,这样可以精准控制哪些不需要的批量
实现
自动批处理的实现在React18中是基于优先级的,用lane来进行优先级的控制。先简单介绍一下lane。
lane 是一个表示 priority 的一个东西,它通过二进制位来表示。优先级最高的 SyncLane 为 1,其次为 2、4、8 等等,所有 lane 的定义可参考源码。react通过 lanes 表达批量更新。lanes 是一个整数,该整数所有二进制位为 1 对应的优先级任务都将被执行。例如 lanes 为 17 (10001)时,表示将异步并行更新 SyncLane(值为 1)和 DefaultLane(值为 16)的任务。
个人理解:lane用来弥补expirationTime 的缺陷,它首先说明这个任务是个什么任务(确定优先级,确定lane值) ,其次说明哪些任务应该被 batching 到一起做(lane相同即batching 到一起做) 。然后通过lanes确定哪些并行更新。
下面结合源码看一下批量更新的实现,该方法是18中每一次更新调度的必经之路,批处理的实现的核心在于当相同优先级的更新发生时,并不会生成新的任务,而是复用上一次的任务,从而实现合并。
为了便于理解,对源码做了一定程度的简化,下同
function ensureRootIsScheduled(root, currentTime) {
......
// Determine the next lanes to work on, and their priority.
var nextLanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
// This returns the priority level computed during the `getNextLanes` call.
var newCallbackPriority = returnNextLanesPriority();
// Check if there's an existing task. We may be able to reuse it.
if (existingCallbackNode !== null) {
var existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) { // The priority hasn't changed. We can reuse the existing task. Exit. return ; }
// The priority changed. Cancel the existing callback. We'll schedule a new
// one below.
cancelCallback(existingCallbackNode);
}
// Schedule a new callback.
var newCallbackNode;
......
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
} // This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
再看看FlushSync
的实现:
export function flushSync(fn) {
try {
// DiscreteEventPriority === SyncLane
setCurrentUpdatePriority(DiscreteEventPriority);
fn && fn();
} finally {
setCurrentUpdatePriority(previousPriority);
}
}
其实是将内部更新的优先级强制指定为SyncLane
,即指定为同步优先级,具体效果就是每一次更新时都会同步的执行渲染。
三、Transitions
过渡是 React 18中的一个新概念,用于区分紧急和非紧急更新。
紧急更新反映了直接交互,例如键入、单击、按下等。
非紧急(过渡)更新将 UI 从一个视图转换到另一个视图。
打字、点击或按下等紧急更新需要立即响应,以符合我们对物理对象行为方式的直觉。否则用户会觉得“不对劲”。但是,过渡是不同的,因为用户不希望在屏幕上看到每个中间值。
下面我们来看一个例子:当滑块滑动时,下方的图表会一起更新,然而图表更新是一个CPU密集型操作,比较耗时。由于阻塞了渲染导致页面失去响应,用户能够非常明显的感受到卡顿。
实际上,当我们拖动滑块的时候,需要做两次更新:
// Urgent: Show what was typed
setSliderValue(input);
// Not urgent: Show the results
setGraphValue(input);
startTransition
包装在 startTransition 中的更新被视为非紧急更新,如果出现更紧急的更新(如点击或按键),则会中断。
import { startTransition } from 'react';
// Urgent
setSliderValue(input);
// Mark any state updates inside as transitions
startTransition( () => {
// Transition: Show the results
setGraphValue(input);
});
使用后可以明显的感受到,虽然图表的更新还是会有些延迟,但是整体的用户体验相对之前是非常好的。
useTransition
一般情况下,我们可能需要通知用户后台正在工作。为此提供了一个带有 isPending
转换标志的 useTransition
,React
将在状态转换期间提供视觉反馈,并在转换发生时保持浏览器响应。
import { useTransition } from 'react';
const [isPending, startTransition] = useTransition();
return isPending && <Spin />
useDeferredValue
返回一个延迟响应的值,可以让一个state
延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValue
和 useTransition
一样,都是标记了一次非紧急更新。
import { useDeferredValue } from 'react';
const deferredValue = useDeferredValue(value);
useDeferredValue
与 useTransition
其实挺相似的:
- 相同:
useDeferredValue
本质上和内部实现与useTransition
一样都是标记成了非紧急更新任务。 - 不同:
useTransition
是把更新任务变成了延迟更新任务,而useDeferredValue
是产生一个新的值,这个值作为延时状态。
同 debounce
的区别:
debounce
即 setTimeout
总是会有一个固定的延迟,而 useDeferredValue
的值只会在渲染耗费的时间下滞后,在性能好的机器上,延迟会变少,反之则变长。
实现
// startTransition
function startTransition(setPending, callback, options) {
......
setPending(true);
var prevTransition = ReactCurrentBatchConfig$2.transition;
// start transition
ReactCurrentBatchConfig$ 2. transition = {};
var currentTransition = ReactCurrentBatchConfig$2.transition;
try {
setPending(false);
callback();
} finally {
// Recovery
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig$2.transition = prevTransition;
}
}
}
export function requestUpdateLane(fiber: Fiber) {
// ...
// requestCurrentTransition => ReactCurrentBatchConfig.transition
const isTransition = requestCurrentTransition() !== null ;
if (isTransition) {
return claimNextTransitionLane();
}
// ...
}
startTransition
内部时会使用一个全局变量ReactCurrentBatchConfig$2.transition作为是否开启transition的开关,后边的setPending(false)和callback()在触发dispatchAction()的时候会调用requestUpdateLane,requestUpdateLane返回的是isTransition任务。优先级下降,因此执行时间要比普通更新晚,同时即使更新发生时,也可以被高优先级的更新打断,从而不阻塞用户渲染。
// useTransition
function mountTransition() {
var _mountState = mountState(false),
isPending = _mountState[0],
setPending = _mountState[1]; // The `start` method never changes.
var start = startTransition.bind(null, setPending);
var hook = mountWorkInProgressHook();
hook.memoizedState = start;
return [isPending, start];
}
useTransition的核心其实就是通过useState维护了一个pending,然后将setPending作为参数传递给startTransition。
// useDeferredValue
function updateDeferredValue(value) {
var hook = updateWorkInProgressHook();
var resolvedCurrentHook = currentHook;
var prevValue = resolvedCurrentHook.memoizedState;
return updateDeferredValueImpl(hook, prevValue, value);
}
function updateDeferredValueImpl(hook, prevValue, value) {
var shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
if (shouldDeferValue) {
// This is an urgent update. If the value has changed, keep using the
// previous value and spawn a deferred render to update it later.
if (!objectIs(value, prevValue)) {
var deferredLane = claimNextTransitionLane();
currentlyRenderingFiber$1.lanes = mergeLanes(currentlyRenderingFiber$1.lanes, deferredLane);
markSkippedUpdateLanes(deferredLane);
hook.baseState = true;
}
// Reuse the previous value
return prevValue;
} else {
// This is not an urgent update, so we can use the latest value
if (hook.baseState) {
// Flip this back to false.
hook.baseState = false;
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = value;
return value;
}
}
useDeferredValue的实现首先是判断当前更新的优先级,如果是一个紧急更新则直接返回prevValue,并且在当前fiber中标记一个transition更新。当非紧急更新发生时,直接返回最新的值。
四、useId
useId
是一个新的hook,用于在客户端和服务器上生成唯一 ID,同时避免hydration mismatches。
我们首先介绍一下 SSR 的流程:
在服务端,我们会将 React 组件渲染成为一个字符串,这个过程叫做脱水「 dehydrate 」。字符串以 html 的形式传送给客户端,作为首屏直出的内容。到了客户端之后,React 还需要对该组件重新激活,用于参与新的渲染更新等过程中,这个过程叫做「 hydrate 」。
当我们在使用 React 进行服务端渲染(SSR)时就会遇到一个问题:如果当前组件已经在服务端渲染过了,但是在客户端我们并没有什么手段知道这个事情,于是客户端还会重新再渲染一次,这样就造成了冗余的渲染。
因此,react18提出了useId这个hook来解决这个问题,它使用组件的树状结构(在客户端和服务端都绝对稳定)来生成id。
实现
function useId() {
var task = currentlyRenderingTask;
var treeId = getTreeId(task.treeContext);
var responseState = currentResponseState;
if (responseState === null) {
throw new Error('Invalid hook call. Hooks can only be called inside of the body of a function component.');
}
var localId = localIdCounter++;
return makeId(responseState, treeId, localId);
}
五、Suspense
SSR
React 18 在服务器上添加了对 Suspense 的支持,并使用并发渲染特性扩展了它的功能。
- 流式 HTML 让你尽早开始发送 HTML,流式 HTML 的额外内容与
<script>
标签一起放在正确的地方。 - 选择性 hydration 让你在 HTML 和 JavaScript 代码完全下载之前,尽早开始为你的应用程序进行 hydration。它还优先为用户正在互动的部分进行 hydration,创造一种即时 hydration 的错觉。
transition
function handleClick() {
setTab('comments');
}
<Suspense fallback={<Spinner />}>
{tab === 'photos' ? <Photos /> : <Comments />}
</Suspense>;
在这个示例中,如果tab
从 'photos'
设置为 'comments'
,但Comments
暂停,用户将看到一个Spinner 。因为用户不想再看到Photos
,Comments
还没有准备好渲染任何东西,而 React 需要保持用户体验一致,所以它只能显示Spinner
上面的内容。
但是,有时这种用户体验并不理想(此时用户无法进行交互)。有时在准备新 UI 时显示“旧” UI 会更好。你可以结合useTransition让 React 做到这一点:
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(() => {
setTab('comments');
});
}
<Suspense fallback={<Spinner />}>
<div style={{ opacity: isPending ? 0.8 : 1 }}>
{tab === 'photos' ? <Photos /> : <Comments />}
</div>
</Suspense>
在这个示例中,我们可以使用isPending
它来向用户反映正在发生的事情。UI 保持完全交互——例如,用户可以根据需要切换回'photos'
选项卡。
demo:codesandbox.io/s/react18-s…
六、useSyncExternalStore
useSyncExternalStore
是由 useMutableSource
改变而来,主要用来解决外部数据tear(撕裂)问题。
useSyncExternalStore
旨在供库使用,而不是应用程序代码。
tearing
Screen tearing is a visual artifact in video display where a display device shows information from multiple frames in a single screen draw - wiki
简单的说,就是在屏幕上看到了同一个物体的不同帧的影像,画面仿佛是“撕裂的”,对应的react中,指使用了过去版本的状态进行画面渲染引起的UI不一致或者崩溃。
引入并发渲染后,渲染是可能被更高优先级的任务中断,这也使得tearing成为可能。 但React本身对于state的更新做了很多的工作来避免这个问题,但是如果我们的依赖了外部的状态,比如 redux,它在控制状态时可能并非直接使用的 React 的 state,而是自己在外部维护了一个 store 对象,脱离了 React 的管理,也就无法依靠 React 自动解决撕裂问题。因此 React 对外提供了这样一个 API。
demo:useSyncExternalStore demo - CodeSandbox
七、useInsertionEffect
useInsertionEffect
应该仅限于 css-in-js 库作者。
useInsertionEffect
是一个新的钩子,它允许 CSS-in-JS 库解决在渲染中注入样式的性能问题。 这个 Hooks 执行时机在 DOM
生成之后,useLayoutEffect
之前,它的工作原理大致和 useLayoutEffect
相同,只是此时无法访问 DOM
节点的引用,一般用于提前注入 <style>
脚本。
转载自:https://juejin.cn/post/7117512204059934733