该文选自React作者 Dan Abramov发布的高热度issue翻译而来,文末有原文链接。文章通俗易通,可读性也相当不错,因此分享给大家,对React18感兴趣的小伙伴欢迎学习讨论。
概述
React 18 通过默认执行更多批处理来增加开箱即用的性能改进,无需在应用程序或库代码中手动批量更新。这篇文章将解释什么是批处理,它以前是如何工作的,以及发生了什么变化。
注意:这是一个我们不希望大多数用户需要考虑的深入功能。但是它可能与布道师和react库开发者有密切关联。
什么是批处理
批处理是 React将多个状态更新分组到单个re-render中以获得更好的性能的操作。
例如,如果你在同一个点击事件中有两个状态更新,React 总是将它们分批处理到一个重新渲染中。如果你运行下面的代码,你会看到每次点击时,React 只执行一次渲染,尽管你设置了两次状态:
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>
);
}
- ✅ 演示:在事件处理程序中反应 17 个批次。(请注意控制台中的每次点击渲染一次。)
这对性能非常有用,因为它避免了不必要的重新渲染。它还可以防止您的组件呈现仅更新一个状态变量的“半完成”状态,这可能会导致错误。这可能会让您想起餐厅服务员在您选择第一道菜时不会跑到厨房,而是等待您完成订单。
然而,React 的批量更新时间并不一致。例如,如果您需要获取数据,然后更新handleClick
上面的状态,那么 React不会批量更新,而是执行两次独立的更新。
这是因为 React 过去只在浏览器事件(如点击)期间批量更新,但这里我们在事件已经被处理(在 fetch 回调中)之后更新状态:
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
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
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
在 React 18 之前,我们只在 React 事件处理程序期间批量更新。默认情况下,React 中不会
对 promise、setTimeout、原生事件处理(native event handlers)或其它React默认不进行批处理的事件进行批处理操作。
什么是自动批处理?
从 React 18的createRoot
开始,所有更新都将自动批处理,无论它们来自何处。
这意味着timeouts, promises, native event handlers或任何其他事件内的更新将以与 React 事件内的更新相同的方式进行批处理。我们希望这会导致更少的渲染工作,从而在您的应用程序中获得更好的性能:
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>
);
}
- ✅ 演示:React 18
createRoot
在事件处理之外的批处理!(注意控制台中的每次点击渲染一次!) - 🟡 演示:React 18 with legacy
render
保留了旧的行为(注意控制台中每次点击两次渲染。)
注意:作为采用 React 18 的一部分,预计您将升级到createRoot
。 旧行为 的render
存在只是为了更容易地对两个版本进行生产实验。
无论更新发生在何处,React 都会自动批量更新,因此:
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
}
与此相同:(setTimeout)
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
}, 1000);
与此相同:(fetch)
fetch(/*...*/).then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
})
行为与此相同:(addEventListener)
elm.addEventListener('click', () => {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
});
注意:React 仅在安全稳定的场景下才批量更新。
例如,React 确保对于每个用户启动的事件(如单击或按键),DOM 在下一个事件之前完全更新。
例如,这可确保在提交时禁用的表单不能被提交两次。
如果不想进行批处理怎么办?
通常,批处理是安全的,但某些代码可能依赖于在状态更改后立即从 DOM 中读取某些内容。对于这些用例,您可以使用ReactDOM.flushSync()
选择退出批处理:
import { flushSync } from 'react-dom'; // Note: react-dom, not react
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React has updated the DOM by now
flushSync(() => {
setFlag(f => !f);
});
// React has updated the DOM by now
}
我们不希望这种场景经常出现。
对 Hooks 有什么影响吗?
如果您使用 Hooks,我们希望自动批处理在绝大多数情况下都能“正常工作”。(如果没有,请告诉我们!)
对 Classes 有什么影响吗?
请记住,React 事件处理程序期间的更新始终是批处理的,因此对于这些更新,没有任何更改。
在类组件中存在边缘情况,这可能是一个问题。
类组件有一个实现的奇怪地方,它可以同步读取事件内部的状态更新。这意味着您将能够在setState
的调用之间读取this.state
:
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
// { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
在 React 18 中,情况不再如此。由于所有更新setTimeout
都是批处理的,因此 React 不会在第一次同步调用setState
时渲染结果——渲染发生在下一次浏览器调度。所以此时渲染还没有发生:
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
// { count: 0, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
请参阅代码。
如果这是升级到 React 18 的阻碍,您可以使用ReactDOM.flushSync
强制更新,但我们建议谨慎使用
:
handleClick = () => {
setTimeout(() => {
ReactDOM.flushSync(() => {
this.setState(({ count }) => ({ count: count + 1 }));
});
// { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
请参阅代码。
此问题不会影响带有 Hooks 的函数组件,因为设置状态不会从useState
以下位置更新现有变量:
function handleClick() {
setTimeout(() => {
console.log(count); // 0
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
console.log(count); // 0
}, 1000)
虽然当您采用 Hooks 时这种行为可能令人惊讶,但它为自动批处理铺平了道路。
unstable_batchedUpdates
怎么办?
一些 React 库使用这个未记录的 API 来强制对setState
外部事件处理程序进行批处理:
import { unstable_batchedUpdates } from 'react-dom';
unstable_batchedUpdates(() => {
setCount(c => c + 1);
setFlag(f => !f);
});
这个 API 在 18 中仍然存在,但不再需要
它了,因为批处理是自动发生的。我们不会在 18 中删除它,尽管在流行的库不再依赖于它的存在之后,它可能会在未来的主要版本中被删除。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。