一文让你不再困惑setState之getState
React高级
专栏收录该内容
10 篇文章0 订阅
订阅专栏
文章目录
setState 的特点
getState() 获取最新状态值
在面试中我们通常会被问到有关 setState 相关的问题。
接下来我们先看几个示例,然后通过 模拟实现 setState的行为来更加深刻的理解实例得出的结论。
setState 的特点
批量执行:多key一次执行,对同一个key多次操作会合并,会执行最后一次
可能是异步的(如在生命周期、react合成事件中)
在定时器和原生事件中,它是同步的;
由于 setState 可能是异步的,如果要立即获取到最新的值,有三种方式:
传函数给setState, 或者在第二个参数回调函数中获取最新值
在定时器setTimeout中
在原生事件中(跳过了react的事件机制)
咋一看,还是觉得比较绕,结合以上特点,我们来看几个例子
多次执行同一个key
class App1 extends React.Component {
state = { count: 1 }
componentDidMount() {
this.setState({ count: this.state.count++ })
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 2 })
console.log(‘count1’, this.state.count); // ??1
// 使用定时器
setTimeout(() => {
console.log(‘count2’, this.state.count) // ??2
})
}
handleClick = () => {
this.setState(({ count }) => {
console.log(‘count3’, count) // ?? 3
return { count: count + 1}
})
console.log(‘count4’, this.state.count) // ?? 4
this.setState(({ count }) => {
console.log(‘count5’, count) // ??5
})
}
render() {
return <div>
<p>count: {this.state.count}</p>
<button onClick={this.handleClick}>加1</button>
</div>
}
}
我们思考下,控制台1, 2 和界面上渲染的值分别是多少呢?
当点击按钮之后,控制台 3,4, 5和界面渲染值又分别是多少?
count1: 2
count2: 4
界面显示:4
点击一次
count4: 4
count3: 4
count5: 5
界面显示:6
你分析对了吗?
我们首先理论分析一下,第一次执行时,同一个key 多次执行会被合并为一次,并执行最后一次,因此最终实际执行的是
this.state.count++
this.setState({ count: this.state.count + 2 })
所以界面上结果显示为 4 ,又因为 setState 可能是异步的,我们在 1 处 读取到的实际上是 this.state.count++ 之后的值,这里需要注意的是this.state.count++是对count本身的一种操作,不是赋予新值。
我们点击一次后,函数中执行了两次 setState,执行顺序是count4: 4,count3: 4,count5: 5;当 setState 接受回调函数时,参数是最新的 state 值,回调会被一一执行,因此在第一个回调函数中的count 为 4,执行加一后,第二个回调中就是 5, 再执行加一,最终界面显示6。
OK,我们再来稍微变化一点点,我们加上第二个参数回掉:
componentDidMount() {
this.setState({ count: this.state.count++ })
this.setState({ count: this.state.count + 1 }, () => {
console.log(‘回调1’, this.state.count)
})
this.setState({ count: this.state.count + 2 }, () => {
console.log(‘回调2’, this.state.count)
})
console.log(‘count1’, this.state.count)
this.setState(({ count }) => {
console.log(‘count2’, count)
return { count: count + 1 }
})
this.setState({ count: this.state.count + 1 }, () => {
console.log(‘回调3’, this.state.count)
})
console.log(‘count3’, this.state.count)
setTimeout(() => {
console.log(‘count4’, this.state.count)
})
如果仔细的同学,会发现这里有一个细节,如果不注意,结果就会不一样,我们看下最终的执行顺序和结果:
count1: 2
count3: 2
count2 :4
回调1: 3
回调2 :3
回调3 :3
count4 :3
界面显示结果:3
这里需要着重分析的是回调中打印的值,在count2 执行完之后 count 值变为了5,接下来执行的 this.setState({ count: this.state.count + 1 }, () => { console.log(‘回调3’, this.state.count) }) 注意,这里this.state.count 的值其实是 2,执行完之后的结果实际是 3, 所以最终我们在回调中读取的 count 都是 3。
如果上面的分析没有理解,没有关系,现在我们来梳理下 setState 是如何更新状态的,如何去获取最终的 state?
getState() 获取最新状态值
setState(nextState, callback) 其实做的事情很简单,就是将接受的 nextState 和 callback 分别添加到异步队列中;
// nextState 可能是对象或函数
setState(nextState, callback) {
// 添加异步队列 不是每次都更新
this.$updater.addCallback(callback)
this.$updater.addState(nextState)
}
这里我们先不讲 setState 的更新具体过程,将在 一文让你不再困惑setState之原理剖析和手写(下)中详细介绍。
组件的更新和状态的收集等都是updater代理执行的,我们来定义一个 Updater , 主要实现addCallback、addState 功能。
class Updater{
constructor(instance){
this.instance = instance // 组件实例
this.pendingStates = [] // 待处理状态数组
this.pendingCallbacks = [] // 待处理回调数组
this.isPending = false
}
addState(nextState) {
if (nextState) {
// 放入更新队列
this.pendingStates.push(nextState)
// 如果当前队列没有工作则直接更新
if (!this.isPending) {
this.emitUpdate() // 这里省略它的实现。。。。
}
}
}
addCallback(callback) {
if (_.isFn(callback)) {
this.pendingCallbacks.push(callback)
}
}
}
可以看到,还是简单的进行了存储,这里会先去判断当前队列是否空闲,如果没有工作,则直接更新。在真正去执行更新时,通过遍历这两个数组,来获取最新的 state 和执行回调函数;
现在我们来实现 getState() 函数
getState() {
let { instance, pendingStates } = this
let { state, props } = instance
if (pendingStates.length > 0) {
state = { …state }
pendingStates.forEach(nextState => {
if (_.isFn(nextState)) {
nextState = nextState.call(instance, state, props)
}
state = { …state, …nextState }
})
pendingStates = []
}
return state
}
10行代码,在执行更新时,通过getState() 拿到最新值,通过遍历pendingStates , r如果 nextState是函数类型,则直接执行,并传入当前最新的 state 和 props,如果是对象,则直接向前覆盖,这就是为何相同的key多次执行合并为一次切执行最后一次 的原因;
在页面更新完成后,执行完 componentDidUpdate ,我们去调用回掉函数执行
clearCallbacks() {
let { pendingCallbacks, instance } = this
if (pendingCallbacks.length > 0) {
pendingCallbacks.forEach(callback => callback.call(instance))
this.pendingCallbacks = []
}
}
到此为止,我们再回过头去看刚才的两个栗子,是不是更好理解了。
我们在面试的过程中经常会被问到,我们通过 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 在合成事件和勾子函数中之所以是异步的,是因为执行函数在更新动作前就执行了。