Published on
623

实现一个简单的 React.js 18 自动批量更新功能

Authors
  • avatar
    Name
    小辉辉
    Twitter

前言

React.js 18之前批量更新state是否是异步操作取决于调用时机,具体表现为以下几种场景:

  • 如果是在事件内部调用的链路则是异步的,例如click点击事件,change值变化事件

  • 在事件内部链路用异步包裹处理,例如setTimeout、Promise则是同步的

  • 其余的地方一律都是同步的

自从更新到React.js 18版本之后,默认上述setState行为全部变成了异步操作,这对整体组件性能优化提升了不少,因为在同一个操作里面所有更新都变成了批量的操作,组件重新渲染的次数就更少了只合并为了一次。

基本实现原理

我们通过打断点的形式,可以很快速的得出React18现在的批量更新实现形式,主要是借助于MessageChannel这个功能来实现,对应步骤如下:

  • React内部维护了一个MessageChannel实例mc
  • mc对应的port1监听setState调用时触发的更新操作
  • 内部维护一个待更新操作update,用于缓存最近的state值
  • 每次调用setState时,会更新update,还有向mc的port2发送消息
  • mc的port2在接收到第一次更新后,会标记内部更新状态isUpdating,避免多次发送操作
  • 所有setState操作完后,mc的port1开始响应port2发送的消息,执行update更新操作

至此一个简单的批量更新操作实现完成。

示例代码

class ReactAutomaticBatchUpdate {
  mc = new MessageChannel()
  state = { num: 0 }
  update
  isUpdating = false

  constructor() {
    this.start()
    this.initMc()
    this.initPromise()
    this.initSetTimetout()
    this.logNum('before setState')
    this.setState({ num: 1 })
    this.logNum('first after setState')
    this.setState({ num: this.state.num + 1 })
    this.end()
  }

  start() {
    this.logNum('start')
  }

  end() {
    this.logNum('end')
  }

  initPromise() {
    Promise.resolve().then(() => {
      this.logNum('promise')
    })
  }

  initSetTimetout() {
    setTimeout(() => {
      this.logNum('timeout 0')
    })
    setTimeout(() => {
      this.logNum('timeout 1')
    }, 1)
  }

  handlePortMsg = () => {
    this.state = this.update
    this.isUpdating = false
    console.log('after msg')
  }

  initMc() {
    this.mc.port1.onmessage = this.handlePortMsg
  }

  sendData() {
    if (!this.isUpdating) {
      this.isUpdating = true
      this.mc.port2.postMessage(null)
    }
  }

  setState(data) {
    this.update = data
    this.sendData()
  }
  logNum(type) {
    console.log(type, this.state.num)
  }
}

new ReactAutomaticBatchUpdate()

输出结果

start 0
before setState 0
first after setState 0
end 0
promise 0
timeout 0 0
after msg
timeout 1 1

通过结果我们能够发现以下几点:

  • 每次setState操作后state没有及时更新,批量更新实现
  • promise先于setTimeout、MessageChannel调用
  • timeout 0先于MessageChannel调用
  • timeout 1在MessageChannel后调用

最后关于React 18自动批量更新更多的说明还可以参考官方这篇文章