React transaction完全解读

更新日期: 2019-11-16阅读: 1.7k标签: 源码

前言

在阅读react源码中,发现其中大量用到了transaction(中文翻译为事务)这个写法,所以单独做一下分析。
其实在react中transaction的本质,其实算是一种设计模式,它的思路其实很像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    |
 *                    +-----------------------------------------+

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

anyMethod: 代表要被包裹的任意方法;
wrapper: 核心概念之一,简单理解为表示一层包裹容器,每个wrapper可选地包含一个initialize(前置方法)和一个close(后置方法),分别会在每次anyMethod函数执行之前或者之后执行;
perform是执行"包裹"动作的api,通常写成transaction1.perform(anyMethod)的形式,表示给anyMethod加上一层wrapper1;
可以有多个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以后,每次点击按钮,在浏览器就可以得到这样的结果:


React transaction源码解读

基本API

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

getTransactionWrappers: 给transaction添加wrappers的方法,每个wrapper可选地(前后置函数都可以为空)包含initialize和close方法;
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的执行顺序,对源码进行解析:

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

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

perform: 核心方法,这里逐行进行分析:

这个函数的前两个参数method和scope表示需要被包裹的目标函数执行上下文,很容易懂;后面的abcdef是可选的额外传参,看到后面的method.call(scope, a, b, c, d, e, f);就知道了,这里经常被质疑的一点是为什么不用apply和数组传参代替call来更优雅的实现呢,原因是在react源码里,目前6个参数确实够用了,开发者懒得改(没错,原因就是这么简单)
接下来开局一个先手invariant判断当前是否已经处于一个进行Transaction,如果是,则终止并抛出错误信息,这一点前面稍微提到了:react中的事务,隐含的被加上一个条件--不允许调用一个正在运行的事务。 这样设定的原因,归根结底还是因为js里最让人头疼的异步问题 --因为react中需要使用transaction来处理dom更新的过程,所以添加上这个条件用来保证异步操作不会影响流程的正常进行。
接下来的两个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来说,错误有可能发生在:

前置函数
目标方法
后置函数

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

initializeAll&initializeAll:closeAll和initializeAll实际上基本是一样的,所以这里只对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吧,大家可以带入检测自己的源码理解程度:

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


延伸-transaction在react中的实际使用

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

下面这段代码其实就是上一篇文章,关于BatchingStrategy的实现,RESET_BATCHED_UPDATES这wrapper只定义了一个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的使用,有兴趣的同学可以自行阅读。


小结

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

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


链接: https://www.fly63.com/article/detial/6868

Js中的 forEach 源码

在日常 Coding 中,码农们肯定少不了对数组的操作,其中很常用的一个操作就是对数组进行遍历,查看数组中的元素,然后一顿操作猛如虎。今天暂且简单地说说在 JavaScript 中 forEach。

微信小程序代码源码案例大全

克隆项目代码到本地(git应该都要会哈,现在源码几乎都会放github上,会git才方便,不会的可以自学一下哦,不会的也没关系,gitHub上也提供直接下载的链接);打开微信开发者工具;

Node 集群源码初探

随着这些模块逐渐完善, Nodejs 在服务端的使用场景也越来越丰富,如果你仅仅是因为JS 这个后缀而注意到它的话, 那么我希望你能暂停脚步,好好了解一下这门年轻的语言,相信它会给你带来惊喜

Vue源码之实例方法

在 Vue 内部,有一段这样的代码:上面5个函数的作用是在Vue的原型上面挂载方法。initMixin 函数;可以看到在 initMixin 方法中,实现了一系列的初始化操作,包括生命周期流程以及响应式系统流程的启动

vue源码解析:nextTick

nextTick的使用:vue中dom的更像并不是实时的,当数据改变后,vue会把渲染watcher添加到异步队列,异步执行,同步代码执行完成后再统一修改dom,我们看下面的代码。

React源码解析之ReactDOM.render()

React更新的方式有三种:(1)ReactDOM.render() || hydrate(ReactDOMServer渲染)(2)setState(3)forceUpdate;接下来,我们就来看下ReactDOM.render()源码

React源码解析之ExpirationTime

在React中,为防止某个update因为优先级的原因一直被打断而未能执行。React会设置一个ExpirationTime,当时间到了ExpirationTime的时候,如果某个update还未执行的话,React将会强制执行该update,这就是ExpirationTime的作用。

扒开V8引擎的源码,我找到了你们想要的前端算法

算法对于前端工程师来说总有一层神秘色彩,这篇文章通过解读V8源码,带你探索 Array.prototype.sort 函数下的算法实现。来,先把你用过的和听说过的排序算法都列出来:

jQuery源码之extend的实现

extend是jQuery中一个比较核心的代码,如果有查看jQuery的源码的话,就会发现jQuery在多处调用了extend方法。作用:对任意对象进行扩;’扩展某个实例对象

vuex源码:state及strict属性

state也就是vuex里的值,也即是整个vuex的状态,而strict和state的设置有关,如果设置strict为true,那么不能直接修改state里的值,只能通过mutation来设置

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!