目录
1.1 假如一次事件中触发一次如上 setState ,在 React 底层主要做了那些事呢?
3、 那this.setState()什么时候同步,什么时候不同步?
3.2 同步时候:比如用 promise 或者 setTimeout 时 批量更新规则被打破,就可以实现同步更新
3.2.2 那么,如何在如上异步环境下,继续开启批量更新模式呢?
4.1 onClick点击事件(),setState是异步的
this.setState是同步还是异步应该困扰好多人吧,然后百度一搜,众说纷纭,有说同步,有说异步...”到底是同步还是异步能不能直接了当说个结果呀!“,如果你也同样这样的想法,那今天说个明白:setState本身是异步的,但在特殊环境下(setTimeout、setInterval 等 DOM 原生事件)它是同步的。
拓展: React 是有多种模式
- React 是有多种模式的,基本平时用的都是 legacy 模式下的 React,除了
legacy
模式,还有blocking
模式和concurrent
模式, blocking 可以视为 concurrent 的优雅降级版本和过渡版本。React 最终目的,不久的未来将以 concurrent 模式作为默认版本,这个模式下会开启一些新功能。下面围绕 legacy 模式下的 state。问题 legacy 模式和 concurrent 模式是什么鬼?
- 通过 ReactDOM.render(<App />, rootNode) 方式创建应用,则为 legacy 模式,这也是 create-react-app 目前采用的默认模式;
- 通过 ReactDOM.unstable_createRoot(rootNode).render(<App />) 方式创建的应用,则为 concurrent模式 。对于 concurrent 模式下,会采用不同 State 更新逻辑。前不久透露出未来的Reactv18 版本,concurrent 将作为一个稳定的功能出现。
1、this.setState的参数
this.setState(param1, param2);
param1: 对象或函数 --- 改变state的值
param2: 回调函数 --- 等待state更新后,执行的函数
先看项目中遇到类似这样的问题:
【问题】
发现 if 语句里拿不到type的值
【原因】
this.setState()不保证是同步的,所以在if条件中调用setState修改后的值,是做不到更新
的。
【解决】
利用this.setState()的第二个参数:回调函数,在等第一个参数内的state更新后再调用
【问题】
this.setState是异步更新,所以console.log(this.state.count)拿不到10
export default class A extends React.pureComponent {
constructor(props){
this.state={
count: 0,
}
}
// 第一个参数:对象
add () {
this.setState({
count:10
})
console.log(this.state.count) // 这里打印this.state.count=0, 这里拿不到 this.state.count=10, 说明了this.setState更新是异步的
}
}
【解决】
假设你想同步获取值,setState还提供了回调函数,在回调函数里可以同步拿到console.log(this.state.count)等于10
// 第二个参数:回调函数,在等待第一个参数更新之后调用
// 假设你想同步获取值,setState还提提供一回调函数。
add () {
this.setState({
count:10
},()=>{
console.log(this.state.count) // 这里是可以拿到的 this.state.count=10
})
}
1.1 假如一次事件中触发一次如上 setState ,在 React 底层主要做了那些事呢?
- 首先,setState 会产生当前更新的优先级(老版本用 expirationTime ,新版本用 lane )。
- 接下来 React 会从 fiber Root 根部 fiber 向下调和子节点,调和阶段将对比发生更新的地方,更新对比 expirationTime ,找到发生更新的组件,合并 state,然后触发 render 函数,得到新的 UI 视图层,完成 render 阶段。
- 接下来到 commit 阶段,commit 阶段,替换真实 DOM ,完成此次更新流程。
- 此时仍然在 commit 阶段,会执行 setState 中 callback 函数,如上的
()=>{ console.log(this.state.number) }
,到此为止完成了一次 setState 全过程。
请记住一个主要任务的先后顺序,这对于弄清渲染过程可能会有帮助:
render 阶段 render 函数执行 -> commit 阶段真实 DOM 替换 -> setState 回调函数执行 callback 。
2、是什么造成了this.setState()的不同步?
只是因为react的性能优化机制体现为异步。在react的生命周期函数或者作用域下为异步,但在特殊环境下(setTimeout、setInterval 等 DOM 原生事件)它是同步的。
setState默认是异步
React18版本之后 setState默认是异步,假如所有setState是同步的,意味着每执行一次setState时(有可能一个同步代码中,多次setState),都重新vnodediff + dom修改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代码中的多个setState合并成一次组件更新。
export default class index extends React.Component{ state = { number:0 } handleClick= () => { this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback1', this.state.number) }) console.log(this.state.number) this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback2', this.state.number) }) console.log(this.state.number) this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback3', this.state.number) }) console.log(this.state.number) } render(){ return <div> { this.state.number } <button onClick={ this.handleClick } >number++</button> </div> } }
点击打印:0, 0, 0, callback1 1 ,callback2 1 ,callback3 1
点击事件触发执行代码,setState是异步,碰到异步就跳过,先 执行console.log。console.log执行完,再从头开始执行异步代码
如上代码,在整个 React 上下文执行栈中会变成这样:
react的性能优化机制:React 的批处理更新导致的(batch the updates)
React为了优化性能,setState()执行时会判断变量isBatchingUpdates的值是true or false, 然后决定是同步更新还是批量更新(结合上面handleClick点击方法的示例)
3、 那this.setState()什么时候同步,什么时候不同步?
3.1 异步的时候:
this.setState在React合成事件/钩子函数中,React会通过batchedUpdates()这个函数将isBatchingUpdates变成true,即批量更新的,是异步的。
在 React 事件执行之前通过
isBatchingUpdates=true
打开开关,开启事件批量更新,当该事件结束,再通过isBatchingUpdates = false;
关闭开关
3.2 同步时候:比如用 promise 或者 setTimeout 时 批量更新规则被打破,就可以实现同步更新
3.2.1 使用setTimeout 使异步变成同步
setTimeout(()=>{ this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback1', this.state.number) }) console.log(this.state.number) this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback2', this.state.number) }) console.log(this.state.number) this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback3', this.state.number) }) console.log(this.state.number) })
打印 : callback1 1 , 1, callback2 2 , 2,callback3 3 , 3
3.2.2 那么,如何在如上异步环境下,继续开启批量更新模式呢?
React-Dom 中提供了批量更新方法
unstable_batchedUpdates
,可以去手动批量更新,可以将上述 setTimeout 里面的内容做如下修改:import ReactDOM from 'react-dom' const { unstable_batchedUpdates } = ReactDOM setTimeout(()=>{ unstable_batchedUpdates(()=>{ this.setState({ number:this.state.number + 1 }) console.log(this.state.number) this.setState({ number:this.state.number + 1}) console.log(this.state.number) this.setState({ number:this.state.number + 1 }) console.log(this.state.number) }) })
打印: 0 , 0 , 0 , callback1 1 , callback2 1 ,callback3 1
在实际工作中,unstable_batchedUpdates 可以用于 Ajax 数据交互之后,合并多次 setState,或者是多次 useState 。原因很简单,所有的数据交互都是在异步环境下,如果没有批量更新处理,一次数据交互多次改变 state 会促使视图多次渲染。
3.2.3 那么如何提升更新优先级呢?
React-dom 提供了 flushSync ,flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。React 设定了很多不同优先级的更新任务。如果一次更新任务在 flushSync 回调函数内部,那么将获得一个较高优先级的更新。
handerClick=()=>{ setTimeout(()=>{ this.setState({ number: 1 }) }) this.setState({ number: 2 }) ReactDOM.flushSync(()=>{ this.setState({ number: 3 }) }) this.setState({ number: 4 }) } render(){ console.log(this.state.number) return ... }
打印 3 4 1
- 首先
flushSync
this.setState({ number: 3 })
设定了一个高优先级的更新,所以 2 和 3 被批量更新到 3 ,所以 3 先被打印。- 更新为 4。
- 最后更新 setTimeout 中的 number = 1。
flushSync补充说明:
flushSync 在同步条件下,会合并之前的 setState | useState,可以理解成,如果发现了 flushSync ,就会先执行更新,如果之前有未更新的 setState | useState ,就会一起合并了,所以就解释了如上,2 和 3 被批量更新到 3 ,所以 3 先被打印。
综上所述, React 同一级别更新优先级关系是:
flushSync 中的 setState > 正常执行上下文中 setState > setTimeout ,Promise 中的 setState。
4、小示例:
分别用两种方法绑定button的click事件,点击button的时候,改变state的值
4.1 onClick点击事件(),setState是异步的
把这两次打印 console.log('prev state:', this.state.type)、console.log('current state:', this.state.type); 放到队列中,一起更新,所以第二次打印值与第一次打印值一样
};export default class A extends React.pureComponent { constructor(props) { super(props); this.state = { type: 'origin', // 原始值为 origin }; } changeState = e => { console.log('prev state:', this.state.type); this.setState({ type: 'changed', // 改变后为 changed }); console.log('current state:', this.state.type); }; render() { console.log('render3'); return ( <Button onClick={this.changeState}>改变state</Button> ); } }
打印结果:
isBatchingUpdates是true,批量更新的,是不同步的,把要执行的内容放到队列中,一起更新,所以第二次打印值与第一次打印值一样
4.2 DOM 原生事件 : setState是同步的
把这两次打印 console.log('prev state:', this.state.type)、console.log('current state:', this.state.type); 分别打印 ,所以第二次打印值与第一次打印值不一样
};export default class A extends React.pureComponent { constructor(props) { super(props); this.state = { type: 'origin', // 原始值为 origin }; } componentDidMount = e => { const dom = document.getElementById('btn'); dom.addEventListener('click', this.changeState); } componentWillUnmount() { // 及时销毁自定义 DOM 事件 document.body.removeEventListener('click', this.changeState) } changeState = e => { console.log('prev state:', this.state.type); this.setState({ type: 'changed', // 改变后为 changed }); console.log('current state:', this.state.type); }; render() { console.log('render'); return ( <Button id="btn">改变state</Button> ); } }
打印结果:
isBatchingUpdates是false,说明是同步更新的,一行执行完紧接着执行下一行。
(在输出prev state 这一行之后,遇到第二行的this.setState()就立即执行了,state更新之后就触发了render,然后才输出current state)
写的太棒了