React 架构的演变 - 从同步到异步

Posted 更了不起的前端

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React 架构的演变 - 从同步到异步相关的知识,希望对你有一定的参考价值。

写这篇文章的目的,主要是想弄懂 React 最新的 fiber 架构到底是什么东西,但是看了网上的很多文章,要不模棱两可,要不就是一顿复制粘贴,根本看不懂,于是开始认真钻研源码。钻研过程中,发现我想得太简单了,React 源码的复杂程度远超我的想象,于是打算分几个模块了剖析,今天先讲一讲 React 的更新策略从同步变为异步的演变过程。

从 setState 说起

React 16 之所以要进行一次大重构,是因为 React 之前的版本有一些不可避免的缺陷,一些更新操作,需要由同步改成异步。所以我们先聊聊 React 15 是如何进行一次 setState 的。

import React from 'react';

class App extends React.Component {
  state = { val0 }
  componentDidMount() {
    // 第一次调用
    this.setState({ valthis.state.val + 1 });
    console.log('first setState'this.state);

    // 第二次调用
    this.setState({ valthis.state.val + 1 });
    console.log('second setState'this.state);

    // 第三次调用
    this.setState({ valthis.state.val + 1 }, () => {
      console.log('in callback'this.state)
    });
  }
  render() {
    return <div> val: { this.state.val } </div>
  }
}

export default App;

熟悉 React 的同学应该知道,在 React 的生命周期内,多次 setState 会被合并成一次,这里虽然连续进行了三次 setState,state.val 的值实际上只重新计算了一次。

render结果

每次 setState 之后,立即获取 state 会发现并没有更新,只有在 setState 的回调函数内才能拿到最新的结果,这点通过我们在控制台输出的结果就可以证实。

React 架构的演变 - 从同步到异步
控制台输出

网上有很多文章称 setState 是『异步操作』,所以导致 setState 之后并不能获取到最新值,其实这个观点是错误的。setState 是一次同步操作,只是每次操作之后并没有立即执行,而是将 setState 进行了缓存,mount 流程结束或事件操作结束,才会拿出所有的 state 进行一次计算。如果 setState 脱离了 React 的生命周期或者 React 提供的事件流,setState 之后就能立即拿到结果。

我们修改上面的代码,将 setState 放入 setTimeout 中,在下一个任务队列进行执行。

import React from 'react';

class App extends React.Component {
  state = { val0 }
  componentDidMount() {
    setTimeout(() => {
      // 第一次调用
      this.setState({ valthis.state.val + 1 });
      console.log('first setState'this.state);
  
      // 第二次调用
      this.setState({ valthis.state.val + 1 });
      console.log('second setState'this.state);
    });
  }
  render() {
    return <div> val: { this.state.val } </div>
  }
}

export default App;

可以看到,setState 之后就能立即看到state.val 的值发生了变化。

React 架构的演变 - 从同步到异步
控制台输出

为了更加深入理解 setState,下面简单讲解一下React 15 中 setState 的更新逻辑,下面的代码是对源码的一些精简,并非完整逻辑。

旧版本 setState 源码分析

setState 的主要逻辑都在 ReactUpdateQueue 中实现,在调用 setState 后,并没有立即修改 state,而是将传入的参数放到了组件内部的 _pendingStateQueue 中,之后调用 enqueueUpdate 来进行更新。

// 对外暴露的 React.Component
function ReactComponent({
  this.updater = ReactUpdateQueue;
}
// setState 方法挂载到原型链上
ReactComponent.prototype.setState = function (partialState, callback{
  // 调用 setState 后,会调用内部的 updater.enqueueSetState
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

var ReactUpdateQueue = {
  enqueueSetState(component, partialState) {
    // 在组件的 _pendingStateQueue 上暂存新的 state
    if (!component._pendingStateQueue) {
      component._pendingStateQueue = [];
    }
    var queue = component._pendingStateQueue;
    queue.push(partialState);
    enqueueUpdate(component);
  },
  enqueueCallbackfunction (component, callback, callerName{
    // 在组件的 _pendingCallbacks 上暂存 callback
    if (component._pendingCallbacks) {
      component._pendingCallbacks.push(callback);
    } else {
      component._pendingCallbacks = [callback];
    }
    enqueueUpdate(component);
  }
}

enqueueUpdate 首先会通过 batchingStrategy.isBatchingUpdates 判断当前是否在更新流程,如果不在更新流程,会调用 batchingStrategy.batchedUpdates() 进行更新。如果在流程中,会将待更新的组件放入 dirtyComponents 进行缓存。

var dirtyComponents = [];
function enqueueUpdate(component{
  if (!batchingStrategy.isBatchingUpdates) {
   // 开始进行批量更新
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 如果在更新流程,则将组件放入脏组件队列,表示组件待更新
  dirtyComponents.push(component);
}

batchingStrategy 是 React 进行批处理的一种策略,该策略的实现基于 Transaction,虽然名字和数据库的事务一样,但是做的事情却不一样。

class ReactDefaultBatchingStrategyTransaction extends Transaction {
  constructor() {
    this.reinitializeTransaction()
  }
  getTransactionWrappers () {
    return [
      {
        initialize() => {},
        close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
      },
      {
        initialize() => {},
        close() => {
          ReactDefaultBatchingStrategy.isBatchingUpdates = false;
        }
      }
    ]
  }
}

var transaction = new ReactDefaultBatchingStrategyTransaction();

var batchingStrategy = {
  // 判断是否在更新流程中
  isBatchingUpdates: false,
  // 开始进行批量更新
  batchedUpdates: function (callback, component{
    // 获取之前的更新状态
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
  // 将更新状态修改为 true
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    if (alreadyBatchingUpdates) {
      // 如果已经在更新状态中,等待之前的更新结束
      return callback(callback, component);
    } else {
      // 进行更新
      return transaction.perform(callback, null, component);
    }
  }
};

Transaction 通过 perform 方法启动,然后通过扩展的 getTransactionWrappers 获取一个数组,该数组内存在多个 wrapper 对象,每个对象包含两个属性:initializeclose。perform 中会先调用所有的 wrapper.initialize,然后调用传入的回调,最后调用所有的 wrapper.close

class Transaction {
 reinitializeTransaction() {
    this.transactionWrappers = this.getTransactionWrappers();
  }
 perform(method, scope, ...param) {
    this.initializeAll(0);
    var ret = method.call(scope, ...param);
    this.closeAll(0);
    return ret;
  }
 initializeAll(startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      wrapper.initialize.call(this);
    }
  }
 closeAll(startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      wrapper.close.call(this);
    }
  }
}
React 架构的演变 - 从同步到异步
transaction.perform

React 源代码的注释中,也形象的展示了这一过程。

/*
*                       wrappers (injected at creation time)
*                                      +        +
*                                      |        |
*                    +-----------------|--------|--------------+
*                    |                 v        |              |
*                    |      +---------------+   |              |
*                    |   +--|    wrapper1   |---|----+         |
*                    |   |  +---------------+   v    |         |
*                    |   |          +-------------+  |         |
*                    |   |     +----|   wrapper2  |--------+   |
*                    |   |     |    +-------------+  |     |   |
*                    |   |     |                     |     |   |
*                    |   v     v                     v     v   | wrapper
*                    | +---+ +---+   +---------+   +---+ +---+ | invariants
* perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
*                    | |   | |   |   |         |   |   | |   | |
*                    | |   | |   |   |         |   |   | |   | |
*                    | |   | |   |   |         |   |   | |   | |
*                    | +---+ +---+   +---------+   +---+ +---+ |
*                    |  initialize                    close    |
*                    +-----------------------------------------+
*/

我们简化一下代码,再重新看一下 setState 的流程。

// 1. 调用 Component.setState
ReactComponent.prototype.setState = function (partialState{
  this.updater.enqueueSetState(this, partialState);
};

// 2. 调用 ReactUpdateQueue.enqueueSetState,将 state 值放到 _pendingStateQueue 进行缓存
var ReactUpdateQueue = {
  enqueueSetState(component, partialState) {
    var queue = component._pendingStateQueue || (component._pendingStateQueue = []);
    queue.push(partialState);
    enqueueUpdate(component);
  }
}

// 3. 判断是否在更新过程中,如果不在就进行更新
var dirtyComponents = [];
function enqueueUpdate(component{
  // 如果之前没有更新,此时的 isBatchingUpdates 肯定是 false
  if (!batchingStrategy.isBatchingUpdates) {
    // 调用 batchingStrategy.batchedUpdates 进行更新
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  dirtyComponents.push(component);
}

// 4. 进行更新,更新逻辑放入事务中进行处理
var batchingStrategy = {
  isBatchingUpdatesfalse,
  // 注意:此时的 callback 为 enqueueUpdate
  batchedUpdates: function (callback, component{
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    if (alreadyBatchingUpdates) {
      // 如果已经在更新状态中,重新调用 enqueueUpdate,将 component 放入 dirtyComponents
      return callback(callback, component);
    } else {
      // 进行事务操作
      return transaction.perform(callback, null, component);
    }
  }
};

启动事务可以拆分成三步来看:

  1. 先执行 wrapper 的 initialize,此时的 initialize 都是一些空函数,可以直接跳过;
  2. 然后执行 callback(也就是 enqueueUpdate),执行 enqueueUpdate 时,由于已经进入了更新状态, batchingStrategy.isBatchingUpdates 被修改成了 true,所以最后还是会把 component 放入脏组件队列,等待更新;
  3. 后面执行的两个 close 方法,第一个方法的 flushBatchedUpdates 是用来进行组件更新的,第二个方法用来修改更新状态,表示更新已经结束。
getTransactionWrappers () {
  return [
    {
      initialize() => {},
      close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
    },
    {
      initialize() => {},
      close() => {
        ReactDefaultBatchingStrategy.isBatchingUpdates = false;
      }
    }
  ]
}

flushBatchedUpdates 里面会取出所有的脏组件队列进行 diff,最后更新到 DOM。

function flushBatchedUpdates({
  if (dirtyComponents.length) {
    runBatchedUpdates()
  }
};

function runBatchedUpdates({
  // 省略了一些去重和排序的操作
  for (var i = 0; i < dirtyComponents.length; i++) {
    var component = dirtyComponents[i];

    // 判断组件是否需要更新,然后进行 diff 操作,最后更新 DOM。
    ReactReconciler.performUpdateIfNecessary(component);
  }
}

performUpdateIfNecessary() 会调用 Component.updateComponent(),在 updateComponent() 中,会从 _pendingStateQueue 中取出所有的值来更新。

// 获取最新的 state
_processPendingState() {
  var inst = this._instance;
  var queue = this._pendingStateQueue;

  var nextState = { ...inst.state };
  for (var i = 0; i < queue.length; i++) {
    var partial = queue[i];
    Object.assign(
      nextState,
      typeof partial === 'function' ? partial(inst, nextState) : partial
   );
  }
  return nextState;
}
// 更新组件
updateComponent(prevParentElement, nextParentElement) {
  var inst = this._instance;
  var prevProps = prevParentElement.props;
  var nextProps = nextParentElement.props;
  var nextState = this._processPendingState();
  var shouldUpdate = 
      !shallowEqual(prevProps, nextProps) ||
      !shallowEqual(inst.state, nextState);
  
  if (shouldUpdate) {
    // diff 、update DOM
  } else {
    inst.props = nextProps;
    inst.state = nextState;
  }
  // 后续的操作包括判断组件是否需要更新、diff、更新到 DOM
}

setState 合并原因

按照刚刚讲解的逻辑,setState 的时候,batchingStrategy.isBatchingUpdatesfalse 会开启一个事务,将组件放入脏组件队列,最后进行更新操作,而且这里都是同步操作。讲道理,setState 之后,我们可以立即拿到最新的 state。

然而,事实并非如此,在 React 的生命周期及其事件流中,batchingStrategy.isBatchingUpdates 的值早就被修改成了 true。可以看看下面两张图:

React 架构的演变 - 从同步到异步
Mount
React 架构的演变 - 从同步到异步
事件调用

在组件 mount 和事件调用的时候,都会调用 batchedUpdates,这个时候已经开始了事务,所以只要不脱离 React,不管多少次 setState 都会把其组件放入脏组件队列等待更新。一旦脱离 React 的管理,比如在 setTimeout 中,setState 立马变成单打独斗。

Concurrent 模式

React 16 引入的 Fiber 架构,就是为了后续的异步渲染能力做铺垫,虽然架构已经切换,但是异步渲染的能力并没有正式上线,我们只能在实验版中使用。异步渲染指的是 Concurrent 模式,下面是官网的介绍:

Concurrent 模式是 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整。

React 架构的演变 - 从同步到异步
优点

除了 Concurrent 模式,React 还提供了另外两个模式, Legacy 模式依旧是同步更新的方式,可以认为和旧版本保持一致的兼容模式,而 Blocking 模式是一个过渡版本。

React 架构的演变 - 从同步到异步
模式差异

Concurrent 模式说白就是让组件更新异步化,切分时间片,渲染之前的调度、diff、更新都只在指定时间片进行,如果超时就暂停放到下个时间片进行,中途给浏览器一个喘息的时间。

浏览器是单线程,它将 GUI 描绘,时间器处理,事件处理,JS 执行,远程资源加载统统放在一起。当做某件事,只有将它做完才能做下一件事。如果有足够的时间,浏览器是会对我们的代码进行编译优化(JIT)及进行热代码优化,一些 DOM 操作,内部也会对 reflow 进行处理。reflow 是一个性能黑洞,很可能让页面的大多数元素进行重新布局。

浏览器的运作流程: 渲染 -> tasks -> 渲染 -> tasks -> 渲染 -> ....

这些 tasks 中有些我们可控,有些不可控,比如 setTimeout 什么时候执行不好说,它总是不准时;资源加载时间不可控。但一些JS我们可以控制,让它们分派执行,tasks的时长不宜过长,这样浏览器就有时间优化 JS 代码与修正 reflow !

总结一句,就是让浏览器休息好,浏览器就能跑得更快。

-- by 司徒正美 《React Fiber架构》

React 架构的演变 - 从同步到异步
模式差异

这里有个 demo,上面是一个

以上是关于React 架构的演变 - 从同步到异步的主要内容,如果未能解决你的问题,请参考以下文章

实现MySQL半同步架构

异步架构和响应式(Reactive)编程

MySQL 5.7的复制架构,在有异步复制半同步增强半同步MGR等的生产中,该如何选择?

JS周刊#410 - 全面的webpack 4配置示例,Storybook4.0更新,异步JS的演变,React Hooks介绍

1.SpringCloud -- 从单体架构到微服务架构代码拆分(maven 聚合)

MySQL 5.7的复制架构,在有异步复制半同步增强半同步MGR等的生产中,该如何选择?