导航栏: 首页 评论列表

React源码分析

默认分类 2021/04/29 03:58

### React源码分析(一)-调用ReactDOM.render后发生了什么

所谓知其然还要知其所以然. 本系列文章将分析 React 15-stable的部分源码, 包括组件初始渲染的过程、组件更新的过程等. 这篇文章先介绍组件初始渲染的过程的几个重要概念, 包括大致过程、创建元素、实例化组件、事务、批量更新策略等. 在这之前, 假设读者已经:

[](#如何分析-React-源码 "如何分析 React 源码")如何分析 React 源码

代码架构预览

首先, 我们找到React在Github上的地址, 把15-stable版本的源码copy下来, 观察它的整体架构, 这里首先阅读关于源码介绍的官方文档, 再接着看.

我们 要分析的源码在 src 目录下:

// src 部分目录

├── ReactVersion.js    # React版本号
├── addons             # 插件
├── isomorphic         # 同构代码,作为react-core, 提供顶级API
├── node_modules
├── package.json
├── renderers          # 渲染器, 包括DOM,Native,art,test等
├── shared             # 子目录之间需要共享的代码,提到父级目录shared
├── test               # 测试代码

分析方法

1、首先看一些网上分析的文章, 对重点部分的源码有个印象, 知道一些关键词意思, 避免在无关的代码上迷惑、耗费时间;

2、准备一个demo, 无任何功能代码, 只安装react,react-dom, Babel转义包, 避免分析无关代码;

3、打debugger; 利用Chrome devtool一步一步走, 打断点, 看调用栈,看函数返回值, 看作用域变量值;

4、利用编辑器查找代码、阅读代码等

[](#正文 "正文")正文

我们知道, 对于一般的React 应用, 浏览器会首先执行代码 ReactDOM.render来渲染顶层组件, 在这个过程中递归渲染嵌套的子组件, 最终所有组件被插入到DOM中. 我们来看看

[](#调用ReactDOM-render-发生了什么 "调用ReactDOM.render 发生了什么")调用ReactDOM.render 发生了什么

[](#大致过程-只展示主要的函数调用 "大致过程(只展示主要的函数调用):")大致过程(只展示主要的函数调用):

React 初始渲染

如果看不清这有矢量图

[](#让我们来分析一下具体过程 "让我们来分析一下具体过程:")让我们来分析一下具体过程:


[](#1、创建元素 "1、创建元素")1、创建元素

首先, 对于你写的jsx, Babel会把这种语法糖转义成这样:

// jsx
ReactDOM.render(
    <C />,
    document.getElementById('app')
)

// 转义后
ReactDOM.render(
  React.createElement(C, null), 
  document.getElementById('app')
);

没错, 就是调用React.createElement来创建元素. 元素是什么? 元素只是一个对象描述了DOM树, 它像这样:

{
  $$typeof: Symbol(react.element)
  key: null
  props: {}        // props有child属性, 描述子组件, 同样是元素
  ref: null
  type: class C    // type可以是类(自定义组件)、函数(wrapper)、string(DOM节点)
  _owner: null
  _store: {validated: false}
  _self: null
  _source: null
}

React.createElement源码在ReactElement.js中, 其他逻辑比较简单, 值得说的是props属性, 这个props属性里面包含的就是我们给组件传的各种属性:

// jsx
return (
    <div className='container'>
          "dscsdcsd"
          <i onClick={(e) => console.log(e)}>{this.state.val}</i>
          <Children val={this.state.val}/>
    </div>
)

// bable 转义后
// createElement(type, props, children)
return React.createElement(
    'div', { className: 'container' }, 
    '"dscsdcsd"',
    React.createElement('i', { onClick: e => console.log(e) }, this.state.val),
    React.createElement(Children, { val: this.state.val })
);

// 对应的元素树
{
  $$typeof: Symbol(react.element)
  key: null
  props: {  // props有children属性, 描述子组件, 同样是元素
    children: [
      ""dscsdcsd"",
      // 子元素
      {$$typeof: Symbol(react.element), type: "i", key: null, ref: null, props: {…}, …},
      {$$typeof: Symbol(react.element), type: class Children, props: {…}, …}
    ]
    className: 'container'
  }  
  ref: null
  type: 'div'
  _owner: null
  _store: {validated: false}
  _self: null
  _source: null
}
[](#2、创建对应类型的React组件 "2、创建对应类型的React组件")2、创建对应类型的React组件

创建出来的元素被当作参数和指定的 DOM container 一起传进ReactDOM.render. 接下来会调用一些内部方法, 接着调用了 instantiateReactComponent, 这个函数根据element的类型实例化对应的component. 当element的类型为:

instantiateReactComponent函数在instantiateReactComponent.js :

/**
 * Given a ReactNode, create an instance that will actually be mounted.
 */
function instantiateReactComponent(node(这里node指element), shouldHaveDebugID) {
  ...

  // 如果element为空
  if (node === null || node === false) {
    // 创建空component
    instance = ReactEmptyComponent.create(instantiateReactComponent);
  } else if (typeof node === 'object') {  // 如果是对象
      ...     // 这里是类型检查

    // 如果element.type是字符串
    if (typeof element.type === 'string') {
      //实例化 宿主组件, 也就是DOM节点
      instance = ReactHostComponent.createInternalComponent(element);
    } else if (isInternalComponentType(element.type)) {
      // 保留给以后版本使用,此处暂时不会涉及到
    } else { // 否则就实例化ReactCompositeComponent
      instance = new ReactCompositeComponentWrapper(element);
    }
  // 如果element是string或number
  } else if (typeof node === 'string' || typeof node === 'number') {
    // 实例化ReactDOMTextComponent
    instance = ReactHostComponent.createInstanceForText(node);
  } else {
    invariant(false, 'Encountered invalid React node of type %s', typeof node);
  }
   ...
  return instance;
}
[](#3、开启批量更新以应对可能的setState "3、开启批量更新以应对可能的setState")3、开启批量更新以应对可能的setState

在调用instantiateReactComponent拿到组件实例后, React 接着调用了batchingStrategy.batchedUpdates并将组件实例当作参数执行批量更新(首次渲染为批量插入).

批量更新是一种优化策略, 避免重复渲染, 在很多框架都存在这种机制. 其实现要点是要弄清楚何时存储更新, 何时批量更新.

在React中, 批量更新受batchingStrategy控制,而这个策略除了server端都是ReactDefaultBatchingStrategy:

不信你看, 在ReactUpdates.js中 :

var ReactUpdatesInjection = {
  ...
  // 注入批量策略的函数声明
  injectBatchingStrategy: function(_batchingStrategy) {
    ... 

    batchingStrategy = _batchingStrategy;
  },
};

在ReactDefaultInjection.js中注入ReactDefaultBatchingStrategy :

ReactInjection.Updates.injectBatchingStrategy(ReactDefaultBatchingStrategy); // 注入

那么React是如何实现批量更新的? 在ReactDefaultBatchingStrategy.js我们看到, 它的实现依靠了事务.

[](#3-1-我们先介绍一下事务 "3.1 我们先介绍一下事务.")3.1 我们先介绍一下事务.

在 Transaction.js中, React 介绍了事务:

* <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>

React 把要调用的函数封装一层wrapper, 这个wrapper一般是一个对象, 里面有initialize方法, 在调用函数前调用;有close方法, 在函数执行后调用. 这样封装的目的是为了, 在要调用的函数执行前后某些不变性约束条件(invariant)仍然成立.

这里的不变性约束条件(invariant), 我把它理解为 “真命题”, 因此前面那句话意思就是, 函数调用前后某些规则仍然成立. 比如, 在调和(reconciliation)前后保留UI组件一些状态.

React 中, 事务就像一个黑盒, 函数在这个黑盒里被执行, 执行前后某些规则仍然成立, 即使函数报错. 事务提供了函数执行的一个安全环境.

继续看Transaction.js对事务的抽象实现:

// 事务的抽象实现, 作为基类
var TransactionImpl = {
  // 初始化/重置实例属性, 给实例添加/重置几个属性, 实例化事务时会调用
  reinitializeTransaction: function () {
    this.transactionWrappers = this.getTransactionWrappers();
    if (this.wrapperInitData) {
      this.wrapperInitData.length = 0;
    } else {
      this.wrapperInitData = [];
    }
    this._isInTransaction = false;
  },

  _isInTransaction: false,

  // 这个函数会交给具体的事务实例化时定义, 初始设为null
  getTransactionWrappers: null,
  // 判断是否已经在这个事务中, 保证当前的Transaction正在perform的同时不会再次被perform
  isInTransaction: function () {
    return !!this._isInTransaction;
  },

  // 顶级API, 事务的主要实现, 用来在安全的窗口下执行函数
  perform: function (method, scope, a, b, c, d, e, f) {
    var ret;
    var errorThrown;
    try {
      this._isInTransaction = true;
      errorThrown = true;
      this.initializeAll(0);  // 调用所有wrapper的initialize方法
      ret = method.call(scope, a, b, c, d, e, f); // 调用要执行的函数
      errorThrown = false;
    } finally {
      // 调用所有wrapper的close方法, 利用errorThrown标志位保证只捕获函数执行时的错误, 对initialize      // 和close抛出的错误不做处理
      try {
        if (errorThrown) {
          try {
            this.closeAll(0);
          } catch (err) {}
        } else {
          this.closeAll(0);
        }
      } finally {
        this._isInTransaction = false;
      }
    }
    return ret;
  },

  // 调用所有wrapper的initialize方法的函数定义
  initializeAll: function (startIndex) {
    var transactionWrappers = this.transactionWrappers; // 得到wrapper
    // 遍历依次调用
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      try {
        ...
        this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this):null;
      } finally {
        if (this.wrapperInitData[i] === OBSERVED_ERROR) {
          try {
            this.initializeAll(i + 1);
          } catch (err) {}
        }
      }
    }
  },

  // 调用所有wrapper的close方法的函数定义
  closeAll: function (startIndex) {
    ...
    var transactionWrappers = this.transactionWrappers; // 拿到wrapper
    // 遍历依次调用
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      var initData = this.wrapperInitData[i];
      var errorThrown;
      try {
        ...
        if (initData !== OBSERVED_ERROR && wrapper.close) {
          wrapper.close.call(this, initData);
        }
        errorThrown = false;
      } finally {
        if (errorThrown) {
          ...
          try {
            this.closeAll(i + 1);
          } catch (e) {}
        }
      }
    }
    this.wrapperInitData.length = 0;
  }
};

这只是React事务的抽象实现(基类), 还需要实例化事务并对其加强的配合, 才能发挥事务的真正作用. 另外, 在React 中, 一个事务里开启另一个事务很普遍, 这说明事务是有粒度大小的, 就像进程和线程一样.

[](#3-2-批量更新依靠了事务 "3.2 批量更新依靠了事务")3.2 批量更新依靠了事务

刚讲到, 在React中, 批量更新受batchingStrategy控制,而这个策略除了server端都是ReactDefaultBatchingStrategy, 而在ReactDefaultBatchingStrategy.js中, 批量更新的实现依靠了事务:

ReactDefaultBatchingStrategy.js :

...
var Transaction = require('Transaction');// 引入事务
...

var RESET_BATCHED_UPDATES = {   // 重置的 wrapper
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;  // 事务结束即一次batch结束
  },
};

var FLUSH_BATCHED_UPDATES = {  // 批处理的 wrapper
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

// 组合成 ReactDefaultBatchingStrategyTransaction 事务的wrapper
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]; 

// 调用 reinitializeTransaction 初始化
function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

// 参数中依赖了事务
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
});

var transaction = new ReactDefaultBatchingStrategyTransaction(); // 实例化这类事务

// 批处理策略
var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false, // 是否处在一次BatchingUpdates标志位

  // 批量更新策略调用的就是这个方法
  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    // 一旦调用批处理, 重置isBatchingUpdates标志位, 表示正处在一次BatchingUpdates中
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // 避免重复分配事务
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);  // 将callback放进事务里执行
    }
  },
};

那么, 为什么批量更新的实现依靠了事务呢? 还记得实现批量更新的两个要点吗?

对于这两个问题, React 在执行事务时调用wrappers的initialize方法, 建立更新队列, 然后执行函数, 接着 :

口说无凭, 得有证据. 我们拿ReactDOM.render会调用的事务ReactReconcileTransaction来看看是不是这样:

ReactReconcileTransaction.js 里有个wrapper, 它是这样定义的(英文是官方注释) :

var ON_DOM_READY_QUEUEING = {
  /**
   * Initializes the internal `onDOMReady` queue.
   */
  initialize: function() {
    this.reactMountReady.reset();
  },

  /**
   * After DOM is flushed, invoke all registered `onDOMReady` callbacks.
   */
  close: function() {
    this.reactMountReady.notifyAll();
  },
};

我们再看ReactReconcileTransaction事务会执行的函数mountComponent, 它在

ReactCompositeComponent.js :

/*
   * Initializes the component, renders markup, and registers event listeners.
*/
  mountComponent: function(
    transaction,
    hostParent,
    hostContainerInfo,
    context,
  ) {
    ...

    if (inst.componentDidMount) {
          if (__DEV__) {
            transaction.getReactMountReady().enqueue(() => { // 将要调用的callback存起来
              measureLifeCyclePerf(
                () => inst.componentDidMount(),
                this._debugID,
                'componentDidMount',
              );
            });
          } else {
            transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
          }
      }

     ...
    }

而上述wrapper定义的close方法调用的this.reactMountReady.notifyAll()在这

CallbackQueue.js :

/**
   * Invokes all enqueued callbacks and clears the queue. This is invoked after
   * the DOM representation of a component has been created or updated.
   */
  notifyAll() {
      ...
      // 遍历调用存储的callback
      for (var i = 0; i < callbacks.length; i++) {
        callbacks[i].call(contexts[i], arg);
      }
      callbacks.length = 0;
      contexts.length = 0;
    }
  }

即证.

[](#你竟然读到这了 "你竟然读到这了")你竟然读到这了

好累(笑哭), 先写到这吧. 我本来还想一篇文章就把组件初始渲染的过程和组件更新的过程讲完, 现在看来要分开讲了… React 细节太多了, 蕴含的信息量也很大…说博大精深一点不夸张…向React的作者们以及社区的人们致敬!

我觉得读源码是一件很费力但是非常值得的事情. 刚开始读的时候一点头绪也没有, 不知道它是什么样的过程, 不知道为什么要这么写, 有时候还会因为断点没打好绕了很多弯路…也是硬着头皮一遍一遍看, 结合网上的文章, 就这样云里雾里的慢慢摸索, 不断更正自己的认知.后来看多了, 就经常会有大彻大悟的感觉, 零碎的认知开始连通起来, 逐渐摸清了来龙去脉.

现在觉得确实很值得, 自己学到了不少. 看源码的过程就感觉是跟作者们交流讨论一样, 思想在碰撞! 强烈推荐前端的同学们阅读React源码, 大神们智慧的结晶!

未完待续…

### React源码分析(二)-组件的初始渲染

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

[](#正文 "正文")正文

在React 源码中, 首次渲染组件有一个重要的过程, mount, 插入, 即插入到DOM中, 发生在实例化组件之后. 这是一个不断生成(render)不断插入、类似递归的过程. 让我们一步一步来分析.

[](#使用事务执行插入过程 "使用事务执行插入过程")使用事务执行插入过程

我们来看首先在插入之前的准备, ReactMount.js中, batchedMountComponentIntoNode被放到了批量策略batchedUpdates中执行, batchedMountComponentIntoNode 函数正是执行插入过程的第一步

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

这个batchingStrategy就是ReactDefaultBatchingStrategy, 因此调用了ReactDefaultBatchingStrategybatchedUpdates, 并将batchedMountComponentIntoNode当作callback.

在ReactDefaultBatchingStrategy.js中启动了ReactDefaultBatchingStrategyTransaction事务去执行batchedMountComponentIntoNode, 以便利用策略控制更新, 而在这个函数中又启动了一个调和(Reconcile)事务, 执行mountComponentIntoNode进行插入.

// 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,
    ...
  );
    ...
}

相信你注意到了 ReactUpdates.ReactReconcileTransaction.getPooled, 这个函数的作用就是从对象池里拿到ReactReconcileTransaction 对象重用.

[](#React优化策略——对象池 "React优化策略——对象池")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很慢 "GC很慢")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;

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

### React源码分析(三)-全面剖析组件更新机制

React 把组件看作状态机(有限状态机), 使用state来控制本地状态, 使用props来传递状态. 前面我们探讨了 React 如何映射状态到 UI 上(初始渲染), 那么接下来我们谈谈 React 时如何同步状态到 UI 上的, 也就是:

React 是如何更新组件的?

React 是如何对比出页面变化最小的部分?

这篇文章会为你解答这些问题.

[](#在这之前 "在这之前")在这之前

你已经了解了React (15-stable版本)内部的一些基本概念, 包括不同类型的组件实例、mount过程、事务、批量更新的大致过程(还没有? 不用担心, 为你准备好了从源码看组件初始渲染接着从源码看组件初始渲染;

准备一个demo, 调试源码, 以便更好理解;

Keep calm and make a big deal !

[](#React-是如何更新组件的 "React 是如何更新组件的?")React 是如何更新组件的?

[](#TL-DR "TL;DR")TL;DR

这个更新过程像是一套流程, 无论你通过setState(或者replaceState)还是新的props去更新一个组件, 都会起作用.

[](#那么具体是什么 "那么具体是什么?")那么具体是什么?

让我们从这套更新流程的开始部分讲起…

[](#调用-setState-之前 "调用 setState 之前")调用 setState 之前

首先, 开始一次batch的入口是在ReactDefaultBatchingStrategy里, 调用里面的batchedUpdates便可以开启一次batch:

// 批处理策略
var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false, 
  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true; // 开启一次batch

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      // 启动事务, 将callback放进事务里执行
      return transaction.perform(callback, null, a, b, c, d, e);  
    }
  },
};

在 React 中, 调用batchedUpdates有很多地方, 与更新流程相关的如下

// ReactMount.js
ReactUpdates.batchedUpdates(
      batchedMountComponentIntoNode,  // 负责初始渲染
      componentInstance,
      container,
      shouldReuseMarkup,
      context,
);

// ReactEventListener.js
dispatchEvent: function(topLevelType, nativeEvent) {
    ...
    try {
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);  // 处理事件
    } finally {
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
},

第一种情况, React 在首次渲染组件的时候会调用batchedUpdates, 然后开始渲染组件. 那么为什么要在这个时候启动一次batch呢? 不是因为要批量插入, 因为插入过程是递归的, 而是因为组件在渲染的过程中, 会依顺序调用各种生命周期函数, 开发者很可能在生命周期函数中(如componentWillMount或者componentDidMount)调用setState. 因此, 开启一次batch就是要存储更新(放入dirtyComponents), 然后在事务结束时批量更新. 这样以来, 在初始渲染流程中, 任何setState都会生效, 用户看到的始终是最新的状态.

第二种情况, 如果你在HTML元素上或者组件上绑定了事件, 那么你有可能在事件的监听函数中调用setState, 因此, 同样为了存储更新(放入dirtyComponents), 需要启动批量更新策略. 在回调函数被调用之前, React事件系统中的dispatchEvent函数负责事件的分发, 在dispatchEvent中启动了事务, 开启了一次batch, 随后调用了回调函数. 这样一来, 在事件的监听函数中调用的setState就会生效.

也就是说, 任何可能调用 setState 的地方, 在调用之前, React 都会启动批量更新策略以提前应对可能的setState

[](#那么调用-batchedUpdates-后发生了什么 "那么调用 batchedUpdates 后发生了什么?")那么调用 batchedUpdates 后发生了什么?

React 调用batchedUpdates时会传进去一个函数, batchedUpdates会启动ReactDefaultBatchingStrategyTransaction事务, 这个函数就会被放在事务里执行:

// ReactDefaultBatchingStrategy.js
var transaction = new ReactDefaultBatchingStrategyTransaction(); // 实例化事务
var ReactDefaultBatchingStrategy = {
  ...
  batchedUpdates: function(callback, a, b, c, d, e) {
    ...
      return transaction.perform(callback, null, a, b, c, d, e);  // 将callback放进事务里执行
    ...
};

ReactDefaultBatchingStrategyTransaction这个事务控制了批量策略的生命周期:

// ReactDefaultBatchingStrategy.js
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),  // 批量更新
};
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;  // 结束本次batch
  },
};
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

无论你传进去的函数是什么, 无论这个函数后续会做什么, 都会在执行完后调用上面事务的close方法, 先调用flushBatchedUpdates批量更新, 再结束本次batch.

[](#调用-setState-后发生了什么 "调用 setState 后发生了什么")调用 setState 后发生了什么

// 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) {
    // 根据 this.setState 中的 this 拿到内部实例, 也就是组件实例
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
    // 取得组件实例的_pendingStateQueue
    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    // 将partial state存到_pendingStateQueue
    queue.push(partialState);
    // 调用enqueueUpdate
    enqueueUpdate(internalInstance);
 }

// => ReactUpdate.js:
function enqueueUpdate(component) {
  ensureInjected(); // 注入默认策略

    // 如果没有开启batch(或当前batch已结束)就开启一次batch再执行, 这通常发生在异步回调中调用 setState     // 的情况
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
    // 如果batch已经开启就存储更新
  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}

也就是说, 调用 setState 会首先拿到内部组件实例, 然后把要更新的partial state存到其_pendingStateQueue中, 然后标记当前组件为dirtyComponent, 存到dirtyComponents数组中. 然后就接着继续做下面的事情了, 并没有立即更新, 这是因为接下来要执行的代码里有可能还会调用 setState, 因此只做存储处理.

[](#什么时候批量更新 "什么时候批量更新?")什么时候批量更新?

首先, 一个事务在执行的时候(包括initialize、perform、close阶段), 任何一阶段都有可能调用一系列函数, 并且开启了另一些事务. 那么只有等后续开启的事务执行完, 之前开启的事务才继续执行. 下图是我们刚才所说的第一种情况, 在初始渲染组件期间 setState 后, React 启动的各种事务和执行的顺序:

从图中可以看到, 批量更新是在ReactDefaultBatchingStrategyTransaction事务的close阶段, 在flushBatchedUpdates函数中启动了ReactUpdatesFlushTransaction事务负责批量更新.

[](#怎么批量更新的 "怎么批量更新的?")怎么批量更新的?

[](#开启批量更新事务、批量处理callback "开启批量更新事务、批量处理callback")开启批量更新事务、批量处理callback

我们接着看flushBatchedUpdates函数, 在ReactUpdates.js中

var flushBatchedUpdates = function () {
  // 启动批量更新事务
  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);
    }
  }
};
[](#遍历dirtyComponents "遍历dirtyComponents")遍历dirtyComponents

flushBatchedUpdates启动了一个更新事务, 这个事务执行了runBatchedUpdates进行批量更新:

// ReactUpdates.js
function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  // 排序保证父组件优先于子组件更新
  dirtyComponents.sort(mountOrderComparator);

  // 代表批量更新的次数, 保证每个组件只更新一次
  updateBatchNumber++;
  // 遍历 dirtyComponents
  for (var i = 0; i < len; i++) {
    var component = dirtyComponents[i];

    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;
    ...
    // 执行更新
    ReactReconciler.performUpdateIfNecessary(
      component,
      transaction.reconcileTransaction,
      updateBatchNumber,
    );
    ...
    // 存储 callback以便后续按顺序调用
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(
          callbacks[j],
          component.getPublicInstance(),
        );
      }
    }
  }
}

前面 setState 后将组件推入了dirtyComponents, 现在就是要遍历dirtyComponents数组进行更新了.

[](#根据不同情况执行更新 "根据不同情况执行更新")根据不同情况执行更新

ReactReconciler会调用组件实例的performUpdateIfNecessary. 如果接收了props, 就会调用此组件的receiveComponent, 再在里面调用updateComponent更新组件; 如果没有接受props, 但是有新的要更新的状态(_pendingStateQueue不为空)就会直接调用updateComponent来更新:

// ReactCompositeComponent.js
performUpdateIfNecessary: function (transaction) {
    if (this._pendingElement != null) {
        ReactReconciler.receiveComponent(this, this._pendingElement, transaction,               this._context);
    } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
        this.updateComponent(transaction, this._currentElement, this._currentElement,           this._context, this._context);
    } else {
        this._updateBatchNumber = null;
    }
}
[](#调用组件实例的updateComponent "调用组件实例的updateComponent")调用组件实例的updateComponent

接下里就是重头戏updateComponent了, 它决定了组件如果更新自己和它的后代们. 需要特别注意的是, React 内部三种不同的组件类型, 每种组件都有自己的updateComponent, 有不同的行为.

对于 ReactCompositeComponent (矢量图):

updateComponent所做的事情 :

对于 ReactDOMComponent:

updateComponent所做的事情 :

对于 ReactDOMTextComponent :

上面只是每个组件自己更新的过程, 那么 React 是如何一次性更新所有组件的 ? 答案是递归.

[](#递归调用组件的updateComponent "递归调用组件的updateComponent")递归调用组件的updateComponent

观察 ReactCompositeComponent 和 ReactDOMComponent 的更新流程, 我们发现 React 每次走到一个组件更新过程的最后部分, 都会有一个判断 : 如果 nextELement 和 prevElement key 和 type 相等, 就会调用receiveComponent. receiveComponentupdateComponent一样, 每种组件都有一个, 作用就相当于updateComponent 接受了新 props 的版本. 而这里调用的就是子元素的receiveComponent, 进而进行子元素的更新, 于是就形成了递归更新、递归diff. 因此, 整个流程就像这样(矢量图) :

这种更新完一级、diff完一级再进入下一级的过程保证 React 只遍历一次组件树就能完成更新, 但代价就是只要前后 render 出元素的 type 和 key 有一个不同就删除重造, 因此, React 建议页面要尽量保持稳定的结构.

[](#React-是如何对比出页面变化最小的部分 "React 是如何对比出页面变化最小的部分?")React 是如何对比出页面变化最小的部分?

你可能会说 React 用 virtual DOM 表示了页面结构, 每次更新, React 都会re-render出新的 virtual DOM, 再通过 diff 算法对比出前后变化, 最后批量更新. 没错, 很好, 这就是大致过程, 但这里存在着一些隐藏的深层问题值得探讨 :

[](#React-如何表示页面结构 "React 如何表示页面结构")React 如何表示页面结构

class C extends React.Component {
    render () {
        return (
            <div className='container'>
                  "dscsdcsd"
                  <i onClick={(e) => console.log(e)}>{this.state.val}</i>
                  <Children val={this.state.val}/>
            </div>
        )
    }
}
// virtual DOM(React element)
{
  $$typeof: Symbol(react.element)
  key: null
  props: {  // props 代表元素上的所有属性, 有children属性, 描述子组件, 同样是元素
    children: [
      ""dscsdcsd"",
      {$$typeof: Symbol(react.element), type: "i", key: null, ref: null, props: {…}, …},
      {$$typeof: Symbol(react.element), type: class Children, props: {…}, …}
    ]
    className: 'container'
  }  
  ref: null
  type: "div"
  _owner: ReactCompositeComponentWrapper {...} // class C 实例化后的对象
  _store: {validated: false}
  _self: null
  _source: null
}

每个标签, 无论是DOM元素还是自定义组件, 都会有 key、type、props、ref 等属性.

也就是说, 如果元素唯一标识符或者类别或者属性有变化, 那么它们re-render后对应的 key、type 和props里面的属性也会改变, 前后一对比即可找出变化. 综上来看, React 这么表示页面结构确实能够反映前后所有变化.

[](#那么-React-是如何-diff-的 "那么 React 是如何 diff 的?")那么 React 是如何 diff 的?

React diff 每次只对同一层级的节点进行比对 :

上图的数字表示遍历更新的次序.

从父节点开始, 每一层 diff 包括两个地方

子元素的对比:

<pre>// ReactChildReconciler.js

updateChildren: function(...) { ... for (name in nextChildren) { // 遍历 re-render 出的elements ... if ( prevChild != null && shouldUpdateReactComponent(prevElement, nextElement) ) { // 如果key && type 没变进行下一级更新 ReactReconciler.receiveComponent(...); nextChildren[name] = prevChild; // 更新完放入 nextChildren, 注意放入的是组件实例 } else { // 如果变了则移除重建 if (prevChild) { removedNodes[name] = ReactReconciler.getHostNode(prevChild); ReactReconciler.unmountComponent(prevChild, false); } var nextChildInstance = instantiateReactComponent(nextElement, true); nextChildren[name] = nextChildInstance; var nextChildMountImage = ReactReconciler.mountComponent(...); mountImages.push(nextChildMountImage); } } // 再除掉 prevChildren 里有, nextChildren 里没有的组件 for (name in prevChildren) { if ( prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name)) ) { prevChild = prevChildren[name]; removedNodes[name] = ReactReconciler.getHostNode(prevChild); ReactReconciler.unmountComponent(prevChild, false); } } },

shouldComponentUpdate 函数:

<pre>function shouldUpdateReactComponent(prevElement, nextElement) {

var prevEmpty = prevElement === null || prevElement === false; var nextEmpty = nextElement === null || nextElement === false; if (prevEmpty || nextEmpty) { return prevEmpty === nextEmpty; }

  var prevType = typeof prevElement;

var nextType = typeof nextElement; // 如果前后变化都是字符串、数字类型的则允许更新 if (prevType === 'string' || prevType === 'number') { return nextType === 'string' || nextType === 'number'; } else { // 否则检查 type && key return ( nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key ); } }

element diff 检测 type && key 都没变时会进入下一级更新, 如果变化则直接移除重造新元素, 然后遍历同级的下一个.

React 会将同一层级的变化标记, 如 MOVE_EXISTING、REMOVE_NODE、TEXT_CONTENT、INSERT_MARKUP 等, 统一放到 updates 数组中然后批量处理.

[](#And-that‘s-it "And that‘s it !")And that‘s it !

React 是一个激动人心的库, 它给我们带来了前所未有的开发体验, 但当我们沉浸在使用 React 快速实现需求的喜悦中时, 有必要去探究两个问题 : Why and How?

为什么 React 会如此流行, 原因是什么? 组件化、快速、足够简单、all in js、容易扩展、生态丰富、社区强大…

React 反映了哪些思想/理念/思路 ? 状态机、webComponents、virtual DOM、virtual stack、异步渲染、多端渲染、单向数据流、反应式更新、函数式编程…

React 这些理念/思路受什么启发 ? 怎么想到的 ? 又怎么实现的? …

转: https://www.processon.com/u/5a6f2254e4b0874437ae512c 设计师莫凡


>> 留言评论