# setState
# setState
# 写这篇文章的初衷
- react是采用单向数据流的形式,通过
 state-> 视图的更新- 优先处理上层
 props,通过state来维护状态值setState是唯一触发state更新的api- 尽管
 react-redux或者其他的状态库调用更新试图,本质都是通过setStatesetState更新逻辑- 面试
 
# 同步异步问题
- 使用pnpm + vite快速搭建一个react项目
 - 注意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);
  }
 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
- 第一次页面加载效果
 
- 调试工具 打印两次是因为使用了StrictMode模式
 
在事件中触发会如何?
 - 页面展示
 
- 调试器控制台
 
//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
  };
 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
- react通过setState更新state,这个过程是一个异步调用过程,这中间包括react生命周期、react合成事件、浏览器宏任务例如setTimeout中调用都是异步触发。
 - react通过setState以函数形式更新,接收一个函数或者第二个回调函数可以接收到更新之后的最新值如上图
 - 补充: react18版本之前setState在setTimeout或者原生dom事件响应是同步的,这里引入
 react17.0.2版本测试 (opens new window)
# 更新逻辑
流程图
源码选择的 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')
  }
}
 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);
}
 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
  }
}
 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)
    }
  },
}
 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>
 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]
 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
  );
}
 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);
  }
}
 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
 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)