上一篇文章讲到了React 调用ReactDOM.render
首次渲染组件的前几个过程的源码, 包括创建元素、根据元素实例化对应组件, 利用事务来进行批量更新. 我们还穿插介绍了React 事务的实现以及如何利用事务进行批量更新的实现. 这篇文章我们接着分析后面的过程, 包括调用了哪些事务, 组件插入的过程, 组件生命周期方法什么时候被调用等.
正文
在React 源码中, 首次渲染组件有一个重要的过程, mount
, 插入, 即插入到DOM中, 发生在实例化组件之后. React使用批量策略来管理组件插入到DOM的过程. 这个“批量”不是指像遍历数组那样同批次插入, 而是一个不断生成不断插入、类似递归的过程. 让我们一步一步来分析.
使用批量策略管理插入
如何管理呢? 即在插入之前就开始一次batch, 然后插入过程中任何更新都会被enqueue, 在batchingStrategy事务的close阶段批量更新.
启动策略
我们来看首先在插入之前的准备, ReactMount.js中, batchedMountComponentIntoNode
被放到了批量策略batchedUpdates
中执行 :
// 放在批量策略batchedUpdates中执行插入
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode,
componentInstance,
...
);
从上篇文章展示的源码中看到, 这个batchingStrategy
就是ReactDefaultBatchingStrategy
, 因此调用了ReactDefaultBatchingStrategy
的batchedUpdates
, 并将batchedMountComponentIntoNode
当作callback.
执行策略
继续看ReactDefaultBatchingStrategy
的batchedUpdates
, 在ReactDefaultBatchingStrategy.js :
// 批处理策略
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false, // 是否处在一次BatchingUpdates标志位
// 批量更新策略调用的就是这个方法
batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
// 一旦调用批处理, 重置isBatchingUpdates标志位, 表示正处在一次BatchingUpdates中
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// 首次插入时, 由于是第一次启动批量策略, 因此alreadyBatchingUpdates为false, 执行事务
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e); // 将callback放进事务里执行
}
},
};
在执行插入的过程中enqueue更新
我们在componentWillMount
里setState, 看看React会怎么做:
// ReactBaseClasses.js :
ReactComponent.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
//ReactUpdateQueue.js:
enqueueSetState: function(publicInstance, partialState) {
// enqueueUpdate
var queue =
internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
}
//ReactUpdate.js:
function enqueueUpdate(component) {
ensureInjected(); // 注入默认策略
// 如果不是在一次batch就开启一次batch
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 如果是就存储更新
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
批量更新
在ReactUpdates.js中
var flushBatchedUpdates = function () {
// 批量处理dirtyComponents
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
}
// 批量处理callback
if (asapEnqueued) {
asapEnqueued = false;
var queue = asapCallbackQueue;
asapCallbackQueue = CallbackQueue.getPooled();
queue.notifyAll();
CallbackQueue.release(queue);
}
}
};
使用事务执行插入过程
batchedUpdates
启动一个策略事务去执行batchedMountComponentIntoNode
, 以便利用策略控制更新, 而在这个函数中又启动了一个调和(Reconcile)事务, 以便管理插入.
// ReactDefaultBatchingStrategy.js
var transaction = new ReactDefaultBatchingStrategyTransaction();
...
var ReactDefaultBatchingStrategy = {
...
batchedUpdates: function(callback, a, b, c, d, e) {
...
// 启动ReactDefaultBatchingStrategy事务
return transaction.perform(callback, null, a, b, c, d, e);
},
};
// ReactMount.js
function batchedMountComponentIntoNode(
...
) {
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
!shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement,
);
// 启动Reconcile事务
transaction.perform(
mountComponentIntoNode,
...
);
...
}
React优化策略——对象池
在ReactMount.js :
function batchedMountComponentIntoNode(
componentInstance,
container,
shouldReuseMarkup,
context,
) {
// 从对象池中拿到ReactReconcileTransaction事务
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
!shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement,
);
// 启动事务执行mountComponentIntoNode
transaction.perform(
mountComponentIntoNode,
null,
componentInstance,
container,
transaction,
shouldReuseMarkup,
context,
);
// 释放事务
ReactUpdates.ReactReconcileTransaction.release(transaction);
}
React 在启动另一个事务之前拿到了这个事务, 从哪里拿到的呢? 这里就涉及到了React 优化策略之一——对象池
GC很慢
首先你用JavaScript声明的变量不再使用时, js引擎会在某些时间回收它们, 这个回收时间是耗时的. 资料显示:
Marking latency depends on the number of live objects that have to be marked, with marking of the whole heap potentially taking more than 100 ms for large webpages.
整个堆的标记对于大型网页很可能需要超过100毫秒
尽管V8引擎对垃圾回收有优化, 但为了避免重复创建临时对象造成GC不断启动以及复用对象, React使用了对象池来复用对象, 对GC表明, 我一直在使用它们, 请不要启动回收.
React 实现的对象池其实就是对类进行了包装, 给类添加一个实例队列, 用时取, 不用时再放回, 防止重复实例化:
PooledClass.js :
// 添加对象池, 实质就是对类包装
var addPoolingTo = function (CopyConstructor, pooler) {
// 拿到类
var NewKlass = CopyConstructor;
// 添加实例队列属性
NewKlass.instancePool = [];
// 添加拿到实例方法
NewKlass.getPooled = pooler || DEFAULT_POOLER;
// 实例队列默认为10个
if (!NewKlass.poolSize) {
NewKlass.poolSize = DEFAULT_POOL_SIZE;
}
// 将实例放回队列
NewKlass.release = standardReleaser;
return NewKlass;
};
// 从对象池申请一个实例.对于不同参数数量的类,React分别处理, 这里是一个参数的类的申请实例的方法, 其他一样
var oneArgumentPooler = function(copyFieldsFrom) {
// this 指的就是传进来的类
var Klass = this;
// 如果类的实例队列有实例, 则拿出来一个
if (Klass.instancePool.length) {
var instance = Klass.instancePool.pop();
Klass.call(instance, copyFieldsFrom);
return instance;
} else { // 否则说明是第一次实例化, new 一个
return new Klass(copyFieldsFrom);
}
};
// 释放实例到类的队列中
var standardReleaser = function(instance) {
var Klass = this;
...
// 调用类的解构函数
instance.destructor();
// 放到队列
if (Klass.instancePool.length < Klass.poolSize) {
Klass.instancePool.push(instance);
}
};
// 使用时将类传进去即可
PooledClass.addPoolingTo(ReactReconcileTransaction);
可以看到, React对象池就是给类维护一个实例队列, 用到就pop一个, 不用就push回去. 在React源码中, 用完实例后要立即释放, 也就是申请和释放成对出现, 达到优化性能的目的.
插入过程
在ReactMount.js中, mountComponentIntoNode
函数执行了组件实例的mountComponent
, 不同的组件实例有自己的mountComponent方法, 做的也是不同的事情. (源码我就不上了, 太TM…)
ReactCompositeComponent类型的mountComponent方法:
ReactDOMComponent类型:
ReactDOMTextComponent类型:
整个mount过程是递归渲染的(矢量图):
刚开始, React给要渲染的组件从最顶层加了一个ReactCompositeComponent类型的 topLevelWrapper来方便的存储所有更新, 因此初次递归是从 ReactCompositeComponent 的mountComponent
开始的, 这个过程会调用组件的render函数(如果有的话), 根据render出来的elements再调用instantiateReactComponent
实例化不同类型的组件, 再调用组件的 mountComponent
, 因此这是一个不断渲染不断插入、递归的过程.
总结
React 初始渲染主要分为以下几个步骤:
- 构建一个组件的elements tree(subtree)—— 从组件嵌套的最里层(转换JSX后最里层的createElements函数)开始层层调用
createElements
创建这个组件elements tree. 在这个subtree中, 里层创建出来的元素作为包裹层的props.children; - 实例化组件——根据当前元素的类型创建对应类型的组件实例;
-
利用多种事务执行组件实例的
mountComponent
.- 首先执行topLevelWrapper(ReactCompositeComponent)的mountComponent;
- ReactCompositeComponent的
mountComponent
过程中会先调用render(Composite类型 )生成组件的elements tree, 然后顺着props.children, 不断实例化, 不断调用各自组件的mountComponent 形成循环
- 在以上过程中, 依靠事务进行存储更新、回调队列, 在事务结束时批量更新.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。