# setState

# setState

# 写这篇文章的初衷

  1. react是采用单向数据流的形式,通过state -> 视图的更新
  2. 优先处理上层props,通过state来维护状态值
  3. setState是唯一触发state更新的api
  4. 尽管react-redux 或者其他的状态库调用更新试图,本质都是通过setState
  5. setState更新逻辑
  6. 面试

# 同步异步问题

  1. 使用pnpm + vite快速搭建一个react项目
  2. 注意react版本是18.2

那么react调用setState时候到底是同步还是异步?

代码


state = {
   num: 1
}
componentDidMount() {
    // this.setState({
    //   num: this.state.num + 1
    // });
    // this.setState({
    //   num: this.state.num + 1
    // });
    // this.setState({
    //   num: this.state.num + 3
    // });
    // setTimeout(() => {
    //   this.setState({
    //     num: this.state.num + 1
    //   });
    //   this.setState({
    //     num: this.state.num + 1
    //   });
    //   this.setState({
    //     num: this.state.num + 3
    //   });
    //   console.log(this.state.num);
    // });
    // console.log(this.state.num); //react版本 18.2 输出结果都是this.state.num ---> 1

    this.btnRef.addEventListener('click', this.nativeHandleClick, false);
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  • 第一次页面加载效果

image.png

  • 调试工具 打印两次是因为使用了StrictMode模式

image.png

在事件中触发会如何?


1
  • 页面展示

image.png

  • 调试器控制台

image.png

//react合成事件
handleClick = () => {
    this.setState(
      {
        num: this.state.num + 1
      },
      () => {
        console.log(this.state.num, '第1次'); //7
      }
    );
    this.setState(
      {
        num: this.state.num + 1
      },
      () => {
        console.log(this.state.num, '第2次'); //7
      }
    );
    this.setState(
      {
        num: this.state.num + 3
      },
      () => {
        console.log(this.state.num, '第3次'); //7
      }
    );

    console.log(this.state.num); // react版本 18.2 输出依然是4
  };
  //dom 原生事件
  //this.btnRef.addEventListener('click', this.nativeHandleClick, false);
  nativeHandleClick = () => {
    this.setState({
      num: this.state.num + 1
    });
    this.setState({
      num: this.state.num + 1
    });
    this.setState({
      num: this.state.num + 3
    });

    console.log(this.state.num); // react版本 18.2 输出依然是4
  };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
  1. react通过setState更新state,这个过程是一个异步调用过程,这中间包括react生命周期、react合成事件、浏览器宏任务例如setTimeout中调用都是异步触发。
  2. react通过setState以函数形式更新,接收一个函数或者第二个回调函数可以接收到更新之后的最新值如上图
  3. 补充: react18版本之前setState在setTimeout或者原生dom事件响应是同步的,这里引入react17.0.2版本测试 (opens new window)

# 更新逻辑

流程图

image.png

源码选择的 React 版本为15.6.2

# 基于React.Class类组件

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState)
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState')
  }
}
1
2
3
4
5
6

# enqueueSetState

enqueueSetState: function(publicInstance, partialState) {
  // 拿到对应的组件实例
  var internalInstance = getInternalInstanceReadyForUpdate(
    publicInstance,
    'setState',
  );
  // queue 对应一个组件实例的 state 数组
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  queue.push(partialState); // 将 partialState 放入待更新 state 队列
  // 处理当前的组件实例
  enqueueUpdate(internalInstance);
}
1
2
3
4
5
6
7
8
9
10
11
12

_pendingStateQueue表示待更新队列

enqueueSetState 做了两件事:

  • 将新的 state 放进组件的状态队列里;
  • 用 enqueueUpdate 来处理将要更新的实例对象。

# enqueueUpdate

function enqueueUpdate(component) {
  ensureInjected()
  // isBatchingUpdates 标识着当前是否处于批量更新过程
  if (!batchingStrategy.isBatchingUpdates) {
    // 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
    batchingStrategy.batchedUpdates(enqueueUpdate, component)
    return
  }
  // 需要批量更新,则先把组件塞入 dirtyComponents 队列
  dirtyComponents.push(component)
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

batchingStrategy 表示批量更新策略,isBatchingUpdates表示当前是否处于批量更新过程,默认是 false。

# batchingStrategy

/**
 *  batchingStrategy源码
 **/
var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false, // 初始值为 false 表示当前并未进行任何批量更新操作

  // 发起更新动作的方法
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates

    ReactDefaultBatchingStrategy.isBatchingUpdates = true

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e)
    } else {
      // 启动事务,将 callback 放进事务里执行
      return transaction.perform(callback, null, a, b, c, d, e)
    }
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

每当 React 调用 batchedUpdate 去执行更新动作时,会先把isBatchingUpdates置为 true,表明正处于批量更新过程中。

看完批量更新整体的管理机制,发现还有一个操作是transaction.perform,这就引出 React 中的 Transaction(事务)机制。

# transaction

 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

根据以上注释,可以看出:一个 Transaction 就是将需要执行的 method 使用 wrapper(一组 initialize 及 close 方法称为一个 wrapper) 封装起来,再通过 Transaction 提供的 perform 方法执行。

在 perform 之前,先执行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 执行后)再执行所有的 close 方法,而且 Transaction 支持多个 wrapper 叠加。这就是 React 中的事务机制。

# batchingStrategy 批量更新策略

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false
  },
}
//  flushBatchedUpdates 将所有的临时 state 合并并计算出最新的 props 及 state
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
}

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]
1
2
3
4
5
6
7
8
9
10
11
12
13

这两个wrapper的initialize都没有做什么事情,但是在callback执行完之后,RESET_BATCHED_UPDATES 的作用是将isBatchingUpdates置为false, FLUSH_BATCHED_UPDATES 的作用是执行flushBatchedUpdates,然后里面会循环所有dirtyComponent,调用updateComponent来执行所有的生命周期方法,componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, render, componentDidUpdate 最后实现组件的更新。

# react 生命周期钩子函数

// ReactMount.js
_renderNewRootComponent: function( nextElement, container, shouldReuseMarkup, context ) {
  // 实例化组件
  var componentInstance = instantiateReactComponent(nextElement);
  // 调用 batchedUpdates 方法
  ReactUpdates.batchedUpdates(
    batchedMountComponentIntoNode,
    componentInstance,
    container,
    shouldReuseMarkup,
    context
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这段代码是在首次渲染组件时会执行的一个方法,可以看到它内部调用了一次 batchedUpdates 方法(将 isBatchingUpdates 设为 true),这是因为在组件的渲染过程中,会按照顺序调用各个生命周期(钩子)函数。如果在函数里面调用 setState,则看下列代码

# React 合成事件

当我们在组件上绑定了事件之后,事件中也有可能会触发 setState。为了确保每一次 setState 都有效,React 同样会在此处手动开启批量更新。看下面代码:

// ReactEventListener.js

dispatchEvent: function (topLevelType, nativeEvent) {
  try {
    // 处理事件:batchedUpdates会将 isBatchingUpdates设为true
    ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  } finally {
    TopLevelCallbackBookKeeping.release(bookKeeping);
  }
}
1
2
3
4
5
6
7
8
9
10

isBatchingUpdates 这个变量,在 React 的生命周期函数以及合成事件执行前,已经被 React 改为 true,这时我们所做的 setState 操作自然不会立即生效。当函数执行完毕后,事务的 close 方法会再把 isBatchingUpdates 改为 false。

# batchedUpdates 方法

看到这里大概就可以了解 setState 的同步异步机制了,接下来让我们进一步体会,可以把 React 的batchedUpdates拿来试试,在该版本中此方法名称被置为unstable_batchedUpdates即不稳定的方法。

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

class App extends Component {
  state = {
    count: 1,
  }

  handleClick = () => {
    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 1

    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 1

    setTimeout(() => {
      batchedUpdates(() => {
        this.setState({
          count: this.state.count + 1,
        })
        console.log(this.state.count) // 2
      })
    })
  }
  render() {
    return (
      <>
        <button onClick={this.handleClick}>加1</button>
        <div>{this.state.count}</div>
      </>
    )
  }
}

export default App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

如果调用batchedUpdates方法,则 isBatchingUpdates变量会被设置为 true,由上述得为 true 走的是批量更新策略,则 setTimeout 里面的方法也变成异步更新了,所以最终打印值为 2,与本文第一道题结果一样。

# fiber架构的更新机制待续

# 总结

isBatchingUpdates 是一个 react 全局唯一的变量,初始值是 false,意味着“当前并未进行任何批量更新操作”,每当 React 去执行更新动作时,会将 isBatchingUpdates 置为 true,表示现在正处于批量更新过程中。置为 true 时,任何需要更新的组件都只能暂时进入 dirtyComponents 里排队等候下一次的批量更新。 isBatchingUpdates 这个变量,在React 的生命周期函数以及合成事件执行前,已经被 React 悄悄修改为了 true,这时我们所做的 setState 操作自然不会立即生效。当函数执行完毕后,事务的 close 方法会再把 isBatchingUpdates 改为 false。 因为 isBatchingUpdates 是在同步代码中变化的,而 setTimeout 的逻辑是异步执行的。当 this.setState 调用真正发生的时候,isBatchingUpdates 早已经被重置为了 false,这就使得当前场景下的 setState 具备了立刻发起同步更新的能力。

# 参考文档

深入 React 的 setState 机制 (opens new window)

揭密React setState (opens new window)

setState 是同步还是异步的? (opens new window)

最后更新时间: 5/17/2023, 7:48:39 PM