前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React源码学习入门(五)详解React中的Transaction事务机制

React源码学习入门(五)详解React中的Transaction事务机制

作者头像
孟健
发布2022-09-21 11:23:49
7520
发布2022-09-21 11:23:49
举报
文章被收录于专栏:前端工程

详解React中的Transaction事务机制

什么是React中的事务

其实Transaction这个词对我们开发并不陌生,在数据库中,事务表示的是一个原子化的操作序列,要么全部执行,要么全部不执行,是一个不可分割的工作单位。

我们可以思考一下事务的实现原理,要将多个串行的操作原子化,必然需要在出错的时候,撤销之前操作的能力,也就是需要一个现场保护和还原的机制。

而React之所以取名为Transaction,大概也就是因为在它的initializecloseAPI中,做到了close可以拿到initialize的状态的能力,并且对抛出的异常进行比较到位的处理,它的原理如下:

代码语言:javascript
复制
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+

可以看到React中实现的Transaction其实是AOP思想,对一个函数anyMethod进行切片包裹wrapper,每个wrapper可以实现自己的initializeclose接口,可以嵌套使用。

源码分析

本文基于React v15.6.2版本介绍,原因请参见新手如何学习React源码

Transaction的实现位于src/renderers/utils/Transaction.js

代码语言:javascript
复制
perform: function<
    A,
    B,
    C,
    D,
    E,
    F,
    G,
    T: (a: A, b: B, c: C, d: D, e: E, f: F) => G,
  >(method: T, scope: any, a: A, b: B, c: C, d: D, e: E, f: F): G {
  invariant(
    !this.isInTransaction(),
    'Transaction.perform(...): Cannot initialize a transaction when there ' +
      'is already an outstanding transaction.',
  );
  var errorThrown;
  var ret;
  try {
    // 加锁,只允许同时存在一个相同类型的Transaction
    this._isInTransaction = true;
    errorThrown = true;
    // 执行initialize钩子
    this.initializeAll(0);
    // 执行主体函数
    ret = method.call(scope, a, b, c, d, e, f);
    errorThrown = false;
  } finally {
    try {
      if (errorThrown) {
        try {
          // 执行close钩子
          this.closeAll(0);
        } catch (err) {}
      } else {
        // 执行close钩子
        this.closeAll(0);
      }
    } finally {
      this._isInTransaction = false;
    }
  }
  return ret;
},

这段代码看起来好像占了一定篇幅,其实去掉那些边边角角的try catch,这段代码核心就变成了三句话:

代码语言:javascript
复制
// 执行initialize钩子
this.initializeAll(0);
// 执行主体函数
ret = method.call(scope, a, b, c, d, e, f);
// 执行close钩子
this.closeAll(0);

这三行代码也是Transaction实现的主要能力,在主体函数运行前,先运行initialize钩子,运行之后,执行close钩子。

接下来让我们关注一下实现的细节处理:

  • 多个参数的枚举,是React源码的惯用处理手段,为什么不使用arguments我在上篇文章中已经解释过了,不做赘述。
  • 同一时间只能有一个同类的Transaction在执行,这就是_isInTransaction控制锁的作用,也保证了事务运行过程中不被打断。
  • 在finally的代码中可以看到,无论前面的initialize还是主体函数遇到报错,最后的close一定会执行,抛出的错误则以第一个遇到的错误为准。

接下来看一下initializeAll的实现:

代码语言:javascript
复制
  initializeAll: function(startIndex: number): void {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      try {
        this.wrapperInitData[i] = OBSERVED_ERROR;
        // 执行钩子
        this.wrapperInitData[i] = wrapper.initialize
          ? wrapper.initialize.call(this)
          : null;
      } finally {
        if (this.wrapperInitData[i] === OBSERVED_ERROR) {
          try {
            // 继续执行下一个Wrapper的钩子
            this.initializeAll(i + 1);
          } catch (err) {}
        }
      }
    }
  },

可以看到initializeAll的实现,就是拿到所有的wrapper,执行其中的initialize钩子,值得注意的是,如果有钩子报错了,剩下的wrapper的钩子还是会被执行,结合上面的分析我们可以知道React这样做的原因——保持事务的原子性,有一个操作错误了,需要返回之前的现场,也就是完整的initializeclose钩子都要走一遍,以撤销之前可能已经做的操作。

closeAll的实现与initializeAll的实现类似:

代码语言:javascript
复制
  closeAll: function(startIndex: number): void {
    invariant(
      this.isInTransaction(),
      'Transaction.closeAll(): Cannot close transaction when none are open.',
    );
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      var initData = this.wrapperInitData[i];
      var errorThrown;
      try {
        errorThrown = true;
        if (initData !== OBSERVED_ERROR && wrapper.close) {
          // 执行close钩子
          wrapper.close.call(this, initData);
        }
        errorThrown = false;
      } finally {
        if (errorThrown) {
          try {
            // 继续执行下一个Wrapper的close钩子
            this.closeAll(i + 1);
          } catch (e) {}
        }
      }
    }
    this.wrapperInitData.length = 0;
  },
};

这里需要注意的是,close钩子的传参来源是this.wrapperInitData,也就是上一步initialize执行的时候的返回值,这样才能够做到对现场的保护还原。

最后看一下reinitializeTransaction方法的实现:

代码语言:javascript
复制
  reinitializeTransaction: function(): void {
    this.transactionWrappers = this.getTransactionWrappers();
    if (this.wrapperInitData) {
      this.wrapperInitData.length = 0;
    } else {
      this.wrapperInitData = [];
    }
    this._isInTransaction = false;
  },

这个方法比较简单,就是初始化操作,为什么需要这么一个方法呢?我们可以结合前面一篇对象池的文章来思考,transaction对象也是可以在对象池中复用的,那么每一次复用,都需要重置一下之前的状态,实际上在React中transaction大多也是结合对象池一起用。

如何使用

了解原理之后,使用方式就很容易理解了:

代码语言:javascript
复制
const TestTransaction = function() {
  this.reinitializeTransaction();
};
Object.assign(TestTransaction.prototype, Transaction);
TestTransaction.prototype.getTransactionWrappers = function() {
  return [
    {
      initialize: function() {
        console.log('前置函数执行');
        return 'firstResult';
      },
      close: function(initResult) {
        console.log('后置函数执行');
        console.log(initResult);
      },
    }
  ];
};

const transaction = new TestTransaction();
transaction.perform(() => {
  console.log('主体函数执行')
})

用法上,构造函数默认调用reinitializeTransaction,原型继承自Transaction后,挂载getTransactionWrappers方法,然后执行perform包裹要执行的主体函数就可以了。这个时候主体函数相当于是处于一个事务中执行,会原子化地执行前置和后置函数。

在React中的应用

React中的Transaction不多,总共就5个,但每一个都是核心中的核心:

  • ReactReconcileTransaction
  • ReactServerRendingTransaction
  • ReactNativeReconcileTransaction
  • ReactDefaultBatchingStrategyTransaction
  • ReactUpdatesFlushTransaction

不要小看Transaction在React中的地位,上面的ReactReconcileTransaction恰恰是componentDidMount的关键,而ReactDefaultBatchingStrategyTransaction是实现setState异步化的关键。限于篇幅,具体的transaction我们在后续的应用场景展开介绍。

小结一下

React事务实现可以算是React底层的基石,虽然它只是一个utils,但是React很多非常重要的特性都是依赖于事务的。

事务的实现其实不难,可以简单理解为React仅仅是为方法加了前置和后置函数的钩子,并原子化执行函数,只有理解事务机制后,你才不会在React源码中晕头转向,因为React源码的执行顺序跟事务的钩子有极大的关联。

自此开始,我们也真正迈入了React核心实现的大门!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-08-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 孟健的前端认知 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 详解React中的Transaction事务机制
    • 什么是React中的事务
      • 源码分析
        • 如何使用
          • 在React中的应用
            • 小结一下
            相关产品与服务
            批量计算
            批量计算(BatchCompute,Batch)是为有大数据计算业务的企业、科研单位等提供高性价比且易用的计算服务。批量计算 Batch 可以根据用户提供的批处理规模,智能地管理作业和调动其所需的最佳资源。有了 Batch 的帮助,您可以将精力集中在如何分析和处理数据结果上。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档