React setState 同步异步的魅力

简介: 在之前的一篇文章【React setState 异步真的只是为了性能吗?】中为大家简述了 React setState 异步的一些更较深层次原因,保持一致性和为以后需的架构升级启动并发更新。文章发出之后,也收到了一位学长的思考,原话是“ 除了这个还可以思考什么是 Web,从最初的顶层设计就不可能是同步的,之后的 Fiber 也是要解决 idle 的问题,最后完成资源的完美调度”。这一句话也给出了更深次的见解,在这里感恩,后续会从这些角度深层次的挖掘。昨天聊完 React setState 异步的原因,今天我们来聊聊 React setState 同步异步的魅力。

网络异常,图片无法展示
|


这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战


前言


在之前的一篇文章【React setState 异步真的只是为了性能吗?】中为大家简述了 React setState 异步的一些更较深层次原因,保持一致性为以后需的架构升级启动并发更新。文章发出之后,也收到了一位学长的思考,原话是“ 除了这个还可以思考什么是 Web,从最初的顶层设计就不可能是同步的,之后的 Fiber 也是要解决 idle 的问题,最后完成资源的完美调度”。这一句话也给出了更深次的见解,在这里感恩,后续会从这些角度深层次的挖掘。昨天聊完 React setState 异步的原因,今天我们来聊聊 React setState 同步异步的魅力。


  • 什么时候同步?
  • 什么时候异步?
  • 为什么会有同步?


1. 一道说起 setState 必考的面试题


这道面试题,不仅仅经常出现在 BAT 大厂的面试中,也出现在各类文章中,如下:


import React from "react";
import "./styles.css";
export default class App extends React.Component{
  state = {
    count: 0
  }
    // count +1
  increment = () => {
    console.log('increment setState前的count', this.state.count)
    this.setState({
      count: this.state.count + 1
    });
    console.log('increment setState后的count', this.state.count)
  }
  // count +1 三次
  triple = () => {
    console.log('triple setState前的count', this.state.count)
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
    console.log('triple setState后的count', this.state.count)
  }
    // count - 1
  reduce = () => {
    setTimeout(() => {
      console.log('reduce setState前的count', this.state.count)
      this.setState({
        count: this.state.count - 1
      });
      console.log('reduce setState后的count', this.state.count)
    }, 0);
  }
  render(){
    return <div>
      <button onClick={this.increment}> +1 </button>
      <button onClick={this.triple}> +1 三次 </button>
      <button onClick={this.reduce}> -1 </button>
    </div>
  }
}


测试代码地址:codesandbox.io/s/setstate-…

从左往右依次点击三个按钮,如果你能在脑海中快速的得出结果,哪恭喜你,你对 setState 的同步和异步有了一个了解。在我们最开始学习这个 API 的时候就能清楚的知道 setState 是一个异步的方法,当我们执行完 setState 时,并不会马上去触发状态的更新,所以在 increment 函数中两次输出都是 0 。在 triple 函数中虽然执行了三次 setState,但是批量更新收集三次相同的操作,变成了一个更新操作,在加上 setState 的异步,所以 triple 输出的值,只是在第一步最后变更后的值 1。在来看看第三个函数 reduce ,如果你是一个 React 初学者你可能有一点困惑 setState 竟然是同步更新。不要怀疑他就是同步更新了。哪对于一个老手来说,可能了然于胸,哪为什么会出现有时候是同步更新,有时候又是异步更新,今天我们就来聊聊 setState 异步同步更新的魅力(原理)。


2. 异步的魅力,批量操作的艺术


不管同步异步,setState 在被调用过后, React 做了什么?如果你对 React 版本的更新的历史比较了解,在不同 React 版本 setSatate 触发之后可能会存在一些小的差异,但是整体的思路是一样的。

React15

  • 触发 setState
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

React16.3

  • 触发 setState
  • shouldComponentUpdate
  • render
  • getSnpshotBeforeUpdate
  • componentDidUpdate

React16.4

  • 触发 setState
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

网络异常,图片无法展示
|

从这个简易的流程图可以看到,当触发 setState 之后,会有一个完整的更新流程,涉及了包括 re-render(重渲染) 在内的多个步骤。re-render 本身涉及对 DOM 的操作,它会带来较大的性能开销。假如说“一次 setState 就触发一个完整的更新流程”这个结论成立,那么每一次 setState 的调用都会触发一次 re-render,我们的视图很可能没刷新几次就卡死了。

this.setState({
  count: this.state.count + 1
}); // 触发re-render
this.setState({
  count: this.state.count + 1
}); // 触发re-render
this.setState({
  count: this.state.count + 1
}); // 触发re-render
...
// 页面卡死


说到这里你可能就知道为什么 setState 需要批量操作了。一个重要的原因就是,避免频繁的重渲染。他内部的机制和 Vue 的 $nextTick 和浏览器的 Event-loop 有点类似,多个 setState 执行,就把它塞进一个队列里存储起来,等到这次操作(当前的同步操作)完成,在将在队列中存储的状态(state)做合并,所以无论你执行多少次 setState ,最后只会针对最新的 state 值走一次更新流程,这就是批量操作更新。

this.setState({
  count: this.state.count + 1
});
// 进入队列[count + 1]
this.setState({
  count: this.state.count + 1
});
// 进入队列[count + 1, count + 1]
this.setState({
  count: this.state.count + 1
});
// 进入队列[count + 1, count + 1, count + 1]
...
// 合并state[count + 1]
// 执行count + 1

看到这里你可能已经对 React 的异步更新、批量更新有一定的了解,接着往下看,好戏还在后面。


3. 合成事件


在分析同步场景之前,需要先补充一个很重要的知识点,即 React 的合成事件,同样它也是 React 面试中很容易被考察的点,本文只是抛砖引玉简述 React 合成事件,后面会专门写一篇文章来说说 React 的合成事件。 在说合成事件之前,我们先说说我们最原始的事件委托,事件委托出现的目的更多的是为了性能考虑,举个例子:

<div>
  <div onclick="geText(this)">text 1</div>
  <div onclick="geText(this)">text 2</div>
  <div onclick="geText(this)">text 3</div>
  <div onclick="geText(this)">text 4</div>
  <div onclick="geText(this)">text 5</div>
   // ... 16~9999
  <div onclick="geText(this)">text 10000</div>
</div>


假设一个大的 div 标签下面有 10000 个 小的 div 标签。现在需要添加点击事件,通过点击获取当前 div 标签中的文本。那该如何操作?最简单的操作就是为每一个 内部 div 标签添加 onclick 事件。有 10000 个 div 标签,则会添加 10000 个事件。这是一种非常不友好的方式,会对页面的性能产生影响。所以事件委托起了大作用。通过将事件绑定在 外面大的 div 标签上这样的方式来解决。当 内部 div 点击时,由事件冒泡到父级的标签去触发,并在标签的 onclick 事件中,确认是哪一个标签触发的点击事件。


无独有偶,React 的合成事件也是如此,React 给 document 挂上事件监听;DOM 事件触发后冒泡到 document;React 找到对应的组件,造出一个合成事件出来;并按组件树模拟一遍事件冒泡。这样就有一个问题,就是在一个页面中,只能有一个版本的 React。如果有多个版本,事件就乱套了。


网络异常,图片无法展示
|


但是在 17 版本之后,这个问题得到了解决,事件委托不在挂载到 document 上,而是挂在 DOM 容器上,也就是 ReactDom.Render 所调用的节点上。


网络异常,图片无法展示
|


合成事件与 setState 的触发更新有千丝万缕的关系,也只有在了解合成事件后,我们才能继续聊 同步 setState。


4. 同步背后的故事


回到之前的例子,setState 在 setTimeout 函数的“包谷”之下,有了同步这一“功能”。为什么 setTimeout 可以将 setState 的执行顺序从异步变为同步?


setState 的工作机制


从源码的角度,我们来看看 setState 到底怎么工作的,注意源码 React 版本是 React 15(明古通今)。

// 入口
ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
enqueueSetState: function (publicInstance, partialState) {
  // 获取组件实例
  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
  // 这个 queue 对应的就是一个组件实例的 state 数组
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  // 将新的 state 放进组件的状态队列里
  queue.push(partialState);
  //  enqueueUpdate 用来处理当前的组件实例
  enqueueUpdate(internalInstance);
}
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;
  }
}
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)
    }
  }
}


源码中 isBatchingUpdates 属性直接决定了当下是要走更新流程,还是应该排队等待;其中的 batchedUpdates 方法更是能够直接发起更新流程。由此我们可以大胆推测,batchingStrategy 或许正是 React 内部专门用于管控批量更新的对象。


isBatchingUpdates 上" 锁 "


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

网络异常,图片无法展示
|

在 onClick、onFocus 等事件中,由于合成事件封装了一层,所以可以将 isBatchingUpdates 的状态更新为 true;在 React 的生命周期函数中,同样可以将 isBatchingUpdates 的状态更新为 true。那么在 React 自己的生命周期事件和合成事件中,可以拿到 isBatchingUpdates 的控制权,将状态放进队列,控制执行节奏。而在外部的原生事件中,并没有外层的封装与拦截,无法更新 isBatchingUpdates 的状态为 true。这就造成 isBatchingUpdates 的状态只会为 false,且立即执行。所以在 addEventListener 、setTimeout、setInterval 这些原生事件中都会同步更新。


总结


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


参考


目录
相关文章
|
2月前
|
前端开发 JavaScript
学习react基础(3)_setState、state、jsx、使用ref的几种形式
本文探讨了React中this.setState和this.state的区别,以及React的核心概念,包括核心库的使用、JSX语法、类与函数组件的区别、事件处理和ref的使用。
68 3
学习react基础(3)_setState、state、jsx、使用ref的几种形式
|
3月前
|
资源调度 前端开发 API
React Suspense与Concurrent Mode:异步渲染的未来
React的Suspense与Concurrent Mode是16.8版后引入的功能,旨在改善用户体验与性能。Suspense组件作为异步边界,允许子组件在数据加载完成前显示占位符,结合React.lazy实现懒加载,优化资源调度。Concurrent Mode则通过并发渲染与智能调度提升应用响应性,支持时间分片和优先级调度,确保即使处理复杂任务时UI仍流畅。二者结合使用,能显著提高应用效率与交互体验,尤其适用于数据驱动的应用场景。
72 20
|
3月前
|
存储 前端开发 JavaScript
处理 React 应用程序中的异步数据加载
【8月更文挑战第31天】
63 0
|
4月前
|
存储 前端开发 安全
|
4月前
|
前端开发 JavaScript
react18【系列实用教程】useState —— 声明响应式变量(2024最新版)含useState 的异步更新机制,更新的合并,函数传参获取更新值,不同版本异步更新差异,更新对象和数组
react18【系列实用教程】useState —— 声明响应式变量(2024最新版)含useState 的异步更新机制,更新的合并,函数传参获取更新值,不同版本异步更新差异,更新对象和数组
263 0
|
5月前
|
前端开发
前端React篇之React setState 调用的原理、React setState 调用之后发生了什么?是同步还是异步?
前端React篇之React setState 调用的原理、React setState 调用之后发生了什么?是同步还是异步?
|
6月前
|
存储 前端开发 JavaScript
探索前端框架React Hooks的魅力
【2月更文挑战第2天】本文深入探讨了前端框架React Hooks的核心概念及其在现代Web开发中的重要性,分析了Hooks相较于传统class组件的优势所在,展示了它带来的便利和灵活性,为开发者提供了更加高效和优雅的解决方案。
|
6月前
|
前端开发
说说React中setState和replaceState的区别?
在 React 中,setState()和 replaceState()是用于更新组件状态的两个方法。它们之间有一些区别
49 0
|
18天前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
50 9
|
1月前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。