React源码分析(二)

前端之家收集整理的这篇文章主要介绍了React源码分析(二)前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

上一篇文章讲到了React 调用ReactDOM.render首次渲染组件的前几个过程的源码,包括创建元素、根据元素实例化对应组件,利用事务来进行批量更新. 我们还穿插介绍了React 事务的实现以及如何利用事务进行批量更新的实现. 这篇文章我们接着分析后面的过程,包括调用了哪些事务,组件插入的过程,组件生命周期方法什么时候被调用等.

正文

在React 源码中,首次渲染组件有一个重要的过程,mount,插入,即插入到DOM中,发生在实例化组件之后. React使用批量策略来管理组件插入到DOM的过程. 这个“批量”不是指像遍历数组那样同批次插入,而是一个不断生成不断插入、类似递归的过程. 让我们一步一步来分析.

使用批量策略管理插入

如何管理呢? 即在插入之前就开始一次batch,然后插入过程中任何更新都会被enqueue,在batchingStrategy事务的close阶段批量更新.

启动策略

我们来看首先在插入之前的准备,ReactMount.js中,batchedMountComponentIntoNode被放到了批量策略batchedUpdates中执行 :

// 放在批量策略batchedUpdates中执行插入
ReactUpdates.batchedUpdates(
    batchedMountComponentIntoNode,componentInstance,...
);

从上篇文章展示的源码中看到,这个batchingStrategy就是ReactDefaultBatchingStrategy,因此调用ReactDefaultBatchingStrategybatchedUpdates,并将batchedMountComponentIntoNode当作callback.

执行策略

继续看ReactDefaultBatchingStrategybatchedUpdates,在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 初始渲染主要分为以下几个步骤:

  1. 构建一个组件的elements tree(subtree)—— 从组件嵌套的最里层(转换JSX后最里层的createElements函数)开始层层调用createElements创建这个组件elements tree. 在这个subtree中,里层创建出来的元素作为包裹层的props.children;
  2. 实例化组件——根据当前元素的类型创建对应类型的组件实例;
  3. 利用多种事务执行组件实例的mountComponent.

    1. 首先执行topLevelWrapper(ReactCompositeComponent)的mountComponent;
    2. ReactCompositeComponent的mountComponent过程中会先调用render(Composite类型 )生成组件的elements tree,然后顺着props.children,不断实例化,不断调用各自组件的mountComponent 形成循环
  4. 在以上过程中,依靠事务进行存储更新、回调队列,在事务结束时批量更新.

猜你在找的React相关文章