React高级|一文让你不再困惑setState之getState(上)


文章目录

  • setState 的特点
  • getState() 获取最新状态值

在面试中我们通常会被问到有关 setState 相关的问题。
接下来我们先看几个示例,然后通过 模拟实现 setState的行为来更加深刻的理解实例得出的结论。
setState 的特点
  • 批量执行:多key一次执行,对同一个key多次操作会合并,会执行最后一次
  • 可能是异步的(如在生命周期、react合成事件中)
  • 在定时器和原生事件中,它是同步的;
由于 setState 可能是异步的,如果要立即获取到最新的值,有三种方式:
  • 传函数给setState, 或者在第二个参数回调函数中获取最新值
  • 在定时器setTimeout中
  • 在原生事件中(跳过了react的事件机制)
咋一看,还是觉得比较绕,结合以上特点,我们来看几个例子
  1. 多次执行同一个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 count: {this.state.count}
} }

我们思考下,控制台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 = [] } }

到此为止,我们再回过头去看刚才的两个栗子,是不是更好理解了。
【React高级|一文让你不再困惑setState之getState(上)】下一篇,我们看看 当页面执行 setState() 发生了什么?它的工作原理是什么?

    推荐阅读