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结构如下图

react-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();

简要画个调用关系

react-injection

本来,按照顺序执行,应该由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一个实例,以重复使用。

后记

不管用什么语言实现,软件设计的思路和技巧是一致的。尤其规模化的框架,脱离不了经典的数据结构和设计模式。看其他类型语言的实现方法,思考对比,没坏处。

#react

SideEffect is a blog for front-end web development.
Code by Axiu / rss