2022- 从用法到实现,一文带你拥抱React 18
从用法到实现,一文带你拥抱React 18
前言
然而因为这背后的复杂性、稳定性、兼容性等问题,从
今天希望通过这篇文章带大家一起深入了解一下
Concurrent Rendering
核心实现是通过组件作为一个基本的工作单元将一个大的更新任务进行拆分,然后以时间切片的方式,分布在不同的时间片来执行,每个时间片执行完成后都会主动释放控制权,使得浏览器能够处理其它用户事件。而具体时间片上执行哪个任务是由任务上的相关优先级决定的,当高优先级的更新到来时,会中断旧的更新,优先执行高优先级更新,待完成后继续执行低优先级更新,因此在一个时间段内,我们看
Auto Batching
首先批处理是指
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount((c) => c + 1); // Does not re-render yet
setFlag((f) => !f); // Does not re-render yet
// 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>
);
}
然而如果更新发生在
function handleClick() {
fetchSomething().then(() => {
// React 17 and earlier does NOT batch these because
// they run *after* the event in a callback, not *during* it
setCount((c) => c + 1); // Causes a re-render
setFlag((f) => !f); // Causes a re-render
});
}
在
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
// { count: 1, flag: false } before 18
// { count: 0, flag: false } in 18
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
在
针对这种情况,我们可以使用
handleClick = () => {
setTimeout(() => {
ReactDOM.flushSync(() => {
this.setState(({ count }) => ({ count: count + 1 }));
});
// { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
Deep dive
自动批处理实现的关键在于
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// Determine the next lanes to work on, and their priority.
const nextLanes = getNextLanes(root);
// We use the highest priority lane to represent the priority of the callback.
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// Check if there's an existing task. We may be able to reuse it.
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
// The priority hasn't changed. We can reuse the existing task. Exit.
return;
}
// Cancel the existing callback.
cancelCallback(existingCallbackNode);
// schedule a new one.
if (newCallbackPriority === SyncLane) {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
}
如果理解了这个,那个
export function flushSync(fn) {
try {
// DiscreteEventPriority === SyncLane
setCurrentUpdatePriority(DiscreteEventPriority);
fn && fn();
} finally {
setCurrentUpdatePriority(previousPriority);
}
}
Transition

实际上,当我们拖动滑块的时候,需要做两次更新:
// Urgent: Show what was typed
setSliderValue(input);
// Not urgent: Show the results
setGraphValue(input);
一个是比较紧急需要立刻反应在界面上,否则用户会觉得遇到了
we know from research that interactions like hover and text input need to be handled within a very short period of time, while clicks and page transitions can wait a little longer without feeling laggy. —— Putting Research into Production
在
- 一类是用户输入、点击等紧急更新,另一类是将
UI 从一个视图转变成另外一个 transition 中对应的更新类型为第二种。
startTransition
通过
import { startTransition } from "react";
// Urgent
setSliderValue(input);
// Mark any state updates inside as transitions
startTransition(() => {
// Transition: Show the results
setGraphValue(input);
});
useTransition
可以通过
import { useTransition } from "react";
const [isPending, startTransition] = useTransition();
return isPending && <Spinner />;
然而即使我们根据
research shows that displaying too many intermediate loading states when transitioning between screens makes a transition feel slower. —— Putting Research into Production
我们再来感受一下使用了

应该可以明显的感受到,虽然图表的更新还是会有些延迟,但是整体的用户体验相对之前是非常好的。
与以往解决方案的的区别
虽然在
setSliderValue(input);
setTimeout(() => {
setGraphValue(input);
}, 0);
与之相比,transition
的区别主要表现在
- 执行时机,
setTimout/throttle/debounce
均为异步执行,而transition
为同步执行,因此会比他们更早的触发更新调度,在性能较好时可能在同一帧完成更新,而这种情况在比如throttle
中被强制拉大,比如100ms - 交互体验,不管是延迟还是减频,当真正触发更新,如果渲染时间比较久,依然会发生界面卡顿,而通过
transition
触发的更新并不会阻塞用户界面,能够一直保持响应 - 精确控制,需要额外实现
loading 控制,而且往往不够精确,现在transition
内部会为我们自动维护这个loading 状态,并且足够精确
Deep dive
我们结合源码看一下
function startTransition(setPending, callback, options) {
// ...
const prevTransition = ReactCurrentBatchConfig.transition;
// Tag We are inside transition
ReactCurrentBatchConfig.transition = {};
callback();
// Recovery
ReactCurrentBatchConfig.transition = prevTransition;
}
// Request priority when fiber update
export function requestUpdateLane(fiber: Fiber) {
// ...
// requestCurrentTransition => ReactCurrentBatchConfig.transition
const isTransition = requestCurrentTransition() !== null;
if (isTransition) {
return claimNextTransitionLane();
}
// ...
}
function mountTransition() {
const [isPending, setPending] = mountState(false);
const start = (callback) => {
setPending(true);
const prevTransition = ReactCurrentBatchConfig.transition;
// Tag We are inside transition
ReactCurrentBatchConfig.transition = {};
setPending(false);
callback();
// Recovery
ReactCurrentBatchConfig.transition = prevTransition;
};
return [isPending, start];
}
可以思考一下,🤔 下面这两种方式有什么区别吗
startTransition(() => {
setValue1(value);
setValue2(value);
});
startTransition(() => {
setValue1(value);
});
startTransition(() => {
setValue2(value);
});
从执行的效果看,由于目前所有的
但是从合理性的角度,两个完全独立的
useDeferredValue
现在我们知道更新发生时,我们可以通过
import { startTransition } from "react";
const Comp = () => {
const handleChange = () => {
setSliderValue(input);
startTransition(() => {
setGraphValue(input);
});
};
// ...
};
那如果更新触发的实际我们并不知道,比如从
import { useDeferredValue } from "react";
const Comp = (input) => {
const graphValue = useDeferredValue(input);
// ...updating depends on graphValue
};
上方代码的具体效果是当
而实现上我们可以看到,其实就是封装了
function updateDeferredValue(value) {
const [prevValue, setValue] = updateState(value);
updateEffect(() => {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = {};
setValue(value);
ReactCurrentBatchConfig.transition = prevTransition;
}, [value]);
return prevValue;
}
但是上面的实现有一个问题,比如当接受的
function updateDeferredValue(value) {
const 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.
const prevValue = currentHook.memoizedState;
if (!is(value, prevValue)) {
// Mark an Transition Update
const deferredLane = claimNextTransitionLane();
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
deferredLane
);
}
return prevValue;
}
hook.memoizedState = value;
return value;
}
可以看到新的实现不会依赖
Upgrade
Upgrade Root API
同时还需要升级一下
- Legacy Root API
import ReactDOM from "react-dom";
import App from "App";
const container = document.getElementById("app");
// Initial render.
ReactDOM.render(<App />, container);
- New Root API
import * as ReactDOMClient from "react-dom/client";
import App from "App";
const container = document.getElementById("app");
// Create a root.
const root = ReactDOMClient.createRoot(container);
// Initial render: Render an element to the root.
root.render(<App />);
为了代码的平滑升级,两种方式都将会保留,不然效果会是,升级完
新旧
import * as ReactDOM from 'react-dom';
import App from 'App';
const container = document.getElementById('app');
ReactDOM.render(container, <App tab="home" />, function() {
// Called after inital render or any update.
console.log('rendered').
});
但是可以通过其它方式来代替,比如:
- 通过
ref
import * as ReactDOMClient from "react-dom/client";
function App({ callback }) {
// Callback will be called when the div is first created.
return (
<div ref={callback}>
<h1>Hello World</h1>
</div>
);
}
const rootElement = document.getElementById("root");
const root = ReactDOMClient.createRoot(rootElement);
root.render(<App callback={() => console.log("renderered")} />);
- 通过
useEffect
import { useEffect } from "react";
import * as ReactDOMClient from "react-dom/client";
function App() {
useEffect(() => {
// Callback will be called when the div is first created.
console.log("renderered");
}, []);
return (
<div>
<h1>Hello World</h1>
</div>
);
}
const rootElement = document.getElementById("root");
const root = ReactDOMClient.createRoot(rootElement);
root.render(<App />);
两者的区别在于
Ref 会在应用添加到dom 之后同步的执行Effects 由于本身的实现是异步的,会有一个小的延迟
使用哪种方式取决于你具体的业务场景
Fix Tearing & useSyncExternalStore
绝大多数类库不需要做任何改动就能够兼容
Screen tearing is a visual artifact in video display where a display device shows information from multiple frames in a single screen draw - wiki
简单的说,就是在屏幕上看到了同一个物体的不同帧的影像,画面仿佛是“撕裂的”,对应的
- 同步渲染

- 并发渲染

这里
具体用法的话,这边直接做了一个
相信大家应该可以感受到区别,不过我们看到虽然使用了
function updateSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T
): T {
const nextSnapshot = getSnapshot();
const prevSnapshot = hook.memoizedState;
const snapshotChanged = !is(prevSnapshot, nextSnapshot);
// store内值更新时,进行一致性检查
updateEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [
subscribe,
]);
// commit环境开始之前,进行一次一致性检查
if (inst.getSnapshot !== getSnapshot || snapshotChanged) {
pushEffect(
HookHasEffect | HookPassive,
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
undefined,
null
);
}
// render环节结束时,进行一次一致性检查
if (!includesBlockingLane(root, renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
}
// ...
// 一致性检查方法内
if (checkIfSnapshotChanged(inst)) {
// Force a sync re-render.
scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
}
// ...
可以看到,实现的核心是加了三重保障来进行一致性检查,当出现不一致时就发起一个同步更新的调度,因此
另外对于类库开发者来说,在社区并发特性全面铺开前,不鼓励在类库中使用