React源码——常见工具类

介绍

最近读起了React源码,React实现太过巧妙,底层有许多有用的工具类,了解这些工具类,在读源码的时候可以更好更快的理解。

PooledClass

简介

PooledClass是React的基本类库,见字如面,用于将常用类做一个缓冲池,将常用类的实例缓存在内存中,省略其实例化和释放的开销。

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var PooledClass = require('PooledClass');
class Foo {
constructor(arg) {
this.arg = arg;
}
destructor() {
this.arg = null;
}
}
//初始化,绑定缓存池
PooledClass.addPoolingTo(Foo);
//从池中取出一个实例
let foo = Foo.getPooled("foo");
//执行constructor
//foo.arg = "foo"
//释放实例,将实例放回池中
Foo.release(foo);
//执行destructor();

源码分析

https://github.com/facebook/react/blob/15-stable/src/shared/utils/PooledClass.js

Flow 是 Facebook 宣布推出一个开源的 JavaScript 静态类型检查器,旨在发现 JS 程序中的类型错误,以提高程序员的效率和代码质量。React源码使用了Flow。

addPoolingTo方法有两个参数,CopyConstructor为类的构造器,是一个function对象;pooler负责类的初始化,默认为下文的oneArgumentPooler
addPoolingTo给这个function对象添加了instancePool用于缓存实例;poolSize用于指示缓存池大小;getPooled()用于取出实例;release()用于放回实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var DEFAULT_POOL_SIZE = 10;
var DEFAULT_POOLER = oneArgumentPooler;
/**
* Augments `CopyConstructor` to be a poolable class, augmenting only the class
* itself (statically) not adding any prototypical fields. Any CopyConstructor
* you give this may have a `poolSize` property, and will look for a
* prototypical `destructor` on instances.
*
* @param {Function} CopyConstructor Constructor that can be used to reset.
* @param {Function} pooler Customizable pooler.
*/
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;
};

oneArgumentPooler()对应getPooled(),从缓存池中取出实例并初始化,如果缓存池为空,则使用new新建一个实例并初始化。 除此之外还有twoArgumentPoolerthreeArgumentPoolerfourArgumentPooler函数,对应有多个构造函数的情况。尽管他们可以用一个通用的方法实现,但是这样需要访问arguments对象。
standardReleaser()对应release(),执行实例的destructor()后,如果缓存池未满则将实例放入缓存池。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Static poolers. Several custom versions for each potential number of
* arguments. A completely generic pooler is easy to implement, but would
* require accessing the `arguments` object. In each of these, `this` refers to
* the Class itself, not an instance. If any others are needed, simply add them
* here, or in their own files.
*/
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);
}
};
...
var standardReleaser = function(instance) {
var Klass = this;
invariant(
instance instanceof Klass,
'Trying to release an instance into a pool of a different type.'
);
instance.destructor();
if (Klass.instancePool.length < Klass.poolSize) {
Klass.instancePool.push(instance);
}
};


CallbackQueue

简介

CallbackQueue用于批量存储回调函数,并在合适的时候触发。

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var CallbackQueue = require('CallbackQueue');
//获取实例
var queue = CallbackQueue.getPooled();
//已经调用PooledClass.addPoolingTo(CallbackQueue);
var foo = {
bar: 0
}
var plusBar = function() {
if (typeof(this.bar) !== "undefined")
this.bar++;
}
//初始化
queue.reset();
//将回调加入队列,第二个参数指定作用域
queue.enqueue(plusBar, foo);
queue.enqueue(plusBar, foo);
//调用所有回调
queue.notifyAll();
console.log(foo.bar);
//输出2,plusBar()作用域为foo
//释放实例
CallbackQueue.release(this.callbackQueue);

源码解析

https://github.com/facebook/react/blob/15-stable/src/renderers/shared/utils/CallbackQueue.js

CallbackQueue包含三个成员属性,_callbacks_contexts_arg,分别储存回调的函数、作用域、参数。

  • enqueue(callback, context):将一个回调函数及其作用域压入数组。
  • notifyAll():依次触发所有回调函数,使用对应的作用域,使用_arg作为参数。调用完成后清空回调队列,Array.length = 0;用于清空队列数组,避免内存泄露。
  • checkpoint(): 获取一个检查点,返回队列长度。
  • rollback(len): 恢复检查点对应状态,参数为队列长度。
  • reset(): 重置队列,将回调队列及其作用域_callbacks_contexts清空,但是不会清空_arg
    最后一行module.exports = PooledClass.addPoolingTo(CallbackQueue);,给CallbackQueue做了线程池处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
* A specialized pseudo-event module to help keep track of components waiting to
* be notified when their DOM representations are available for use.
*
* This implements `PooledClass`, so you should never need to instantiate this.
* Instead, use `CallbackQueue.getPooled()`.
*
* @class ReactMountReady
* @implements PooledClass
* @internal
*/
class CallbackQueue<T> {
_callbacks: ?Array<() => void>;
_contexts: ?Array<T>;
_arg: ?mixed;
constructor(arg) {
this._callbacks = null;
this._contexts = null;
this._arg = arg;
}
/**
* Enqueues a callback to be invoked when `notifyAll` is invoked.
*
* @param {function} callback Invoked when `notifyAll` is invoked.
* @param {?object} context Context to call `callback` with.
* @internal
*/
enqueue(callback: () => void, context: T) {
this._callbacks = this._callbacks || [];
this._callbacks.push(callback);
this._contexts = this._contexts || [];
this._contexts.push(context);
}
/**
* Invokes all enqueued callbacks and clears the queue. This is invoked after
* the DOM representation of a component has been created or updated.
*
* @internal
*/
notifyAll() {
var callbacks = this._callbacks;
var contexts = this._contexts;
var arg = this._arg;
if (callbacks && contexts) {
invariant(
callbacks.length === contexts.length,
'Mismatched list of contexts in callback queue'
);
this._callbacks = null;
this._contexts = null;
for (var i = 0; i < callbacks.length; i++) {
callbacks[i].call(contexts[i], arg);
}
callbacks.length = 0;
contexts.length = 0;
}
}
checkpoint() {
return this._callbacks ? this._callbacks.length : 0;
}
rollback(len: number) {
if (this._callbacks && this._contexts) {
this._callbacks.length = len;
this._contexts.length = len;
}
}
/**
* Resets the internal queue.
*
* @internal
*/
reset() {
this._callbacks = null;
this._contexts = null;
}
/**
* `PooledClass` looks for this.
*/
destructor() {
this.reset();
}
}
module.exports = PooledClass.addPoolingTo(CallbackQueue);