在阅读react源码中,发现其中大量用到了transaction(中文翻译为事务)这个写法,所以单独做一下分析。
其实在react中transaction的本质,其实算是一种设计模式,它的思路其实很像AOP切面编程:
给目标函数添加一系列的前置和后置函数,对目标函数进行功能增强或者代码环境保护。
接下来进行详细说明。
在日常业务中,经常会遇到这样的场景:
在这些情况下,我们往往需要给一些的函数,添加上类似功能的前置或者后置函数(比如前面说的时间log功能),但是我们又不希望在每次使用到时都重新去写一遍。这时候就要考虑一些技巧方法。
当然这些问题在js里可以用另一种技巧处理--高阶函数,不过不是本文的重点,暂不赘述。
transaction的设计就是为了方便解决这类的问题而产生的。
先看看官方的描述的一个示例图:
* wrappers (injected at creation time)
* + +
* | |
* +-----------------|--------|--------------+
* | v | |
* | +---------------+ | |
* | +--| wrapper1 |---|----+ |
* | | +---------------+ v | |
* | | +-------------+ | |
* | | +----| wrapper2 |--------+ |
* | | | +-------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +---------+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +---------+ +---+ +---+ |
* | initialize close |
* +-----------------------------------------+
这个图咋一看挺复杂的,但是实际上并不难:
当然,到这里看不懂也没关系,理论描述毕竟稍显抽象。所以接下来我们通过一个简单的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以后,每次点击按钮,在浏览器就可以得到这样的结果:
在前面的例子中,已经用到了其中几个api,分别是:
接下来我们看一下源码是如何实现的:
文件地址: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: 这个方法做了以下事情:
perform: 核心方法,这里逐行进行分析:
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: 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错误处理的把戏:
后面依然是错误吞吃的逻辑,可以看代码上的说明。
其实到这里,核心的源码就已经基本讲完了,可以看到稍微复杂的也就是其中的错误捕获,阅读源码最重要的就是三点:耐心,耐心,耐心。如果对错误捕获不够清晰,推荐直接拷贝前面的demo的源码 然后分别在前置函数,目标方法,后置函数中尝试代码错误,然后进入debugger查看。
顺便留个task吧,大家可以带入检测自己的源码理解程度:
文件路径: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的使用,有兴趣的同学可以自行阅读。
总结一下本文的主要内容:
在日常 Coding 中,码农们肯定少不了对数组的操作,其中很常用的一个操作就是对数组进行遍历,查看数组中的元素,然后一顿操作猛如虎。今天暂且简单地说说在 JavaScript 中 forEach。
克隆项目代码到本地(git应该都要会哈,现在源码几乎都会放github上,会git才方便,不会的可以自学一下哦,不会的也没关系,gitHub上也提供直接下载的链接);打开微信开发者工具;
随着这些模块逐渐完善, Nodejs 在服务端的使用场景也越来越丰富,如果你仅仅是因为JS 这个后缀而注意到它的话, 那么我希望你能暂停脚步,好好了解一下这门年轻的语言,相信它会给你带来惊喜
在 Vue 内部,有一段这样的代码:上面5个函数的作用是在Vue的原型上面挂载方法。initMixin 函数;可以看到在 initMixin 方法中,实现了一系列的初始化操作,包括生命周期流程以及响应式系统流程的启动
nextTick的使用:vue中dom的更像并不是实时的,当数据改变后,vue会把渲染watcher添加到异步队列,异步执行,同步代码执行完成后再统一修改dom,我们看下面的代码。
React更新的方式有三种:(1)ReactDOM.render() || hydrate(ReactDOMServer渲染)(2)setState(3)forceUpdate;接下来,我们就来看下ReactDOM.render()源码
在React中,为防止某个update因为优先级的原因一直被打断而未能执行。React会设置一个ExpirationTime,当时间到了ExpirationTime的时候,如果某个update还未执行的话,React将会强制执行该update,这就是ExpirationTime的作用。
算法对于前端工程师来说总有一层神秘色彩,这篇文章通过解读V8源码,带你探索 Array.prototype.sort 函数下的算法实现。来,先把你用过的和听说过的排序算法都列出来:
extend是jQuery中一个比较核心的代码,如果有查看jQuery的源码的话,就会发现jQuery在多处调用了extend方法。作用:对任意对象进行扩;’扩展某个实例对象
state也就是vuex里的值,也即是整个vuex的状态,而strict和state的设置有关,如果设置strict为true,那么不能直接修改state里的值,只能通过mutation来设置
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!