时间: 2019-11-16阅读: 53标签: 源码前言
本文使用的是react 15.6.1的代码
在阅读react源码中,发现其中大量用到了transaction(中文翻译为事务)这个写法,所以单独做一下分析。其实在react中transaction的本质,其实算是一种设计模式,它的思路其实很像AOP切面编程:
在上篇文章组件的渲染中_renderNewRootComponent中最后调用了ReactUpdates.batchedUpdates函数,上文中只是简单的分析了最后实际是在内部调用了其中的第一个参数batchedMountComponentIntoNode,其实并没有那么简单,今天我们就来看看batchedUpdates实际做了些什么内容
给目标函数添加一系列的前置和后置函数,对目标函数进行功能增强或者代码环境保护。
ReactUpdates.batchedUpdates
//批量更新方法
function batchedUpdates(callback, a, b, c, d, e) {
ensureInjected(); //不用理会,单纯的打印方法
return batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
}
实际上是调用了batchingStrategy.batchedUpdates中的方法,但是翻遍了整个文件,发现batchingStrategy这个对象一直是null,那么batchingStrategy是怎么初始化的呢?在ReactUpdates中,发现定义了这样一个对象ReactUpdatesInjection,其中代码如下
var ReactUpdatesInjection = {
injectReconcileTransaction: function(ReconcileTransaction) {
ReactUpdates.ReactReconcileTransaction = ReconcileTransaction;
},
injectBatchingStrategy: function(_batchingStrategy) {
batchingStrategy = _batchingStrategy;
},
};
其中有一个方法injectBatchingStrategy
,那么是不是外部谁调用了该方法,使得最后batchingStrategy被注入进来了,在ReactDOM源码中,我们发现有一行
ReactDefaultInjection.inject(); //注入事件,环境变量,各种参数值
查看ReactDefaultInjection.inject()方法后,又找到了这样一行代码
ReactInjection.Updates.injectBatchingStrategy(ReactDefaultBatchingStrategy);
再次查看ReactInjection中的源代码
var ReactInjection = {
Component: ReactComponentEnvironment.injection,
DOMProperty: DOMProperty.injection,
EmptyComponent: ReactEmptyComponent.injection,
EventPluginHub: EventPluginHub.injection,
EventPluginUtils: EventPluginUtils.injection,
EventEmitter: ReactBrowserEventEmitter.injection,
HostComponent: ReactHostComponent.injection,
Updates: ReactUpdates.injection,
};
也就是说,在ReactDOM之中,会执行到
ReactUpdates.injection.injectBatchingStrategy
方法,同时,将ReactDefaultBatchingStrategy
传入其中,所以最后batchingStrategy = ReactDefaultBatchingStrategy
,那么回到前文batchingStrategy.batchedUpdates(callback, a, b, c, d, e)
,这里面的batchedUpdates
实际就是执行的ReactDefaultBatchingStrategy中的方法
源码地址[ReactDefaultBatchingStrategy]
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
/**
* Call the provided function in a context within which calls to `setState`
* and friends are batched such that components aren't updated unnecessarily.
*/
batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// The code is written this way to avoid extra allocations
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e);//重点代码
}
},
};
代码很简单,第一次调用的时候ReactDefaultBatchingStrategy.isBatchingUpdates肯定是false,那么就会调用transaction.perform方法,并将相应回调传入其中,那么我们来看看ReactDefaultBatchingStrategy相关代码
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();
}
// ReactDefaultBatchingStrategyTransaction.prototype添加Transaction对象的方法,如reinitializeTransaction,同时用新的函数覆盖getTransactionWrappers
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
getTransactionWrappers: function() {
return TRANSACTION_WRAPPERS;
},
});
var transaction = new ReactDefaultBatchingStrategyTransaction();
发现transation是ReactDefaultBatchingStrategyTransaction的实例,但是perform方法是Transaction.js提供的,我们看看Transaction方法
'use strict';
var invariant = require('invariant');
var OBSERVED_ERROR = {};
/**
* `Transaction` creates a black box that is able to wrap any method such that
* certain invariants are maintained before and after the method is invoked
* (Even if an exception is thrown while invoking the wrapped method). Whoever
* instantiates a transaction can provide enforcers of the invariants at
* creation time. The `Transaction` class itself will supply one additional
* automatic invariant for you - the invariant that any transaction instance
* should not be run while it is already being run. You would typically create a
* single instance of a `Transaction` for reuse multiple times, that potentially
* is used to wrap several different methods. Wrappers are extremely simple -
* they only require implementing two methods.
*
* <pre>
* wrappers (injected at creation time)
* + +
* | |
* +-----------------|--------|--------------+
* | v | |
* | +---------------+ | |
* | +--| wrapper1 |---|----+ |
* | | +---------------+ v | |
* | | +-------------+ | |
* | | +----| wrapper2 |--------+ |
* | | | +-------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +---------+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +---------+ +---+ +---+ |
* | initialize close |
* +-----------------------------------------+
* </pre>
*
* Use cases:
* - Preserving the input selection ranges before/after reconciliation.
* Restoring selection even in the event of an unexpected error.
* - Deactivating events while rearranging the DOM, preventing blurs/focuses,
* while guaranteeing that afterwards, the event system is reactivated.
* - Flushing a queue of collected DOM mutations to the main UI thread after a
* reconciliation takes place in a worker thread.
* - Invoking any collected `componentDidUpdate` callbacks after rendering new
* content.
* - (Future use case): Wrapping particular flushes of the `ReactWorker` queue
* to preserve the `scrollTop` (an automatic scroll aware DOM).
* - (Future use case): Layout calculations before and after DOM updates.
*
* Transactional plugin API:
* - A module that has an `initialize` method that returns any precomputation.
* - and a `close` method that accepts the precomputation. `close` is invoked
* when the wrapped process is completed, or has failed.
*
* @param {Array<TransactionalWrapper>} transactionWrapper Wrapper modules
* that implement `initialize` and `close`.
* @return {Transaction} Single transaction for reuse in thread.
*
* @class Transaction
*/
var TransactionImpl = {
reinitializeTransaction: function(): void {
//获取wrappers
this.transactionWrappers = this.getTransactionWrappers();
if (this.wrapperInitData) {
this.wrapperInitData.length = 0;
} else {
this.wrapperInitData = [];
}
this._isInTransaction = false;
},
_isInTransaction: false,
/**
* @abstract
* @return {Array<TransactionWrapper>} Array of transaction wrappers.
* 由具体Transaction来提供,如ReactDefaultBatchingStrategyTransaction
*/
getTransactionWrappers: null,
isInTransaction: function(): boolean {
return !!this._isInTransaction;
},
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 {
var errorThrown;
var ret;
try {
//正在Transaction ing中
this._isInTransaction = true;
errorThrown = true;
this.initializeAll(0);
ret = method.call(scope, a, b, c, d, e, f);
errorThrown = false;
} finally {
try {
//如果initializeAll没有抛出异常的话
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.
//进入close生命周期
this.closeAll(0);
}
} finally {
// Transaction结束
this._isInTransaction = false;
}
}
return ret;
},
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;
//调用wrapper的initialize方法
this.wrapperInitData[i] = wrapper.initialize
? wrapper.initialize.call(this)
: null;
} finally {
//如果调用initialize有问题,则startIndex+1在调用
if (this.wrapperInitData[i] === OBSERVED_ERROR) {
try {
this.initializeAll(i + 1);
} catch (err) {}
}
}
}
},
closeAll: function(startIndex: number): void {
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;
},
};
export type Transaction = typeof TransactionImpl;
module.exports = TransactionImpl;
代码比较少,从代码注释的简图看,在创建Transations的时候,会先注入wrapper,当调用perform方法的时候,会依次调用wrapper的initialize方法,随后在调用perform中传入的方法,最后在调用依次调用wrapper中的close方法,简单看下来,有设计模式(代理)
的思路在里面,我们具体看perform的实现首先,调用了initializeAll
方法,然后循环拿到transactionWrappers依次调用initialize方法,随后调用perform的第一参数,最后又调用closeAll
方法,依次调用transactionWrappers中的close的方法,最后将_isInTransaction
置为false表示transation结束。和图中描述的一样,也就是说,不同的Transation会去实现不同的wrapper来代理具体的方法,那么ReactDefaultBatchingStrategyTransaction
的wrapper是什么?在ReactDefaultBatchingStrategy之中,实例化ReactDefaultBatchingStrategyTransaction
时会调用
this.reinitializeTransaction()方法,该方法中调用了getTransactionWrappers
获取具体的wrappers,对应的,ReactDefaultBatchingStrategyTransaction
重写了getTransactionWrappers
方法,因此,得到返回值
[FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]
这是wrapper的实现
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function() {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
},
};
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};
代码比较简单,两个wraper都只有close方法,根据代码描述,会先执行FLUSH_BATCHED_UPDATES的close方法也就是ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
var flushBatchedUpdates = function() {
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
// 从缓存池中获取ReactUpdatesFlushTransaction对象
var transaction = ReactUpdatesFlushTransaction.getPooled();
// 调用runBatchedUpdates
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
}
if (asapEnqueued) {
asapEnqueued = false;
var queue = asapCallbackQueue;
asapCallbackQueue = CallbackQueue.getPooled();
queue.notifyAll();
CallbackQueue.release(queue);
}
}
};
react运行中当存在脏组件的时候,会用transaction调用runBatchedUpdates
方法,在执行之前,react使用了对象池这样的优化手段来获取/生成transaction对象,该部分暂时跳过,后面在细读,我们在来看看runBatchedUpdates做了些什么
function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
// 排序,保证 dirtyComponent 从父级到子级的 render 顺序
dirtyComponents.sort(mountOrderComparator);
updateBatchNumber++;
for (var i = 0; i < len; i++) {
var component = dirtyComponents[i];
// 获取该组件的回调数组
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;
var markerName;
if (ReactFeatureFlags.logTopLevelRenders) {
var namedComponent = component;
// Duck type TopLevelWrapper. This is probably always true.
if (component._currentElement.type.isReactTopLevelWrapper) {
namedComponent = component._renderedComponent;
}
markerName = 'React update: ' + namedComponent.getName();
console.time(markerName);
}
// 更新该 dirtyComponent 重点代码
ReactReconciler.performUpdateIfNecessary(
component,
transaction.reconcileTransaction,
updateBatchNumber,
);
if (markerName) {
console.timeEnd(markerName);
}
// 如果存在组件回调函数,将其放入回调队列中
if (callbacks) {
for (var j = 0; j < callbacks.length; j++) {
transaction.callbackQueue.enqueue(
callbacks[j],
component.getPublicInstance(),
);
}
}
}
}
代码不多,首先是将脏components进行排序,变成从父组件到子组件的顺序排列,随后遍历组件调用performUpdateIfNecessary更新数据,同时将组件的回调压入回调队列中,等待最后调用,这里我们注意到并不是更新一个组件的时候就调用其回调,而是等到所有组件都更新完以后才去调用,看看核心代码performUpdateIfNecessary方法,内部就一行核心代码internalInstance.performUpdateIfNecessary(transaction)
即调用了ReactComponent对象的performUpdateIfNecessary方法,即ReactCompositeComponent.performUpdateIfNecessary方法
performUpdateIfNecessary: function(transaction) {
if (this._pendingElement != null) {
ReactReconciler.receiveComponent(
this,
this._pendingElement,
transaction,
this._context,
);
} else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
//注意,调用了this.updateComponent方法,但是element以及content都是component自己的即_currentElement,_context
this.updateComponent(
transaction,
this._currentElement,
this._currentElement,
this._context,
this._context,
);
} else {
this._updateBatchNumber = null;
}
}
绕了一圈,最后还是调用组件自己的updateComponent方法,而updateComponent会去执行React组件的生命周期,如componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate,render, componentDidUpdate。完成整套流程。后面我们慢慢介绍React的生命周期。
回到上面代码,刚才知道runBatchedUpdates是有事务去调用的,react事务会先执行wrapper中的init方法,在执行close方法,我们在来看看ReactUpdatesFlushTransaction的wrapper相关代码做了什么。
var NESTED_UPDATES = {
initialize: function() {
this.dirtyComponentsLength = dirtyComponents.length;
},
close: function() {
// 在批量更新,如果有新的dirtyComponents被push,那么,需要再一次批量更新,从新加入的dirtyComponents开始
if (this.dirtyComponentsLength !== dirtyComponents.length) {
dirtyComponents.splice(0, this.dirtyComponentsLength);
flushBatchedUpdates();
} else {
dirtyComponents.length = 0;
}
},
};
var UPDATE_QUEUEING = {
initialize: function() {
// 重置回调队列
this.callbackQueue.reset();
},
close: function() {
// 执行回调方法
this.callbackQueue.notifyAll();
},
};
var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];
看到上面代码,不得不感叹react设计的巧妙,这里巧妙的使用了transition的生命周期,在initialize时获取当前dirtyComponent的个数,最后结束(close)的时候再次校验,如果数量不对,那么说明dirtyComponents有新增,再一次进行flushBatchedUpdates,因为更新操作是批量的,因此不会出现改一个就执行一次的情况,大大提高了性能,第二个wrapper主要是处理回调队列里面的内容,当更新处理完成后,最后调用UPDATE_QUEUEING
close方法,调用notifyAll
去执行所有dirtyComponent的回调函数队列
接下来进行详细说明。
总结
注入的设计极大的方便了代码的维护,将实现给原子化,若以后版本升级,算法更新,可以直接切换injectiion中的原子换成新的版本即可。同时,高度抽象的代码也给阅读带来了困难
初识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.jsimport 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; },});// 实例化一个我们定义的transactionvar 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的执行顺序,对源码进行解析:
本文由10bet发布于Web前端,转载请注明出处:React transaction完全解读
关键词: