上一篇文章讲到了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 初始渲染主要分为以下几个步骤: