阅读 337

你知道 React setState 的原理吗?

大家都知道,在 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 主线程的控制,则不会合并和批量更新。先看下面这张图:

setState

这个地方是 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 事件实现的差异
文章分类
前端
文章标签