上一篇文章讲到了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,e);
- } else {
- return transaction.perform(callback,null,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,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,e) {
- ...
- // 启动ReactDefaultBatchingStrategy事务
- return transaction.perform(callback,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,transaction,);
- // 释放事务
- 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 初始渲染主要分为以下几个步骤: