我们在面试的过程中经常会被问到,我们通过
setState
更改状态后发生了什么?状态是如何变更的?本期将从以下几个方面来深入了解
setState
的工作原理。
- 原理剖析
- 实现异步队列
updateQueue
、Updater
、Component
setState 原理剖析
我们通过 class Cmp extends Component
来定义一个class 组件,在源码中,Component的实现很简单,除了定义了一些实例变量,只有setState 和 forceUpdate
两个方法。
class Component{
static isReactComponent = {}
constructor(props, context){
// 更新器: 管理当前组件中所有变更
this.$updater = new Updater(this)
this.$cache = { isMounted: false }
this.props = props
this.state = {}
this.refs = {}
this.context = context
}
// 跳过所有生命周期执行强制更新, 实际更新组件的函数
forceUpdate(callback) {}
// nextState 可能是对象或函数
setState(nextState, callback) {
// 添加异步队列 不是每次都更新
this.$updater.addCallback(callback)
this.$updater.addState(nextState)
}
}
暂且先不看 forceUpdate,在组件中我们通过 new Updater(this)
一个更新器(与组件一一对应),来管理组件的所有变更,setState
中也只是简单的通过更新器将变更动作和回调添加到异步队列中。通过updater.addState(nextState)和updater.addCallback(callback)
将 nextState 和 callback 分别添加到 penddingStates 和peddingCallbacks
中,然后 React 中通过 updateQueue
来管理这些 updater
, 调用 updateQueue.add
将任务添加到队列等待系统批量更batchUpdate
。
刚刚说了一堆,眼睛有点缭乱了,我们来画个图,理解一下它们之间的关系。
用5分钟给大家用图更加客观的描述了 updateQueue
、updater
、Component
的关系。
到此,我们还需要了解 updater 是如何来管理当前组件变更的,来实现一个 Updater
Updater
class Updater{
constructor(instance){
this.instance = instance // 组件实例
this.pendingStates = [] // 待处理状态数组
this.pendingCallbacks = [] // 待处理回调数组
this.isPending = false
this.nextProps = this.nextContext = null
this.clearCallbacks = this.clearCallbacks.bind(this)
}
// 通知更新函数
emitUpdate(nextProps, nextContext) {
this.nextProps = nextProps
this.nextContext = nextContext
// 如果有接受到新的props,则立即更新
nextProps || !updateQueue.isPending
? this.updateComponent()
: updateQueue.add(this)
}
// 实际更新函数
updateComponent() {
let { instance, pendingStates, nextProps, nextContext } = this
if (nextProps || pendingStates.length > 0) {
nextProps = nextProps || instance.props
nextContext = nextContext || instance.context
this.nextProps = this.nextContext = null
// getState 合并所有的state的数据,一次更新
shouldUpdate(instance, nextProps, this.getState(), nextContext, this.clearCallbacks)
}
}
addState(nextState) {
if (nextState) {
this.pendingStates.push(nextState)
// 如果当前队列空闲则直接更新
if (!this.isPending) {
this.emitUpdate()
}
}
}
addCallback(callback) {
if (_.isFn(callback)) {
this.pendingCallbacks.push(callback)
}
}
getState() {
let { instance, pendingStates } = this
let { state, props } = instance
if (pendingStates.length) {
state = {...state}
pendingStates.forEach(nextState => {
if (_.isFn(nextState)) {
nextState = nextState.call(instance, state, props)
}
state = {...state, ...nextState}
})
pendingStates = []
}
return state
}
clearCallbacks() {
let { pendingCallbacks, instance } = this
if (pendingCallbacks.length > 0) {
pendingCallbacks.forEach(callback => callback.call(instance))
this.pendingCallbacks = []
}
}
}
这里 Updater
除了在 setState 中用到的两个方法,还实现了另外两个重要的方法,emitUpdate 和 updateComponent
分别用于通知组件更新和 更新组件。在组件实例化时,我们通过new Updater(this)
将组件实例存储在 this.instance
中。
通过分析addState 和 emitUpdate
,组件只有在updater.isPedding 和 updateQueue.isPending
均处于空闲时才会调用 updateComponent 去执行组件更新,注意在 updater.isPedding 空闲且组件存在新的 props 时,组件会立即更新 。
OK,重点来了,在 updateComponent
方法中 着重看 shouldUpdate(instance, nextProps, this.getState(), nextContext, this.clearCallbacks)
,通过 this.getState()
将合并后的新状态传入方法中,this.clearCallbacks
是用来批量执行回调的。
我们知道 在Component 中 setState 和 forceUpdate 的主要区别是,前者会去判断是否需要执行更新,后者会跳过这些步骤,强制更新。
function shouldUpdate(component, nextProps, nextState, nextContext, callback) {
// 是否应该更新 判断shouldComponentUpdate生命周期
let shouldComponentUpdate = true
if (component.shouldComponentUpdate) {
shouldComponentUpdate = component.shouldComponentUpdate(nextProps, nextState, nextContext)
}
if (shouldComponentUpdate === false) {
component.props = nextProps
component.state = nextState
component.context = nextContext || {}
return
}
let cache = component.$cache
cache.props = nextProps
cache.state = nextState
cache.context = nextContext || {}
component.forceUpdate(callback)
}
可以看出,绕了这么多,真正执行更新的是组件实例的 forceUpdate
, 在执行更新前,回去判断组件是否有定义 shouldComponentUpdate
,根据其返回值来决定是否更新,将当前状态和参数缓存在$cache中,显然在 forceUpdate 中我们会用到。
foreUpdate
forceUpdate
会跳过所有生命周期,强制执行组件更新。
forceUpdate(callback) {
let { $updater, $cache, props, state, context } = this
if (!$cache.isMounted) {
return
}
if ($updater.isPending) {
$updater.addState(state)
return;
}
let nextProps = $cache.props || props
let nextState = $cache.state || state
let nextContext = $cache.context || context
let parentContext = $cache.parentContext
let node = $cache.node
let vnode = $cache.vnode
$cache.props = $cache.state = $cache.context = null
$updater.isPending = true
if (this.componentWillUpdate) {
this.componentWillUpdate(nextProps, nextState, nextContext)
}
this.state = nextState
this.props = nextProps
this.context = nextContext
// 对比vnode
let newVnode = renderComponent(this)
let newNode = compareTwoVnodes(vnode, newVnode, node, getChildContext(this, parentContext))
if (newNode !== node) {
newNode.cache = newNode.cache || {}
syncCache(newNode.cache, node.cache, newNode)
}
$cache.vnode = newVnode
$cache.node = newNode
clearPending()
if (this.componentDidUpdate) {
this.componentDidUpdate(props, state, context)
}
if (callback) {
callback.call(this)
}
$updater.isPending = false
$updater.emitUpdate()
}
可以暂时不看缓存和虚拟 dom 相关方法的具体实现部分,在forceUpdate
中,执行过程是:
isPending = true
——> 执行 componentWillUpdate
——> 对比vnode,更新 dom ——> 执行componentDidUpdate
——> 批量执行回调 ——> isPending = false
——> 继续调用emitUpdate
直到没有需要更新工作(!(nextProps || pendingStates.length > 0)
)。
最后,补齐下 updateQueue
相关的实现。
updateQueue
let updateQueue = {
updaters: [],
isPending: false,
add(updater) {
this.updaters.push(updater)
},
batchUpdate() {
if (this.isPending) {
return
}
this.isPending = true
let { updaters } = this
let updater
while (updater = updaters.pop()) {
updater.updateComponent()
}
this.isPending = false
}
}
总结
说了这么多,最后我们来理一下思路,再来画一张图,源代码后期整理后会放在github中。
由此可见,setState 在合成事件和勾子函数中之所以是异步的,是因为执行函数在更新动作前就执行了。