从 React BatchUpdates 逻辑分析,看 React 是 如何优化性能的?

前言

React 16.8 版本之前 关于多次调用 setState 时候逻辑,里面有一个优化性能,批量更新的优化,我们就来看看,到底底层的逻辑是什么,本次的例子是运行在 React 15 版本上,看看 React 到底是如何优化性能的

batchUpdate 概念

事件处理函数自带 batchedUpdates 调用多次 setState 将会开启,batchUpdate 会将多次 setState 合并到一次 React 任务更新中,然后依次调用 setState 下面我们观察 this.state.number 的变化,从而深入了解 batchedUpdates 的过程

import React from 'react'
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'

export default class BatchedDemo extends React.Component {
  state = {
    number: 0,
  }

  handleClick = () => {
    // 事件处理函数自带`batchedUpdates`
    // this.countNumber()
  }

  countNumber() {
    const num = this.state.number
    this.setState({
      number: num + 1,
    })
    console.log(this.state.number) // 0 这里 state 并没有被更新
    this.setState({
      number: num + 2,
    })
    console.log(this.state.number) // 0 这里 state 并没有被更新
    this.setState({
      number: num + 3,
    })
    console.log(this.state.number) // 0 这里 state 并没有被更新
  }
  
  // 这里调用 点击事件
  // 点击后当前的 state 是 3 显示在页面上
  render() {
    return <button onClick={this.handleClick}>Num: {this.state.number}</button>
  }
}
复制代码

debug

我们在 React 源码中找到 requestWork 在里面进行 添加一个断点 debugger, 我们可以观察到

image.png

因为 isBatchingUpdates 赋值为 true 那么 将直接 return 不进行后面的 performSynvWork 操作, 所以我们可以理解 state 并没有发生更新,同样我们在 enqueueSetState 中是可以看到已经创建好更新,已经在更新队列中

image.png

源码部分

我们继续深入进行观察,看看到底 isBatchingUpdates 状态是如何发生变化的 我们看到源码上,其实我们重点关注到的是 previousIsBatchingUpdates ,这个变量会保存之前的 isBatchingUpdates 状态,在最后 finally 重新赋值到 isBatchingUpdates,然后在一起批量更新。 所以出现这种情况的原因我就顺利找到了

// TODO: Batching should be implemented at the renderer level, not inside
// the reconciler.
function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  // 当前是否 批量更新赋值到 previous 状态上
  const previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    return fn(a); // 这里调用的是 实际上是 handleClick 方法
  } finally {
    // 将过去上一次更新的 previous 存到全局变量 BatchingUpdates 上
    isBatchingUpdates = previousIsBatchingUpdates;
    // 当不是批量更新 而且不是在渲染阶段,那么state的值将会一次更新,调用 performSyncWork
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork(); // 直接同步一起更新 所以这里我们可以
    }
  }
}
复制代码

setTimeout 中特殊情况

setTimeout 的思考:因为 setTimeout 是一个宏任务,从内存角度来说,和之前任务不是在同一个栈中。所以执行到这个宏任务的时候,之前的栈中数据会被还原(isBatchingUpdates 是初始值false),所以不是批量更新。也可以这样去理解 setTimeout 在宏任务中,调用的事件是没有开启批量更新的

import React from 'react'

export default class BatchedDemo extends React.Component {
  state = {
    number: 0,
  }

  handleClick = () => {
      // 这样就不会开启 batchUpdate isBatchingUpdates 属性就会为 false
      setTimeout(() => {  
          this.countNumber()
      }, 0)
  }

  countNumber() {
    const num = this.state.number
    this.setState({
      number: num + 1,
    })
    console.log(this.state.number) // 1
    this.setState({
      number: num + 2,
    })
    console.log(this.state.number) // 2
    this.setState({
      number: num + 3,
    })
    console.log(this.state.number) // 3
  }
  ...
}
复制代码

在 setTimeout 中 强行调用 实现 batchUpdate

我们尝试一下在 setTimeout 宏任务中强行调用 batchedUpdates,这次同 例子一 逻辑是一致的

import React from 'react'
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'

export default class BatchedDemo extends React.Component {
  state = {
    number: 0,
  }
  handleClick = () => {
    // setTimeout中没有`batchedUpdates`
    setTimeout(() => {
      batchedUpdates(() => this.countNumber())
    }, 0)
  }
  ...
}
复制代码

总结

在上面的例子中,我们大概了解到了 batchUpdate 的过程,下面我们总结一下具体流程。其实关键的一步是 previousIsBatchingUpdates 记录上一次的 isBatchingUpdates 状态赋值给下一次, 依次执行 setState 后,就会将之前的状态赋值到下一次更新的队列中,最后开始执行更新应用 performSyncWork,这里的原因是 React 为了提供性能而做出的优化

image.png

Guess you like

Origin juejin.im/post/7062928241920573453