深入理解 React setState

一、为什么使用 setState

React 修改 state 方法有两种:
1、构造函数里修改 state ,只需要直接操作 this.state 即可, 如果在构造函数里执行了异步操作,就需要调用 setState 来触发重新渲染。
2、在其余的地方需要改变 state 的时候只能使用 setState,这样 React 才会触发 UI 更新,如果在其余地方直接修改 state 中的值,会报错:

this.state.counter += 1   // Do not mutate state directly. Use setState()

React 不能直接通过修改 state 的值来使界面发生更新,原因如下:

1、React 并没有实现类似于 Vue2 的 Object.defineProperty 或者 Vue3 的 Proxy 的方式来监听数据的变化;
2、直接修改 state 时 React 并不知道数据发生了变化,需通过 setState 来告知 React 数据已经发生了变化;

二、setState 是同步还是异步的

先来看 React 官网对于 setState 的说明:

将 setState() 认为是一次请求而不是一次立即执行更新组件的命令。为了更为可观的性能,React 可能会推迟它,稍后会一次性更新这些组件。React 不会保证在 setState 之后,能够立刻拿到改变的结果。

以上说明 setState 本身并不是异步的,只是因为 React 的性能优化机制将其体现为异步。

1、为什么大部分情况下是异步的?

来看以下这样一段代码执行:

for (let i = 0; i < 100; i++) {
  this.setState({ num: this.state.num + 1 });
}

如果此时 setState 同步执行,那么这个组件会被重新渲染 100 次,非常耗性能。

总结:
如果所有 setState 是同步的,意味着每执行一次 setState 时(一个方法中可能多次调用 setState),都重新 vnode diff + dom 修改,这对性能来说是极为不好的。
如果是异步,则可以把一个同步代码中的多个 setState 合并成一次组件更新。

2、什么情况下异步?

在组件生命周期或 React 合成事件中,setState 是异步的,例如:

state = {
  number: 1
};

componentDidMount(){
  this.setState({ number: 3 })
  console.log(this.state.number)  // 1
}

上述例子调用了 setState 后输出 number 的值,仍为 1,这看似异步的行为,实则是因为 React 框架本身的性能机制所导致的。
因为每次调用 setState 都会触发更新,异步操作是为了提高性能,将多个状态合并一起更新,减少 re-render 调用。

3、什么情况下同步

在回调函数、setTimeout 或原生 dom 事件中,setState 是同步的;

① 通过回调函数的方法

setState 第二个参数提供回调函数供开发者使用,在回调函数中,我们可以实时的获取到更新之后的数据,例如:

state = {
  number: 1
};

componentDidMount(){
  this.setState({ number: 3 }, () => {
    console.log(this.state.number)  // 3
  })
}

上述例子调用了 setState 后输出 number 的值就为 3 了,我们也就实时的获取到了最新的数据。

② 通过 setTimeout 的方法

上面我们讲到了,setState 本身并不是一个异步方法,其之所以会表现出一种异步的形式,是因为 React 框架本身的一个性能优化机制。
那么基于这一点,如果我们能够越过 React 的机制,是不是就可以令 setState 以同步的形式体现了呢~

state = {
  number: 1
};

componentDidMount(){
  setTimeout(() => {
    this.setState({ number: 3 })
    console.log(this.state.number)  // 3
  }, 0)
}

上述例子调用了 setState 后输出 number 的值也是最新的数据 3,这也完美的印证了我们的猜想是正确的。

③ 通过原生事件中修改状态的方法

上面已经印证了避过 React 的机制,可以同步获取到更新之后的数据,那么除了 setTimeout 外,在原生事件中也是可以的:

state = {
  number: 1
};

componentDidMount() {
  document.body.addEventListener('click', this.changeVal, false);
}

changeVal = () => {
  this.setState({
    number: 3
  })
  console.log(this.state.number)  // 3
}

经过实践,同样这种方法也是可行的。

三、调用 setState 发生了什么

setState 设置 state 数据时的流程图:

1、setState 过程

下面来看下每一步的源码,首先是 setState 入口函数:

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

入口函数在这里就是充当一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去。这里我们以对象形式的入参为例,可以看到它直接调用了 this.updater.enqueueSetState 这个方法。

2、enqueueSetState 过程

enqueueSetState: function (publicInstance, partialState) {
  // 根据 this 拿到对应的组件实例
  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
  // 这个 queue 对应的就是一个组件实例的 state 数组
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  queue.push(partialState);
  //  enqueueUpdate 用来处理当前的组件实例
  enqueueUpdate(internalInstance);
}

这里 enqueueSetState 做了两件事:

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

3、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;
  }
}

这个 enqueueUpdate 引出了一个关键的对象——batchingStrategy,该对象所具备的 isBatchingUpdates 属性直接决定了当下是要走更新流程,还是应该排队等待;其中的 batchedUpdates 方法更是能够直接发起更新流程。由此可以推测,batchingStrategy 或许正是 React 内部专门用于管控批量更新的对象。

4、batchingStrategy 过程

var ReactDefaultBatchingStrategy = {
  // 全局唯一的锁标识
  isBatchingUpdates: false,
  // 发起更新动作的方法
  batchedUpdates: function(callback, a, b, c, d, e) {
    // 缓存锁变量
    var alreadyBatchingStrategy = ReactDefaultBatchingStrategy. isBatchingUpdates
    // 把锁“锁上”
    ReactDefaultBatchingStrategy. isBatchingUpdates = true
    if (alreadyBatchingStrategy) {
      callback(a, b, c, d, e)
    } else {
      // 启动事务,将 callback 放进事务里执行
      transaction.perform(callback, null, a, b, c, d, e)
    }
  }
}

batchingStrategy 对象可以理解为它是一个 “锁管理器”。

这里的 “锁”,是指 React 全局唯一的 isBatchingUpdates 变量,isBatchingUpdates 的初始值是 false,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate 去执行更新动作时,会先把这个锁给“锁上”(置为 true),表明“现在正处于批量更新过程中”。当锁被“锁上” 的时候,任何需要更新的组件都只能暂时进入 dirtyComponents 里排队等候下一次的批量更新,而不能随意 “插队”。此处体现的“任务锁” 的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。

五、总结

1、React 什么情况下可以直接修改 state?

在类组件的构造函数中可以直接修改 state ,只需要直接操作 this.state 即可。

2、setState 是同步更新还是异步更新的?

setState 并不是单纯同步 / 异步的,它的表现会因调用场景的不同而不同:在 React 钩子函数及合成事件中,它表现为异步;而在 setTimeout、setInterval 和原生 dom 事件等情况下,它都表现为同步。这种差异,本质上是由 React 事务机制和批量更新机制的工作方式来决定的。

3、setState 是判断同步还是异步的原理?

setState 源码中,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。

4、isBatchingUpdates 什么情况下为 true?

① 在 React 可以控制的地方,isBatchingUpdates 就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。
② 在 React 无法控制的地方,比如原生事件,具体就是在 addEventListenersetTimeoutsetInterval 等事件中,就只能同步更新。

5、问题扩展:

以下代码输出什么?

class Test extends React.Component {
  state = {
    count: 0
  };
  componentDidMount() {
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count);  // 输出
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count);  // 输出
    setTimeout(() => {
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count);  // 输出
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count);  // 输出
    }, 0);
  }
  render() {
    return null;
  }
};
  • 首先第一次和第二次的 console.log,都在 React 的生命周期事件中,所以是异步的处理方式,则输出都为 0;
  • 而在 setTimeout 中的 console.log 处于原生事件中,所以会同步的处理再输出结果,但需要注意,虽然 count 在前面经过了两次的 this.state.count + 1,但是每次获取的 this.state.count 都是初始化时的值,也就是 0;
  • 所以此时 count 是 1,那么后续在 setTimeout 中的输出则是 2 和 3。

所以输出 0,0,2,3

posted @ 2021-05-20 22:34  Leophen  阅读(380)  评论(0编辑  收藏  举报