大家都知道,在 React 中是通过 setState 来更新类组件的状态的,但是你真的了解其运行机制吗?
现象描述
下面是一个最基础的类组件:
import React from 'react'
export class SetStateDemo extends React.Component {
constructor(props) {
super(props)
this.state = { number: 0 }
}
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>+</button>
</div>
)
}
handleClick = () => {
this.setState({ number: this.state.number + 1 })
}
}
当点击按钮的时候,会把内部状态 number 的值在原有基础上递增 1,这个并没有什么异议,但是当我们在 handleClick 函数中添加两行 setState 的时候,有意思的问题就来了:
handleClick = () => {
this.setState({ number: this.state.number + 1 })
this.setState({ number: this.state.number + 1 })
}
当每次点击之后,程序并没有将 number 的值递增 2,而是仍然递增 1,这是为什么呢?这里先不忙解释具体原因,而是再把另外一个场景也放在这里对比一下,即添加定时器包裹之后:
handleClick = () => {
setTimeout(() => {
this.setState({ number: this.state.number + 1 })
this.setState({ number: this.state.number + 1 })
})
}
这个时候会发现 number 的值每次会按 2 递增!如果这个现象也使你感到迷惑的话,可以继续看下去:
原理分析
在 React 中,状态的更新默认情况下是异步的、批量的,并非同步更新,也就是说当开发者调用 setState 的时候,并没有立即执行,而是先缓存起来,等到事件函数处理完成之后,再批量更新,只更新和渲染一次。
为什么要这么做?试想下面的一个场景:
handleClick = () => {
this.setState({ number: 1 })
this.setState({ number: 2 })
this.setState({ number: 3 })
this.setState({ number: 4 })
this.setState({ number: 5 })
}
开发者在事件处理函数中频繁调用 setState,如果每次都是立即更新和渲染,就会导致页面频繁刷新,对性能造成影响。在上面的例子中,最终的状态就是 number 等于 5,中间的状态完全不必要保留。
在 React 中,事件处理函数被 React 接管,只要是在 React 的控制范围内,都会进行合并和批量更新,而 setTimeout 等异步操作脱离了 React 主线程的控制,则不会合并和批量更新。先看下面这张图:
这个地方是 React 的一个重点和难点,一定要搞懂其实现原理哦,接下来上代码!
非批量更新
我们知道类组件总是继承自 React.Component,下面就是该 Component 的实现逻辑:
import { createDOM } from './react-dom'
class Component {
static isReactComponent = true
constructor(props) {
this.props = props
this.state = {}
}
// 非批量更新的 setState 实现
setState(nextState, callback) {
if (typeof nextState === 'function') nextState = nextState(this.state)
this.state = { ...this.state, ...nextState }
this.forceUpdate()
if (typeof callback === 'function') callback()
}
// 强制刷新
forceUpdate() {
this.componentWillUpdate?.() // 调用 componentWillUpdate 钩子
const oldDOM = this.dom // 类组件当前对应的真实DOM
const newDOM = createDOM(this.render()) // 新的DOM
oldDOM.parentNode.replaceChild(newDOM, oldDOM) // 替换
this.dom = newDOM
this.componentDidUpdate?.() // 调用 componentDidUpdate 钩子
}
render() {
throw new Error('此方法为抽象方法,需要子类实现')
}
}
可以看到,代码很简单,就是内部使用 this.state 保存状态,用 setState 函数更新 this.state 的值,然后用 react-dom 库中的 createDOM 方法把 render 函数中返回的虚拟 DOM 生成真实 DOM 并替换旧的。
批量更新
这样的话,每次开发者调用 setState 都会重新渲染和替换。为了优化这个过程,可以引入了 updateQueue 全局对象和 Updater 类专门用于批量更新:
export const updateQueue = {
isBatchingUpdate: false, // 是否处于批量更新模式
updaters: new Set(),
batchUpdate() {
for (let updater of updateQueue.updaters) {
updater.updateClassComponent()
}
updateQueue.isBatchingUpdate = false
},
}
// 专门负责更新的类
class Updater {
constructor(it) {
this.classInstance = it // 类组件的实例
this.pendingStates = [] // 等待生效的状态,可能是一个对象,也可能是一个函数
this.callbacks = [] // 回调数组
}
addState(partialState, callback) {
this.pendingStates.push(partialState) // 缓存用户设置的新状态(可能是函数或对象)
if (typeof callback === 'function') this.callbacks.push(callback)
this.emitUpdate() // 通知更新
}
// 无论属性变了还是状态变了,都会更新
emitUpdate() {
if (updateQueue.isBatchingUpdate) {
updateQueue.updaters.add(this) // 如果处于批量更新模式,则不立即更新类组件,setState处理完毕
} else {
this.updateClassComponent() // 更新函数组件
}
}
// 立即更新函数组件
updateClassComponent() {
const { classInstance, pendingStates, callbacks } = this
if (pendingStates.length > 0) {
const nextState = this.getState() // 计算新状态
classInstance.state = nextState
/* 生命周期钩子拒绝更新的场景 */
const flag = classInstance.shouldComponentUpdate?.(classInstance.props, nextState)
if (flag === false) return
classInstance.forceUpdate() // 强制更新
callbacks.forEach(cb => cb())
callbacks.length = 0
}
}
// 获取批量更新最后的状态
getState() {
let { state } = this.classInstance
this.pendingStates.forEach(nextState => {
if (typeof nextState === 'function') nextState = nextState(state)
state = { ...state, ...nextState }
})
this.pendingStates.length = 0
return state
}
}
代码量并不多,其实就是做了一件事:提供一个接口,允许将开发者每次 setState 的状态给缓存起来,最后一次性批量更新。
合成事件
同步代码的使用效果为每次值递增 1:
handleClick = () => {
updateQueue.isBatchingUpdate = true
this.setState({ number: this.state.number + 1 })
this.setState({ number: this.state.number + 1 })
updateQueue.batchUpdate()
}
异步代码的使用效果为每次值递增 2:
handleClick = () => {
updateQueue.isBatchingUpdate = true
setTimeout(() => {
this.setState({ number: this.state.number + 1 })
this.setState({ number: this.state.number + 1 })
})
updateQueue.batchUpdate()
}
这其实就是 setState 的原理,React 认为这种批量更新的行为应该是内置的,所以对每个事件监听函数进行了一层封装,即以 on 开头的 DOM 事件例如 onClick 等不再是原生的 DOM 事件,而是被 React 封装过一层的事件了,这样做的好处有:
- 可以对所有事件处理函数内置批量更新逻辑
- 可以屏蔽不同浏览器对 DOM 事件实现的差异