前言
在阅读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
对错误处理有很优雅的方式,值得学习一下。
写在最后
本文是前一篇文章的填坑之作,大概率也是2019年的最后一篇产出(作为是一个懒惰的靓仔,希望自己2020年勤快一些!),希望阅读后能对大家有帮助!如果有什么错误或者疑问欢迎指出!
最后,也没什么好说的,就给大家拜个早年吧!(溜
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。