目录
- React的setState的前世今生
- 概述
- setState的三种用法
- setState异步函数
React的setState的前世今生
setState
是修改state
的一个函数,在修改后会自动调用render
函数重新渲染DOM,那么你知道它有多少种用法、各写法直接有什么区别和好处、为什么在React18
是异步而不是同步、哪种情况下它会变成同步呢?接下来我将一一解惑。
以下javascript
代码均是在handleChangeCount
中被执行。
- Test.jsx
import React, { Component } from "react";
export class Test extends Component {
constructor(props) {
super();
this.state = {
count: 0,
};
}
handleChangeCount = () => {
// ...
};
render() {
const { count } = this.state;
return (
<div>
<h1>{count}</h1>
<button onClick={this.handleChangeCount}>count + 1</button>
</div>
);
}
}
export default Test;
概述
Vue 3 的渲染机制和 React 18 中的 setState 机制在某些方面有相似之处,但也有一些区别。简单来说,Vue中是使用数据劫持,订阅发布,数据变化就更新视图。React中是主动调用
setState
函数才会更新视图。
-
异步更新: React 18 中的 setState 机制使用了异步更新,即当调用 setState 时,React 并不会立即更新组件,而是将更新放入队列中,在合适的时机批量更新,这样可以提高性能。
Vue 3 中的渲染机制也是异步更新的,但是它使用的是响应式系统,当响应式数据发生变化时,Vue 会将更新放入微任务队列中,在下一个事件循环中执行更新,这也能够提高性能。 -
调度器:React 18 引入了新的调度器(Scheduler)机制,可以让 React 更加灵活地控制更新的优先级和时机,以提高性能和用户体验。
Vue 3 中并没有像 React 18 中那样明确的调度器概念,但是其使用的异步更新机制也可以让开发者在一定程度上控制更新的时机。
更新策略: -
在 React 中,更新是基于组件的状态变化而触发的,而且由于状态是不可变的,React 可以更容易地进行优化,例如使用浅比较来决定是否需要重新渲染组件。
在 Vue 中,更新是基于响应式数据的变化触发的,Vue 会使用更复杂的策略来决定是否需要重新渲染组件,包括对模板和虚拟 DOM 的分析等。
setState的三种用法
- 用法一:使用setState传入对象修改count
this.setState({
count: this.state.count + 1,
});
- 用法二: 使用回调函数修改count。
以函数回调形式修改有两点好处:
- 能够使对state修改的操作代码高内聚,集中在
setState
回调函数中。- 能够获取到更新前的state和props,做更多的事情。
this.setState((prevState, prevProps) => {
// 更多的处理...
return {
count: prevState.count + 1,
}
})
- 用法三:
setState
是一个异步函数,不会阻塞同步代码。如果你需要使用到最新的state,可以使用第二个参数——更新后的回调。
this.setState(
{
count: this.state.count + 1,
},
() => {
console.log("更新后:" + this.state.count); // 输出:1
}
);
console.log("更新前:" + this.state.count); // 输出:0
setState异步函数
在
React18
中被修改为自动异步操作,所以会将setState
的三次操作加入队列中,批量处理,只做一次更新。当然也提供了同步的方式,使用官方提供的API——flushSync
包裹食用。问题:为什么是异步,而不是同步?更详细的解答
答:从性能上,以下三次
setState
可以想象出,如果是同步,React
将依次执行三次Diff
和render
。如果是异步,那么React
执行阈值为1,极大提升了性能,只会有一次Diff
和render
。而往往在实际开发场景中,数据有先后顺序且要实时更新时,就会出现很多地方调用setState
。从安全上,如果是同步,且
count
是作为props
传递给了子组件,在第一个setState
执行后,已经改变了父组件中的count
值,接着执行第二个setState
,但是未进行render
渲染DOM,所以子组件中的props.count
还是原来的值,子组件视图是没有更新的,这将导致state
、props
、refs
之间数据的不一致性。诶,这时候又会有聪明的同学问了:难道我三次操作变成一次不会导致延迟渲染视图吗?
所以
React18
引入了新的调度器(Scheduler)机制,可以让 React 更加灵活地控制更新的优先级和时机,以提高性能和用户体验。
- 多次调用,批处理
setState
。
this.setState((prevState) => {
console.log(this.state.count, prevState.count) // 0, 0
return {
count: prevState.count + 1,
};
});
this.setState((prevState) => {
console.log(this.state.count, prevState.count) // 0, 1
return {
count: prevState.count + 1,
};
});
this.setState((prevState) => {
console.log(this.state.count, prevState.count) // 0, 2
return {
count: prevState.count + 1,
};
});
console.log(this.state.count) // 3
- 在
React18
中的setState
同步
flushSync(() => {
// flushSync函数内部仍然是异步状态
this.setState({count: this.state.count + 1});
});
console.log(this.state.count); // 1
React18
之前的同步setState
复现
以下代码使用的
React17
,作为测试复现setState
的同步情况版本。问题:为什么用
setTimeout
包裹就是同步,不用就是异步?答:在类组件中的所有函数,都将委托
React
调用。所以如果不用setTimeout
包裹,那么add
函数中的setState
将被加入队列中,进行批量处理,异步更新。可一但被setTimeout
包裹,表示被添加到浏览器的宏任务队列中(Event Loop),那么将是浏览器调用setTimeout
的箭头函数,执行宏任务队列,setState
也将不会走异步逻辑。像这种情况的还有使用原生DOM的回调、微任务队列。更多情况
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<script src="https://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.development.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<body>
<div id="app"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidMount() {
// 同步
const addBtnEl = document.getElementById("addBtn");
addBtnEl.addEventListener("click", () => {
this.setState((prevState) => {
return {
count: prevState.count + 1,
};
});
console.log(this.state.count);
});
}
add() {
// 异步
this.setState((prevState) => {
return {
count: prevState.count + 1,
};
});
console.log(this.state.count);
// 同步
setTimeout(() => {
this.setState((prevState) => {
return {
count: prevState.count + 1,
};
});
console.log(this.state.count);
}, 0);
}
render() {
return (
<div>
<h2>{this.state.count}</h2>
<button id="addBtn" onClick={() => this.add()}>+1</button>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("app"));
</script>
</body>
</html>