React transaction完全解读

前言

在阅读react源码中,发现其中大量用到了transaction(中文翻译为事务)这个写法,所以单独做一下分析。
其实在reacttransaction的本质,其实算是一种设计模式,它的思路其实很像AOP切面编程:

给目标函数添加一系列的前置和后置函数,对目标函数进行功能增强或者代码环境保护

接下来进行详细说明。

初识transaction

在日常业务中,经常会遇到这样的场景:

  • 在后台系统需要记录操作日志,这时就需要给很多API添加时间监控功能;
  • 权限验证,在执行某些方法前,要先进行权限校验;
  • 某些涉及到重要全局环境变量的修改,如果中途发生异常,需要保证全局变量正常回滚;

在这些情况下,我们往往需要给一些的函数,添加上类似功能的前置或者后置函数(比如前面说的时间log功能),但是我们又不希望在每次使用到时都重新去写一遍。这时候就要考虑一些技巧方法。

当然这些问题在js里可以用另一种技巧处理--高阶函数,不过不是本文的重点,暂不赘述。

transaction的设计就是为了方便解决这类的问题而产生的。

先看看官方的描述的一个示例图:

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

这个图咋一看挺复杂的,但是实际上并不难:

  1. anyMethod: 代表要被包裹的任意方法;
  2. wrapper: 核心概念之一,简单理解为表示一层包裹容器,每个wrapper可选地包含一个initialize(前置方法)和一个close(后置方法),分别会在每次anyMethod函数执行之前或者之后执行;
  3. perform是执行"包裹"动作的api,通常写成transaction1.perform(anyMethod)的形式,表示给anyMethod加上一层wrapper1;
  4. 可以有多个wrapper,执行时按照"包裹"的顺序,依次执行对应的前置和后置函数;

当然,到这里看不懂也没关系,理论描述毕竟稍显抽象。所以接下来我们通过一个简单的demo来介绍一下transaction-- 我们写一个简化版的log功能的transaction:

// 这里import的Transaction文件其实是React15.6源码里的react-15.6.0/src/renderers/shared/utils/Transaction.js
import React,{Component} from 'react';
import Transaction from './Transaction';

// 1. 定义一个wrapper 在这个例子中,它的功能是:在目标函数执行前后,打印时间戳
// initialize表示在目标函数之前执行
// close表示在目标函数完成之后执行
const TRANSACTION_WRAPPERS = [{
    initialize:function (){
        console.log('log begin at'+ new Date().getTime())
    },
    close:function (){
        console.log('log end at'+ new Date().getTime())
    },
}];

// 2.定义最基本的LogTransaction类 `reinitializeTransaction`是Transaction基本方法在,后面源码部分会详述
function LogTransaction(){
    this.reinitializeTransaction();
}

// 3. LogTransaction继承Transaction 这里的getTransactionWrappers也是Transaction基本方法,在后面源码部分会详述
Object.assign(LogTransaction.prototype, Transaction, {
    getTransactionWrappers: function() {
      return TRANSACTION_WRAPPERS;
    },
});

// 实例化一个我们定义的transaction
var transaction = new LogTransaction();

class Main extends Component {
    // 目标函数 一个简单的say hello
    sayHello(){
        console.log('Hello,An ge')
    }
    
    handleClick = () =>{
        // 使用transaction.perform完成包裹
        transaction.perform(this.sayHello)
    }
    
    render() {
        return (
            <div>
               <button onClick={this.handleClick}>say Hello</button>      
            </div>
        );
    }
}
ReactDOM.render(
    <Main />,
    document.getElementById('root')
);

通过perform包裹sayHello以后,每次点击按钮,在浏览器就可以得到这样的结果:
demo1.png

React transaction源码解读

基本API

在前面的例子中,已经用到了其中几个api,分别是:

  • getTransactionWrappers: 给transaction添加wrappers的方法,每个wrapper可选地(前后置函数都可以为空)包含initializeclose方法;
  • perform: 用于对目标函数完成【包裹】动作;
  • reinitializeTransaction: 用于初始化和每次重新初始化

接下来我们看一下源码是如何实现的:
文件地址:react-15.6.0/src/renderers/shared/utils/Transaction.js

在react中 事务被加上了一个隐含条件:不允许调用一个正在运行的事务。

先呈上完整的源码部分,可以大概过一下,然后跟着下面的解析来仔细阅读。

// 为了方便阅读 稍微去掉了一些ts相关的代码和一些注释

// invariant库 是用来处理错误抛出的 不必深究
var invariant = require('invariant');

// OBSERVED_ERROR只是一个flag位,后面会解释
var OBSERVED_ERROR = {};
var TransactionImpl = {
  // 初始化和重新初始化都会调用reinitializeTransaction
  //`wrapperInitData`用于后面的错误处理的,可以先不理会
  reinitializeTransaction: function() {
    this.transactionWrappers = this.getTransactionWrappers();
    if (this.wrapperInitData) {
      this.wrapperInitData.length = 0;
    } else {
      this.wrapperInitData = [];
    }
    this._isInTransaction = false;
  },

  _isInTransaction: false, // 标志位,表示当前事务是否正在进行
  getTransactionWrappers: null, // getTransactionWrappers前面提到过,需要使用时手动重写,所以这里是null

  // 成员函数,简单工具用于判断当前tracsaction是否在执行中
  isInTransaction: function() {
    return !!this._isInTransaction;
  },
  
  // 核心函数之一,用于实现【包裹动作的函数】
  perform: function(method, scope, a, b, c, d, e, f) {
    /* eslint-enable space-before-function-paren */
    invariant(
      !this.isInTransaction(),
      'Transaction.perform(...): Cannot initialize a transaction when there ' +
        'is already an outstanding transaction.',
    );
    // 用于标记是否抛出错误
    var errorThrown;
    // 方法执行的返回值
    var ret;
    try {
      // 标记当前是否已经处于某个事务中
      this._isInTransaction = true;
      // Catching errors makes debugging more difficult, so we start with
      // errorThrown set to true before setting it to false after calling
      // close -- if it's still set to true in the finally block, it means
      // one of these calls threw.
      errorThrown = true;
      // initializeAll
      this.initializeAll(0);
      ret = method.call(scope, a, b, c, d, e, f); 
      // 如果method执行错误 这句就不会被正常执行
      errorThrown = false;
    } finally {
      try {
        if (errorThrown) {
          // If `method` throws, prefer to show that stack trace over any thrown
          // by invoking `closeAll`.
          try {
            this.closeAll(0);
          } catch (err) {}
        } else {
          // Since `method` didn't throw, we don't want to silence the exception
          // here.
          this.closeAll(0);
        }
      } finally {
        this._isInTransaction = false;
      }
    }
    return ret;
  }, 

  //  执行所有的前置函数
  initializeAll: function(startIndex){
    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 {
            this.initializeAll(i + 1);
          } catch (err) {}
        }
      }
    }
  },
  
  
  // 执行所有的后置函数
  closeAll: function(startIndex) {
    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) {
          wrapper.close.call(this, initData);
        }
        errorThrown = false;
      } finally {
        if (errorThrown) {
          try {
            this.closeAll(i + 1);
          } catch (e) {}
        }
      }
    }
    this.wrapperInitData.length = 0;
  },
};

module.exports = TransactionImpl;

终极解析

虽然咋一看有点复杂,但是不要慌,泡杯茶,沉心静气,这一段代码不难,但有不少细节,请务必保持耐心。接下来我们按照前面demo的执行顺序,对源码进行解析:

  1. reinitializeTransaction: 这个方法做了以下事情:

    1. this.transactionWrappers = this.getTransactionWrappers(),获取所有的wrappers.
    2. if分支语句,清空wrapperInitData数组,wrapperInitData是在后面用于错误处理的,后面会详述
    3. this._isInTransaction = false; 初始化事务状态,_isInTransactionfalse表示当前处于"解锁"状态,可以进行一个事务
  2. perform: 核心方法,这里逐行进行分析:

    1. 这个函数的前两个参数methodscope表示需要被包裹的目标函数执行上下文,很容易懂;后面的abcdef是可选的额外传参,看到后面的method.call(scope, a, b, c, d, e, f);就知道了,这里经常被质疑的一点是为什么不用apply和数组传参代替call来更优雅的实现呢,原因是在react源码里,目前6个参数确实够用了,开发者懒得改(没错,原因就是这么简单)
    2. 接下来开局一个先手invariant判断当前是否已经处于一个进行Transaction,如果是,则终止并抛出错误信息,这一点前面稍微提到了:react中的事务,隐含的被加上一个条件--不允许调用一个正在运行的事务。 这样设定的原因,归根结底还是因为js里最让人头疼的异步问题 --因为react中需要使用transaction来处理dom更新的过程,所以添加上这个条件用来保证异步操作不会影响流程的正常进行。
    3. 接下来的两个try很有意思,来逐句仔细品味一下:
         try {
          // 标记当前已经处于某个事务中 相当于给进程”加锁“
          this._isInTransaction = true;
          // 这里用了一个比较优雅的错误捕获技巧,初始地设置errorThrown为true 表示已经有错误发生
          errorThrown = true;
    
          //initializeAll执行所有的前置函数 为什么参数传入0后面深入分析
          this.initializeAll(0);
    
          // 执行目标方法
          ret = method.call(scope, a, b, c, d, e, f); 
    
          // 关键句:如果上一行method执行错误 下面这行代码就不会被正常执行
          // 那么在后面的`finally`中的`errorThrown`就会为`true`,代表确实有错误抛出;
          // 反之,这行代码正常执行,表示没有错误,这里的写法简洁而优雅
          errorThrown = false;
        } finally {
          // 进入这个finally之后,根据前面是否抛出异常 进入不同分支:
          try {
            if (errorThrown) {
              // 如果前面函数的执行发生了错误,也依然要执行所有的后置方法, 但是此时可以吃掉closeAll抛出的异常
              try {
                //执行所有的后置函数
                this.closeAll(0);
              } catch (err) {}
            } else {
              // 如果前面函数正常执行,那么直接执行所有的后置函数 并且不需要吃掉closeAll抛出的异常
              this.closeAll(0);
            }
          } finally {
             // 最后都要把当前rtacnsaction还原为解锁状态
            this._isInTransaction = false;
          }
        }

    这段代码里,可能有读者对于if(errorThrown)这个分支的代码有疑问:既然始终都要执行this.closeAll,那么为什么errorThrown为true时需要加try catch来捕获this.closeAll可能抛出的异常呢?

    其实是这样的:对于一个transaction来说,错误有可能发生在:

    1. 前置函数
    2. 目标方法
    3. 后置函数

      但是transaction只需要抛出它遇到的第一个error就可以让开发者正常调试了,因此上文的条件判断里,如果errorThrowntrue,说明method.call执行已经已经抛出了异常,那么this.closeAll的异常就应该被捕获(吃掉)而不用抛出,因此这个分支里加上了try..catch

  3. initializeAll&initializeAllcloseAllinitializeAll实际上基本是一样的,所以这里只对initializeAll进行逐行解析:
    initializeAll: function(startIndex){
    var transactionWrappers = this.transactionWrappers;
    // 首先是一个平平无奇的循环
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      try {
        // 这里采用了和perform类似的错误处理思路:
        // 先把this.wrapperInitData[i]指向OBSERVED_ERROR对象,这是一个特定空对象,单纯用来做标记的
        this.wrapperInitData[i] = OBSERVED_ERROR;
        
        // 这里是类似的把戏:如果wrapper.initialize存在,那么this.wrapperInitData[i]会被指向为wrapper.initialize.call的执行结果,结果无论是什么,肯定都不是OBSERVED_ERROR了
        // 如果wrapper.initialize不存在,this.wrapperInitData[i]指向null
        // 当wrapper.initialize存在且wrapper.initialize.call执行出错时,this.wrapperInitData[i]就不会被重新赋值,即this.wrapperInitData[i] === OBSERVED_ERROR
        this.wrapperInitData[i] = wrapper.initialize
          ? wrapper.initialize.call(this)
          : null;
      } finally {
        // 根据前面的代码 如果这里为true ,则表示wrapper.initialize.call执行抛出了异常,此时要保证循环执行 
        // 但是和perform类似的 需要吞吃后续其他wrapper的initialize执行可能抛出的异常,理由是一样的
        // 如果这里为false 那直接继续正常进行正常的for循环
        if (this.wrapperInitData[i] === OBSERVED_ERROR) {
          try {
            this.initializeAll(i + 1);
          } catch (err) {}
        }
      }
    }
  }
  

这里的代码其实也不难,核心部分的逻辑:

this.wrapperInitData[i] = wrapper.initialize
          ? wrapper.initialize.call(this)
          : null;

这里是类似前面perform错误处理的把戏:

  • 如果wrapper.initialize存在且wrapper.initialize.call执行没有出错,那么this.wrapperInitData[i]会被指向为wrapper.initialize.call的执行结果,结果无论是什么,肯定都不是OBSERVED_ERROR了;
  • 如果wrapper.initialize不存在,this.wrapperInitData[i]指向null
  • wrapper.initialize存在且wrapper.initialize.call执行出错时,this.wrapperInitData[i]不会被重新赋值,此时进入this.wrapperInitData[i] === OBSERVED_ERROR分支

后面依然是错误吞吃的逻辑,可以看代码上的说明。

其实到这里,核心的源码就已经基本讲完了,可以看到稍微复杂的也就是其中的错误捕获,阅读源码最重要的就是三点:耐心,耐心,耐心。如果对错误捕获不够清晰,推荐直接拷贝前面的demo的源码 然后分别在前置函数目标方法后置函数中尝试代码错误,然后进入debugger查看。

顺便留个task吧,大家可以带入检测自己的源码理解程度:

  1. 如果某个前置函数出现error,transaction的其他部分(perform 、后置函数们)会怎么执行?
  2. 如果perform出现错误,后置函数们会怎么执行?
  3. 如果某个后置函数出现错误,剩余的后置函数会怎么执行?

延伸-transactionreact中的实际使用

文件路径:react-15.6.0/src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js

下面这段代码其实就是上一篇文章,关于BatchingStrategy的实现,RESET_BATCHED_UPDATESwrapper只定义了一个close方法,是保证每次isBatchingUpdates都能恢复为false。这里就是最前面提到的transaction的其中一个作用:保护代码环境,即使某一次batchUpdate执行过程出错,也不会影响后续的进行。

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
});

var transaction = new ReactDefaultBatchingStrategyTransaction();

源码里其他地方还有transaction的使用,有兴趣的同学可以自行阅读。

小结

总结一下本文的主要内容:

  1. transaction其实是一个设计模式或者说设计方法,核心概念就是针对目标函数(anyMethod)设定一套或者多套前置后置函数(每一套就是一个wrapper),从而实现想要的增强或者保护功能。
  2. 每个wrapper可选的可以有一个initialise和一个close方法
  3. react中的transaction设置了“进程锁”的概念,不允许执行一个正在进行中的transaction
  4. react中的transaction对错误处理有很优雅的方式,值得学习一下。

写在最后

本文是前一篇文章的填坑之作,大概率也是2019年的最后一篇产出(作为是一个懒惰的靓仔,希望自己2020年勤快一些!),希望阅读后能对大家有帮助!如果有什么错误或者疑问欢迎指出!

最后,也没什么好说的,就给大家拜个早年吧!(溜

阅读 1.6k

推荐阅读
前端路漫漫
用户专栏

路漫漫其修远兮 吾将上下左右东西南北中发白而求索

1182 人关注
34 篇文章
专栏主页