从 React 18 版本开始,会默认自动进行批处理(automatic batching),减少组件的重复渲染,以此提升性能。本篇文章主要介绍以下的内容:什么是批处理、它以前是如何工作的,以及现在发生了什么变化。

文章目录

什么是批处理什么是自动批处理如果我不想进行批处理该怎么办?这对 Hooks 有什么影响吗?这对 Class 组件有什么影响吗?关于 unstable_batchedUpdates总结

什么是批处理

批处理是 React 将多个状态更新分组到一个重新渲染(re-render)中以获得更好的性能。

例如,如果你在同一个点击事件中有两个状态更新,React 总是将它们批处理到一个重新渲染中。 如果您运行以下代码,您会看到每次单击时,尽管您设置了两次状态,但 React 只执行一次渲染:

function App() {

const [count, setCount] = useState(0);

const [flag, setFlag] = useState(false);

function handleClick() {

setCount(c => c + 1); // 还未重渲染

setFlag(f => !f); // 还未重渲染

// React 只会在最后重新渲染一次(这就是批处理!)

}

return (

{count}

);

}

例子:React 17 事件处理函数中的批处理(注意观察控制台,每次点击后只渲染 1 次)

这对提升性能非常有用,因为它避免了不必要的重新渲染。 它还可以防止你的组件呈现仅更新一个状态变量的“半完成”状态,这可能会导致错误。举个例子,比如在餐厅点菜,当你选择完第一道菜时,服务员并不会立马跑到厨房,而是等待你完成整个订单后才进行下单。

然而,React 在何时批量更新方面并不一致。 例如,如果你需要获取数据,然后在上面的 handleClick 中更新状态,那么 React 不会批量更新,而是进行两次独立更新。

这是因为 React 过去只在浏览器事件(如点击)期间进行批量更新,但这里我们在事件已经处理后更新状态(在 fetch 回调中):

function App() {

const [count, setCount] = useState(0);

const [flag, setFlag] = useState(false);

function handleClick() {

fetchSomething().then(() => {

// React 18 以前的版本不会进行批处理,因为它们是在事件处理回调结束后调用的,

// 而不是在回调的执行期间

setCount(c => c + 1); // 导致重复渲染

setFlag(f => !f); // 导致重复渲染

});

}

return (

{count}

);

}

例子:React 17 版本在事件处理回调以外不会进行批处理 (注意观察控制台,每次点击会触发 2 次渲染)

什么是自动批处理

从 React 18 带有 createRoot 开始,所有更新都将自动批处理,无论它们来自何处。

这意味着 定时器、promise、原生事件处理回调或任何其他事件内部的更新都将以与 React 事件内部的更新相同的方式进行批处理。 这会减少渲染次数,从而提高应用程序的性能:

function App() {

const [count, setCount] = useState(0);

const [flag, setFlag] = useState(false);

function handleClick() {

fetchSomething().then(() => {

// React 18 版本以后会对这些进行批处理

setCount(c => c + 1);

setFlag(f => !f);

// React 只会重渲染 1 次(这就是批处理!)

});

}

return (

{count}

);

}

例子:React 18 中使用 createRoot 的方式,即使在事件处理流程以外也会进行批处理(注意观察控制台,每次点击只渲染 1 次) 例子:React 18 中使用旧的渲染方式,并不会进行批处理 (注意观察控制台,每次点击还是会渲染 2 次)

React 将自动批量更新,无论更新发生在哪里,因此:

function handleClick() {

setCount(c => c + 1);

setFlag(f => !f);

// React 最终只会重渲染 1 次 (这就是批处理)

}

行为与此相同:

setTimeout(() => {

setCount(c => c + 1);

setFlag(f => !f);

// React React 最终只会重渲染 1 次 (这就是批处理)

}, 1000);

行为与此相同:

fetch(/*...*/).then(() => {

setCount(c => c + 1);

setFlag(f => !f);

// React React 最终只会重渲染 1 次 (这就是批处理)

})

行为与此相同:

elm.addEventListener('click', () => {

setCount(c => c + 1);

setFlag(f => !f);

// React React 最终只会重渲染 1 次 (这就是批处理)

});

注意:React 通常只在安全的情况下才进行批量更新。例如,React 确保对于每个用户发起的事件(如单击或按键) ,DOM 都会在下一个事件执行之前完成更新。例如,这样处理可以确保在表单禁用提交时不会被提交两次(a form that disables on submit can’t be submitted twice.)。

如果我不想进行批处理该怎么办?

通常,批处理是安全的,但某些代码可能依赖于在状态更改后立即从 DOM 中读取某些内容。 对于这些用例,你可以使用 ReactDOM.flushSync() 选择退出批处理:

import { flushSync } from 'react-dom'; // 注意: 是从 react-dom 中引入, 不是 react

function handleClick() {

flushSync(() => {

setCounter(c => c + 1);

});

// React 此时已经更新 DOM

flushSync(() => {

setFlag(f => !f);

});

// React 此时已经更新 DOM

}

这种使用情况应该不是很常见。

这对 Hooks 有什么影响吗?

自动批处理应该是不会有什么影响。

这对 Class 组件有什么影响吗?

请记住,React 事件处理程序期间的更新始终是批处理的,因此对于这些更新没有任何影响。

类组件中有一个边缘情况,可能会存在问题:类组件有一个实现怪癖( implementation quirk),可以在事件内部同步读取状态更新。这意味着你将能够在调用 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 的结果——渲染发生在下一个浏览器滴答(browser tick)期间。 所以渲染还没有发生:

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 中删除它,但是它可能会在未来的主要版本中被删除,谨慎使用。

总结

默认开启批处理的条件:React 18 以上的版本,并且使用 createRoot() 的方式进行渲染。

React 版本是否进行批处理React 18 使用 createRoot() 的方式是React 18 使用 旧的 ReactDOM.render() 的方式否React 18 以前的版本否

内容参考自: Automatic batching for fewer renders in React 18

精彩链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: