React-函数batchedUpdates和Transaction执行
2018年12月14日 • ... • ☕️ 5 min read
在【React-简单组件渲染(render)过程】里,留了两个小坑:BatchingStrategy的运行机制和transaction的运行机制。这几天抽时间继续做一些记录。
在看batchedUpdates的执行时,常常会有错觉,执行的时候有并发操作存在。后来想想,应该是里面的一些概念,比如:池(pool)、事务(transaction)。还有一些变量定义,比如:isInTransaction(是否处在事务中?如果不是有多个进程,为什么会做这个判断呢?)、isBatchingUpdates(正在批处理更新中?)等等。一定要记住:JS是单线程的。
事务(Transaction)
本篇涉及一个概念–事务(Transaction),常见于数据库的并发操作里,由于js是单线程的,所以和概念上理解的事务是有区别的。
事务(Transaction),是一个操作序列,这些操作要么都执行,要么都不执行,它是-个不可分割的工作单位。
以最简单的JDBC事务(java)为例
public void JdbcTransfer() {
java.sql.Connection conn = null;
try{
conn = conn =DriverManager.getConnection("xxx@xxx","username","userpwd");
action1();
action2();
action3();
// 提交事务
conn.commit();
} catch(SQLException e){
try{
// 发生异常,回滚在本事务中的操作
conn.rollback();
conn.close();
}catch(Exception ignore){
}
e.printStackTrace();
}
}
事务中最主要的操作就是如果捕获到异常,那么就要回滚事务。而在React里用到的事务,基本逻辑也是一致的。
基础的transaction调用的perform方法都是定义在父类Transaction的。需要子类实现的方法有3个:initializeAll、perform、closeAll。一目了然,执行的顺序就是
-this.initializeAll(0); // start:0
-callback.call(a, b, c, d, e, f);
-this.closeAll(0); // start:0
initializeAll中会依次调用transactionWrapper的initialize方法
for (var i = startIndex; i < transactionWrappers.length; i++) {
try {
this.wrapperInitData[i] = wrapper.initialize
} finally {
// 如果wrapper.initialize抛出异常 {
// 静默执行initializeAll(i+1)
// }
}
}
第i个wrapper初始化遇到异常,会抛出,剩下的会继续执行。
perform这么写
try {
// ...
this.initializeAll
ret = method.call
// ...
} finally {
// ...
closeAll
}
如果遇到初始化异常,会导致函数执行的时候跳过ret = method.call,直接走到finally处理,执行closeAll收尾。而执行method过程中遇到异常,也是一样的逻辑。
简单组件渲染涉及的两个transaction结构如下图
这里父子类关系使用了继承,JS中怎么实现继承?让一个对象有另一个对象的属性和方法就行了,常用方式是加在原型(prototype)上,React里由_assign(current, Parent)实现。
batchedUpdates代码流程
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode, // targetFunction
componentInstance, // ReactCompositeComponentWrapper
container, // HTML DOM div#container
shouldReuseMarkup,
context
);
=batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
batchingStrategy.isBatchingUpdates = true;
=transaction.perform(callback, null, a, b, c, d, e); // Transaction.perform
注意这里transaction为ReactDefaultBatchingStrategyTransaction,定义如下
transaction = new ReactDefaultBatchingStrategyTransaction();
1、可能翻batchingStrategy或者ReactUpdates代码的时候,看不到相关的赋值操作,这是因为这个ReactDefaultBatchingStrategyTransaction和下面的ReactReconcileTransaction,都是通过初始化ReactDOM的时候,注入(inject)进去的。这样做的目的是减小函数的耦合,方便以后更改策略,相关说明请看篇尾。
2、在React里,batchingStrategy是以单例存在的。这就意味着对于组件的渲染和更新操作,都会通过同一个实例进行。这样做的目的是为类似setState等操作创建一个唯一的“操作环境”,避免不必要的更新。这里的一个关键标志位就是 batchingStrategy.isBatchingUpdates。
后续文章可以看到,如setState方法触发的组件update都会通过isBatchingUpdates这个判断。
3、初次渲染时,传入的callback即batchedMountComponentIntoNode,这样就接到了之前文章的最后,后面具体的HTML-dom渲染过程先不看。
ReactReconcileTransaction的调用过程
-transaction.perform( // Transaction.perform
mountComponentIntoNode,
null,
componentInstance,
container,
transaction, // 当前的transaction
shouldReuseMarkup,
context
);
-this.initializeAll(0); // start:0
-callback.call(a, b, c, d, e, f); // 开始执行mountComponentIntoNode
-this.closeAll(0); // start:0
-ReactUpdates.ReactReconcileTransaction.release(transaction); // ReactReconcileTransaction
相关概念及实现
依赖注入
var ReactDefaultInjection = require('ReactDefaultInjection');
ReactDefaultInjection.inject();
简要画个调用关系
本来,按照顺序执行,应该由ReactUpdates来主动发起调用,比如使用ReactReconcileTransaction。这里控制权转给了ReactDOM,由它初始化的时候进行注入。这样做只需仿照ReactDefaultInjection.js的结构,引入不同的策略,就可以进行不同的注入,而不必修改ReactUpdates等具体文件。
池(Pool)
ReactReconcileTransaction使用了Pool,即“池”的概念。除了对象本身,还使用了CallbackQueue这个已提前实例化好的对象,用来跟踪对象的update(这个目前未涉及,先不看)。
对于池化的对象,使用方式是PooledClass.getPooled()来获取实例。
使用池,可以 1、节省创建类的实例的开销; 2、节省创建类的实例的时间; 3、防止存储空间随着对象的增多而增大。
React中使用的PooledClass是静态池,由于方法和属性都是直接添加到原对象上的,所以PooledClass中所有方法中的this均指向调用的对象,而非池实例。
使用PooledClass.addPoolingTo(targetObj)给原对象添加
instancePool, // 存放池实例,默认为空数组[]
getPooled, // 获取池方法,默认为oneArgumentPooler
poolSize, // 池大小,默认为10
release // 池释放方法
之后,即可在原对象上使用getPooled、release等方法来操作。
池化对象的使用步骤: 1)第一次,使用getPooled来从池中获取一个实例,instancePool的长度为0,所以会直接返回一个新的实例; 2)在实例上进行目标操作; 3)释放池实例,调用实例的destructor,并将实例放入instancePool中; 4)第二次,会直接从instancePool中pop一个实例,以重复使用。
后记
不管用什么语言实现,软件设计的思路和技巧是一致的。尤其规模化的框架,脱离不了经典的数据结构和设计模式。看其他类型语言的实现方法,思考对比,没坏处。