React PooledClass

PooledClass用途

PooledClass在整个react运作流程中用处很多。前面分析各个模块时候都曾遇到过。

例如ReactUpdates.js中

1
PooledClass.addPoolingTo(ReactUpdatesFlushTransaction);

例如ReactReconcileTransaction.js中

1
PooledClass.addPoolingTo(ReactReconcileTransaction);

例如CallbackQueue.js

1
module.exports = PooledClass.addPoolingTo(CallbackQueue);

用到的地方确实不少。有些是事务上,也有一些是Class(这里应该算是一个工厂函数)上。

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var addPoolingTo = function<T>(
CopyConstructor: Class<T>,
pooler: Pooler,
): Class<T> & {
getPooled(): /* arguments of the constructor */ T,
release(): void,
} {
// Casting as any so that flow ignores the actual implementation and trusts
// it to match the type we declared
var NewKlass = (CopyConstructor: any);
NewKlass.instancePool = [];
NewKlass.getPooled = pooler || DEFAULT_POOLER;
if (!NewKlass.poolSize) {
NewKlass.poolSize = DEFAULT_POOL_SIZE;
}
NewKlass.release = standardReleaser;
return NewKlass;
};

这里注意函数传参后的处理就可以了,当CopyConstructor作为参数传入时候会复制一份引用,此时函数内的NewClass和CopyConstructor其实是两个独立的变量,但由于是引用类型所以指向了同一份引用,所以当修改时候这个引用会被修改,这样最终会导致CopyConstructor引用的变量也会变化。这就是call by sharing

这个函数是有返回值的,但是这里需要注意这个返回值最后和CopyConstructor引用了同一份引用,所以捕获不捕获这个返回值其实没有任何区别。

当然addPoolingTo只是一个简单的加工函数,它给CopyConstructor添加了一个属性两个方法。

属性or方法说明
instancePoolArray 实例池
getPooledMethod 从实例池获取实例
releaseMethod

默认情况下NewKlass.getPooled指向oneArgumentPooler

1
2
3
4
5
6
7
8
9
10
var oneArgumentPooler = function (copyFieldsFrom) {
var Klass = this;
if (Klass.instancePool.length) {
var instance = Klass.instancePool.pop();
Klass.call(instance, copyFieldsFrom);
return instance;
} else {
return new Klass(copyFieldsFrom);
}
};

此时oneArgumentPooler作为NewKlass方法指向NewKlass本身。

这个函数的意义在于,会对instancePool做检查,如果instancePool(实例池)里面有东西那么就把最后一个拿出来用,否则新new一个实例。

release指向了standardReleaser

1
2
3
4
5
6
7
8
var standardReleaser = function (instance) {
var Klass = this;
!(instance instanceof Klass) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Trying to release an instance into a pool of a different type.') : _prodInvariant('25') : void 0;
instance.destructor();
if (Klass.instancePool.length < Klass.poolSize) {
Klass.instancePool.push(instance);
}
};

它会调用CopyConstructor.destructor方法,然后将Klass压到对象池内。一般来说此时Klass就是一个空白的实例。当后面哪一次再次调用getPooled它会直接从对象池中返回这个实例而不去new新的返回。

以CallbackQueue为例。首先它返回一个CallbackQueue构造器,它的原型上有destructor方法。

当执行addPoolingTo后,它就加上了instancePool、getPooled、release属性或者方法。

第一次执行getPooled它会new CallbackQueue构造器(此处设赋值给instance)。如果在这之后再次对这个实例执行CallbackQueue.release(instance),那么就会被推入到对象池,下次再次getPooled就会直接从对象池里面获取这个实例。

思考

那么现在的问题很明显,究竟是为什么要做这个机制呢?

观察这个PooledClass包装后的结果的引用位置,会发现基本都是在事务环节。而事务环节有大量的清场、还原现场的需求,而它本身更是存在被频繁触发的可能。所以这里就有了一个缓存的需要。这是PooledClass存在的意义所在。

为了印证这个想法,我把批量更新到函数找了出来 flushBatchedUpdates(ReactUpdates.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var flushBatchedUpdates = function() {
// ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
// array and perform any updates enqueued by mount-ready handlers (i.e.,
// componentDidUpdate) but we need to check here too in order to catch
// updates enqueued by setState callbacks and asap calls.
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
}

if (asapEnqueued) {
asapEnqueued = false;
var queue = asapCallbackQueue;
asapCallbackQueue = CallbackQueue.getPooled();
queue.notifyAll();
CallbackQueue.release(queue);
}
}
};

很显然,这里对dirtyComponents进行了遍历,如果没有对象池处理,每次遍历过程中的更新都会重新new一个事务,然后重新销毁。但是这里有了事务之后就可以直接getPooled()用完之后release,这样对象池里面就是一个初始化状态的实例。提到初始化这个过程显然跑不了查看一下事务上的destructor(transaction.release环节会执行这个函数):

1
2
3
4
5
6
7
destructor: function() {
this.dirtyComponentsLength = null;
CallbackQueue.release(this.callbackQueue);
this.callbackQueue = null;
ReactUpdates.ReactReconcileTransaction.release(this.reconcileTransaction);
this.reconcileTransaction = null;
},

对照一下其构造器:

1
2
3
4
5
6
7
8
function ReactUpdatesFlushTransaction() {
this.reinitializeTransaction();
this.dirtyComponentsLength = null;
this.callbackQueue = CallbackQueue.getPooled();
this.reconcileTransaction = ReactUpdates.ReactReconcileTransaction.getPooled(
/* useCreateElement */ true,
);
}

很容易发现这两个反向操作。很有意思的是他们里面也有一个CallbackQueue的getPooled和release过程。

总结

getPooled其实是基于React事务需要频繁new和销毁的需要构建的抽象对象池逻辑,以达到节省性能和内存支出的目的。