React原始碼解析

風靈使發表於2018-03-28

前言

  • 適合有一定 React 專案經驗閱讀,預設對 React 的常用 api 較為熟悉
  • 研究 React 原始碼是結合網上的一些分析文章+自己看程式碼理解
  • 最開始看是因為專案中遇到效能問題,網上沒有相關資料,所以想找到具體影響的點
  • 以下的程式碼解析以 15.4.1 版本為基礎,且去除了開發環境的warning,為了區分,保留的註釋都為英文,新增的註釋為中文,儘量保持原註釋
  • 文中有部分自己的演繹、理解、猜測,如有誤煩請指出

基礎概念

ReactElement

  • 資料類,只包含 props refs key
  • React.creatElement(ReactElement.js) 建立,React.createClassrender 中返回的實際也是個ReactElement

ReactComponent

  • 控制類,包含元件狀態,操作方法等
  • 包括字元元件、原生 DOM 元件、自定義元件(和空元件)
  • 在掛載元件(mountComponent)的時候,會呼叫到 instantiateReactComponent
    方法,利用工廠模式,通過不同的輸入返回不同的 component

程式碼(instantiateReactComponent.js):

function instantiateReactComponent(node, shouldHaveDebugID) {
var instance;
if (node === null || node === false) {
instance = ReactEmptyComponent.create(instantiateReactComponent);
} else if (typeof node === 'object') {
var element = node;
// Special case string values
if (typeof element.type === 'string') {
  instance = ReactHostComponent.createInternalComponent(element);
} else if (isInternalComponentType(element.type)) {
  // This is temporarily available for custom components that are not string
  // representation, we can drop this code path.
} else {
  instance = new ReactCompositeComponentWrapper(element);
}
} else if (typeof node === 'string' || typeof node === 'number') {
instance = ReactHostComponent.createInstanceForText(node);
} else {
}
// These two fields are used by the DOM and ART diffing algorithms
// respectively. Instead of using expandos on components, we should be
// storing the state needed by the diffing algorithms elsewhere.
instance._mountIndex = 0;
instance._mountImage = null;
return instance;
}
  • ReactDOMTextComponent 只關心文字,ReactDOMComponent 會稍微簡單一些,ReactCompositeComponent 需要關心的最多,包括得到原生 DOM 的渲染內容

ReactClass

這個比較特殊,對比 ES5 寫法: var MyComponent = React.createClass({}),ES6寫法:class MyComponent extends React.Component,為什麼用createClass卻得到了Component呢?通過原始碼來看,這兩個 api 的實現幾乎是一樣的,也可以看到,ES6 的寫法簡潔的多,不用那些getInitialState等特定 api,React 在之後的版本也會拋棄createClass這個 api。並且,在此 api 中,React 進行了autobind
ReactClass.js:

var ReactClass = {
  createClass: function (spec) {
    // ensure that Constructor.name !== 'Constructor'
    var Constructor = identity(function (props, context, updater) {
      // Wire up auto-binding
      if (this.__reactAutoBindPairs.length) {
        bindAutoBindMethods(this);
      }
      this.props = props;
      this.context = context;
      this.refs = emptyObject;
      this.updater = updater || ReactNoopUpdateQueue;
      this.state = null;
      // ReactClasses doesn't have constructors. Instead, they use the
      // getInitialState and componentWillMount methods for initialization.
      var initialState = this.getInitialState ? this.getInitialState() : null;
      this.state = initialState;
    });
    Constructor.prototype = new ReactClassComponent();
    Constructor.prototype.constructor = Constructor;
    Constructor.prototype.__reactAutoBindPairs = [];
    injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor));
    mixSpecIntoComponent(Constructor, spec);
    // Initialize the defaultProps property after all mixins have been merged.
    if (Constructor.getDefaultProps) {
      Constructor.defaultProps = Constructor.getDefaultProps();
    }
    // Reduce time spent doing lookups by setting these on the prototype.
    for (var methodName in ReactClassInterface) {
      if (!Constructor.prototype[methodName]) {
        Constructor.prototype[methodName] = null;
      }
    }
    return Constructor;
  }
}
var ReactClassComponent = function () {};
_assign(ReactClassComponent.prototype, ReactComponent.prototype, ReactClassMixin);

ReactComponent.js:

function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}
ReactComponent.prototype.isReactComponent = {};
ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
ReactComponent.prototype.forceUpdate = function (callback) {
  this.updater.enqueueForceUpdate(this);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'forceUpdate');
  }
};

物件池

  • 開闢空間是需要一定代價的
  • 如果引用釋放而進入 gc,gc 會比較消耗效能和時間,如果記憶體抖動(大量的物件被建立又在短時間內馬上被釋放)而頻繁 gc 則會影響使用者體驗
  • 既然建立和銷燬物件是很耗時的,所以要儘可能減少建立和銷燬物件的次數
  • 使用時候申請(getPooled)和釋放(release)成對出現,使用一個物件後一定要釋放還給池子(釋放時候要對內部變數置空方便下次使用)

程式碼(PooledClass.js):

// 只展示部分
var oneArgumentPooler = function (copyFieldsFrom) {
  var Klass = this;
  if (Klass.instancePool.length) {
    var instance = Klass.instancePool.pop();
    Klass.call(instance, copyFieldsFrom);
    return instance;
  } else {
    return new Klass(copyFieldsFrom);
  }
};
var standardReleaser = function (instance) {
  var Klass = this;
  if (Klass.instancePool.length < Klass.poolSize) {
    Klass.instancePool.push(instance);
  }
};
var DEFAULT_POOL_SIZE = 10;
var DEFAULT_POOLER = oneArgumentPooler;
var addPoolingTo = function (CopyConstructor, pooler) {
  // Casting as any so that flow ignores the actual implementation and trusts
  // it to match the type we declared
  var NewKlass = CopyConstructor;
  NewKlass.instancePool = [];
  NewKlass.getPooled = pooler || DEFAULT_POOLER;
  if (!NewKlass.poolSize) {
    NewKlass.poolSize = DEFAULT_POOL_SIZE;
  }
  NewKlass.release = standardReleaser;
  return NewKlass;
};
var PooledClass = {
  addPoolingTo: addPoolingTo,
  oneArgumentPooler: oneArgumentPooler,
  twoArgumentPooler: twoArgumentPooler,
  threeArgumentPooler: threeArgumentPooler,
  fourArgumentPooler: fourArgumentPooler,
  fiveArgumentPooler: fiveArgumentPooler
};
module.exports = PooledClass;

使用例子(ReactUpdate.js):

var transaction = ReactUpdatesFlushTransaction.getPooled();
destructor: function () {
    this.dirtyComponentsLength = null;
    CallbackQueue.release(this.callbackQueue);
    this.callbackQueue = null;
    ReactUpdates.ReactReconcileTransaction.release(this.reconcileTransaction);
    this.reconcileTransaction = null;
  }
ReactUpdatesFlushTransaction.release(transaction);
  • 可以看到,如果短時間內生成了大量的物件佔滿了池子,後續的物件是不能複用只能新建的
  • 對比連線池、執行緒池:完成任務後並不銷燬,而是可以複用去執行其他任務

事務機制

  • React 通過事務機制來完成一些特定操作,比如 merge state,update component

示意圖(Transaction.js):

這裡寫圖片描述

程式碼(Transaction.js):

var TransactionImpl = {
  perform: function (method, scope, a, b, c, d, e, f) {
    var errorThrown;
    var ret;
    try {
      this._isInTransaction = true;
      // Catching errors makes debugging more difficult, so we start with
      // errorThrown set to true before setting it to false after calling
      // close -- if it's still set to true in the finally block, it means
      // one of these calls threw.
      errorThrown = true;
      this.initializeAll(0);
      ret = method.call(scope, a, b, c, d, e, f);
      errorThrown = false;
    } finally {
      try {
        if (errorThrown) {
          // If `method` throws, prefer to show that stack trace over any thrown
          // by invoking `closeAll`.
          try {
            this.closeAll(0);
          } catch (err) {}
        } else {
          // Since `method` didn't throw, we don't want to silence the exception
          // here.
          this.closeAll(0);
        }
      } finally {
        this._isInTransaction = false;
      }
    }
    return ret;
  },
  // 執行所有 wrapper 中的 initialize 方法
  initializeAll: function (startIndex) {
  },
  // 執行所有 wrapper 中的 close 方法
  closeAll: function (startIndex) {
  }
};
module.exports = TransactionImpl;

可以看到和後端的事務是有差異的(有點類似AOP),雖然都叫transaction,並沒有commit,而是自動執行,初始方法沒有提供rollback,有二次封裝提供的(ReactReconcileTransaction.js)
下文會提到事務機制的具體使用場景

事件分發

  • 框圖(ReactBrowserEventEmitter.js)
    這裡寫圖片描述

  • 元件上宣告的事件最終繫結到了 document 上,而不是 React 元件對應的 DOM 節點,這樣簡化了 DOM 原生事件,減少了記憶體開銷

  • 以佇列的方式,從觸發事件的元件向父元件回溯,呼叫相應 callback,也就是 React 自身實現了一套事件冒泡機制,雖然 React對合成事件封裝了stopPropagation,但是並不能阻止自己手動繫結的原生事件的冒泡,所以專案中要避免手動繫結原生事件

  • 使用物件池來管理合成事件物件的建立和銷燬,好處在上文中有描述

  • ReactEventListener:負責事件註冊和事件分發

  • ReactEventEmitter:負責事件執行

  • EventPluginHub:負責事件的儲存,具體儲存在listenerBank

  • Plugin: 根據不同的事件型別,構造不同的合成事件,可以連線原生事件和元件

  • 當事件觸發時,會呼叫ReactEventListener.dispatchEvent,進行分發:找到具體的 ReactComponent,然後向上遍歷父元件,實現冒泡

  • 程式碼較多,就不具體分析了,這種統一收集然後分發的思路,可以用在具體專案中

生命週期

整體流程:
這裡寫圖片描述

  • 主要講述mountupdate,裡面也有很多相類似的操作
  • componentWillMountrendercomponentDidMount 都是在 mountComponent 中被呼叫

分析 ReactCompositeComponent.js 中的mountComponent,發現輸出是return {?string} Rendered markup to be inserted into the DOM.

mountComponent: function (transaction, hostParent, hostContainerInfo, context) {
    var _this = this;
    this._context = context;
    this._mountOrder = nextMountID++;
    this._hostParent = hostParent;
    this._hostContainerInfo = hostContainerInfo;
    var publicProps = this._currentElement.props;
    var publicContext = this._processContext(context);
    var Component = this._currentElement.type;
    var updateQueue = transaction.getUpdateQueue();
    // Initialize the public class
    var doConstruct = shouldConstruct(Component);
    // 最終會呼叫 new Component()
    var inst = this._constructComponent(doConstruct, publicProps, publicContext, updateQueue);
    var renderedElement;
    // Support functional components
    if (!doConstruct && (inst == null || inst.render == null)) {
      renderedElement = inst;
      inst = new StatelessComponent(Component);
      this._compositeType = CompositeTypes.StatelessFunctional;
    } else {
      // 大家經常在使用者端用到的 PureComponent,會對 state 進行淺比較然後決定是否執行 render
      if (isPureComponent(Component)) {
        this._compositeType = CompositeTypes.PureClass;
      } else {
        this._compositeType = CompositeTypes.ImpureClass;
      }
    }
    // These should be set up in the constructor, but as a convenience for
    // simpler class abstractions, we set them up after the fact.
    inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = updateQueue;
    this._instance = inst;
    // Store a reference from the instance back to the internal representation
    // 以 element 為 key,存在了 Map 中,之後會用到
    ReactInstanceMap.set(inst, this);
    var initialState = inst.state;
    if (initialState === undefined) {
      inst.state = initialState = null;
    }
    this._pendingStateQueue = null;
    this._pendingReplaceState = false;
    this._pendingForceUpdate = false;
    var markup;
    if (inst.unstable_handleError) {
      markup = this.performInitialMountWithErrorHandling(renderedElement, hostParent, hostContainerInfo, transaction, context);
    } else {
      markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context);
    }
    if (inst.componentDidMount) {
       transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
    }
    return markup;
  }
function shouldConstruct(Component) {
  return !!(Component.prototype && Component.prototype.isReactComponent);
}
可以看到,mountComponet 先做例項物件的初始化(props, state 等),然後呼叫performInitialMount掛載(performInitialMountWithErrorHandling最終也會呼叫performInitialMount,只是多了錯誤處理),然後呼叫componentDidMount
transaction.getReactMountReady()會得到CallbackQueue,所以只是加入到佇列中,後續執行
我們來看performInitialMount(依然在 ReactCompositeComponent.js 中)
performInitialMount: function (renderedElement, hostParent, hostContainerInfo, transaction, context) {
    var inst = this._instance;
    var debugID = 0;
    if (inst.componentWillMount) {
      inst.componentWillMount();
      // When mounting, calls to `setState` by `componentWillMount` will set
      // `this._pendingStateQueue` without triggering a re-render.
      if (this._pendingStateQueue) {
        inst.state = this._processPendingState(inst.props, inst.context);
      }
    }
    // If not a stateless component, we now render
    // 返回 ReactElement,這也就是上文說的 render 返回 ReactElement
    if (renderedElement === undefined) {
      renderedElement = this._renderValidatedComponent();
    }
    var nodeType = ReactNodeTypes.getType(renderedElement);
    this._renderedNodeType = nodeType;
    var child = this._instantiateReactComponent(renderedElement, nodeType !== ReactNodeTypes.EMPTY);
    this._renderedComponent = child;
    var markup = ReactReconciler.mountComponent(child, transaction, hostParent, hostContainerInfo, this._processChildContext(context), debugID);
    return markup;
  }
  • performInitialMount 中先呼叫componentWillMount,這個過程中 merge state,然後呼叫_renderValidatedComponent(最終會呼叫inst.render() )返回 ReactElement,然後呼叫_instantiateReactComponentReactElement 建立 ReactComponent,最後進行遞迴渲染。

  • 掛載之後,可以通過setState來更新(機制較為複雜,後文會單獨分析),此過程通過呼叫updateComponent來完成更新。我們來看updateComponent(依然在 ReactCompositeComponent.js 中)

updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
    var inst = this._instance;
    var willReceive = false;
    var nextContext;
    // context 相關,React 建議少用 context
    // Determine if the context has changed or not
    if (this._context === nextUnmaskedContext) {
      nextContext = inst.context;
    } else {
      nextContext = this._processContext(nextUnmaskedContext);
      willReceive = true;
    }
    var prevProps = prevParentElement.props;
    var nextProps = nextParentElement.props;
    // Not a simple state update but a props update
    if (prevParentElement !== nextParentElement) {
      willReceive = true;
    }
    // An update here will schedule an update but immediately set
    // _pendingStateQueue which will ensure that any state updates gets
    // immediately reconciled instead of waiting for the next batch.
    if (willReceive && inst.componentWillReceiveProps) {
      inst.componentWillReceiveProps(nextProps, nextContext);
    }
    var nextState = this._processPendingState(nextProps, nextContext);
    var shouldUpdate = true;
    if (!this._pendingForceUpdate) {
      if (inst.shouldComponentUpdate) {
        shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
      } else {
        if (this._compositeType === CompositeTypes.PureClass) {
          // 這裡,就是上文提到的,PureComponent 裡的淺比較
          shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState);
        }
      }
    }
    this._updateBatchNumber = null;
    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      // Will set `this.props`, `this.state` and `this.context`.
      this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext);
    } else {
      // If it's determined that a component should not update, we still want
      // to set props and state but we shortcut the rest of the update.
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
  }
  • updateComponent中,先呼叫componentWillReceiveProps,然後 merge state,然後呼叫shouldComponentUpdate判斷是否需要更新,可以看到,如果元件內部沒有自定義,且用的是 PureComponent,會對 state 進行淺比較,設定shouldUpdate,最終呼叫_performComponentUpdate來進行更新。而在_performComponentUpdate中,會先呼叫componentWillUpdate,然後呼叫updateRenderedComponent進行更新,最後呼叫componentDidUpdate(過程較簡單,就不列程式碼了)。下面看一下updateRenderedComponent的更新機制(依然在 ReactCompositeComponent.js 中)
_updateRenderedComponent: function (transaction, context) {
    var prevComponentInstance = this._renderedComponent;
    var prevRenderedElement = prevComponentInstance._currentElement;
    var nextRenderedElement = this._renderValidatedComponent();
    var debugID = 0;
    if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
      ReactReconciler.receiveComponent(prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context));
    } else {
      var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
      ReactReconciler.unmountComponent(prevComponentInstance, false);
      var nodeType = ReactNodeTypes.getType(nextRenderedElement);
      this._renderedNodeType = nodeType;
      var child = this._instantiateReactComponent(nextRenderedElement, nodeType !== ReactNodeTypes.EMPTY);
      this._renderedComponent = child;
      var nextMarkup = ReactReconciler.mountComponent(child, transaction, this._hostParent, this._hostContainerInfo, this._processChildContext(context), debugID);
      this._replaceNodeWithMarkup(oldHostNode, nextMarkup, prevComponentInstance);
    }
  },

可以看到,如果需要更新,則呼叫ReactReconciler.receiveComponent,會遞迴更新子元件,否則直接解除安裝然後掛載。所以,重點是在shouldUpdateReactComponent的判斷,React 為了簡化 diff,所以有一個假設:在元件層級、type、key 不變的時候,才進行比較更新,否則先 unmount 然後重新 mount。來看shouldUpdateReactComponent(shouldUpdateReactComponent.js) :

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 {
    // 如果為 ReactDomComponent 或 ReactCompositeComponent,則需要層級 type 和 key 相同,才進行 update(層級在遞迴中保證相同)
    return nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key;
  }
}

接下來是重頭戲:setState,上文中已經提到了此 api 為:

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

可以看到這裡只是簡單的呼叫enqueueSetState放入佇列中,而我們知道,不可能這麼簡單的。來看enqueueSetState(ReactUpdateQueue.js中),this.updater會在 mount 時候賦值為updateQueue

enqueueSetState: function (publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
    if (!internalInstance) {
      return;
    }
    // 獲取佇列,如果為空則建立
    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    // 將待 merge 的 state 放入佇列
    queue.push(partialState);
    // 將待更新的元件放入佇列
    enqueueUpdate(internalInstance);
  },
  function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
      // 上文提到的以 element 為 key 存入 map,這裡可以取到 component
    var internalInstance = ReactInstanceMap.get(publicInstance);
    if (!internalInstance) {
      return null;
    }
    return internalInstance;
  }

再來看enqueueUpdate(ReactUpdates.js):

function enqueueUpdate(component) {
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}
  • 可以看到,如果不處於isBatchingUpdates時,則呼叫batchingStrategy.batchedUpdates,如果處於的話,則將 component 放入 dirtyComponents 中等待以後處理。這樣保證了避免重複 render,因為mountComponentupdateComponent 執行的開始,會將isBatchingUpdates設定為true,之後以事務的方式處理,包括最後時候將isBatchingUpdates置為false。
  • 大家一定對 batchingStrategydirtyComponents的定義,batchingStrategyReactUpdates.injection 注入,而dirtyComponents 是定義在ReactUpdates.js 中,也就是說二者都為全域性的
  • 綜上,在特定生命週期getInitialState,componentWillMount,render,componentWillUpdate中呼叫setState,並不會引起updateComponent(componentDidMount、componentDidUpdate 中會)。來看batchedUpdates(ReactDefaultBatchingStrategy.js):
batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      // 注意這裡,上一個程式碼塊中可以看到,當 isBatchingUpdates 為 false 時,callback 為 enqueueUpdate 自身
      // 所以即以事務的方式處理
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  }
  var transaction = new ReactDefaultBatchingStrategyTransaction();
  • 可以看到,當以事務的方式呼叫進入enqueueUpdate時,isBatchingUpdates已經為true,所以執行dirtyComponents.push(component);
  • 注意到callbakc其實就是自身enqueueUpdate,當isBatchingUpdates為false時,也用transaction.perform呼叫enqueueUpdate,使得結果一樣
  • 詳細介紹事務 transaction 的應用,上文中提到過,事務可以利用wrapper封裝,開始和結束時會呼叫所有 wrapper 的相應方法,來看這兩個wrapper: RESET_BATCHED_UPDATES FLUSH_BATCHED_UPDATES(ReactDefaultBatchingStrategy.js):
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
// flushBatchedUpdates 在 ReactUpdates.js 中
var flushBatchedUpdates = function () {
  // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
  // asapEnqueued 為提前執行回撥,暫不分析
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }
    if (asapEnqueued) {
    }
  }
};
  • 但是,仔細看上面的過程,把元件放入 dirtyComponents 後,事務結束馬上就執行 close方法進行了處理了,和之前理解的流程好像不太一致?這時候再回頭看mountComponent和updateComponent,它們的引數:@param {ReactReconcileTransaction} transaction,也就是說整個過程都在ReactReconcileTransaction事務中(事件回撥同理),自然在其中的生命週期呼叫setState不用引起重複 render,只會將 state 放入佇列和將元件放入 dirtyComponents 中,然後在結束後統一處理
  • ReactReconcileTransaction中 initialize 用於清空回撥佇列;close 用於觸發回撥函式componentDidMount、componentDidUpdate 執行
  • 我開始一直比較疑惑的是ReactDefaultBatchingStrategy.batchedUpdates中的ReactDefaultBatchingStrategyTransaction和ReactReconcileTransaction到底是什麼關係?我試圖找出兩個transaction 中 wrapper 是否有 merge 的情況,發現沒有。目前大概的理解和結論是這樣的:整個生命週期就是一個 transaction,即對應ReactDefaultBatchingStrategy.batchedUpdates,而ReactReconcileTransaction粒度較小,負責單個元件(所以也能看到,前者直接new,而後者利用了物件池)。通過各自 wrapper 可以看到,前者([FLUSH_BATCHED_UPDATES,RESET_BATCHED_UPDATES])負責了全部元件更新 和 callback,後者([SELECTION_RESTORATION, EVENT_SUPPRESSION,ON_DOM_READY_QUEUEING)負責了各自元件自身的問題,如 focus 等。
  • 例證:ReactDom 中呼叫render(插入過程),實際最終呼叫了 ReactMount的_renderNewRootComponent,其中執行了ReactUpdates.batchedUpdates(batchedMountComponentIntoNode,componentInstance, container, shouldReuseMarkup, context);(注意出現了batchedUpdates),而batchedMountComponentIntoNode中呼叫了ReactUpdates.ReactReconcileTransaction.getPooled,這樣,巢狀關係就聯絡起來了
  • 例證: ReactEventListenerdispatchEvent,會呼叫ReactUpdates.batchedUpdates(handleTopLevelImpl,bookKeeping); 和上述同理
  • 熟悉 React 生命週期的同學一定對父子元件各生命週期的執行順序很清晰(比如 componentWillMount是從父到子),以上述的理論,是如何保證的麼?上文中可以看到,FLUSH_BATCHED_UPDATESclose方法利呼叫了runBatchedUpdates,來看這個方法(ReactUpdates.js):
function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  // reconcile them before their children by sorting the array.
  dirtyComponents.sort(mountOrderComparator);
  // Any updates enqueued while reconciling must be performed after this entire
  // batch. Otherwise, if dirtyComponents is [A, B] where A has children B and
  // C, B could update twice in a single batch if C's render enqueues an update
  // to B (since B would have already updated, we should skip it, and the only
  // way we can know to do so is by checking the batch counter).
  updateBatchNumber++;
  for (var i = 0; i < len; i++) {
    // If a component is unmounted before pending changes apply, it will still
    // be here, but we assume that it has cleared its _pendingCallbacks and
    // that was is a noop.
    var component = dirtyComponents[i];
    // If performUpdateIfNecessary happens to enqueue any new updates, we
    // shouldn't execute the callbacks until the next render happens, so
    // stash the callbacks first
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;
    ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction, updateBatchNumber);
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
      }
    }
  }
}
function mountOrderComparator(c1, c2) {
  return c1._mountOrder - c2._mountOrder;
}
  • flushBatchedUpdates在事務ReactUpdatesFlushTransaction中,此事務是對ReactReconcileTransactionCallbackQueue的封裝,結束時置空dirtyComponents 並通知回撥
  • performUpdateIfNecessary最終會呼叫updateComponent,進行更新

diff 演算法

  • 傳統對於樹的 diff 演算法,時間複雜度要達到 o(n^3),這對於使用者端顯然是不能接受的。而 React基於幾個基礎假設,將時間複雜度優化為 o(n)
  • 假設(策略)
  • Web UI 中 DOM 節點跨層級的移動操作特別少,可以忽略不計
  • 擁有相同類的兩個元件將會生成相似的樹形結構,擁有不同類的兩個元件將會生成不同的樹形結構
  • 對於同一層級的一組子節點,它們可以通過唯一 id進行區分

場景

  • tree diff:只對比同層級節點(注意前文中所有程式碼中,都是隻比較prevRenderedElement和nextRenderedElement)
  • component diff: 如果型別相同則繼續比較,如果型別不同則直接解除安裝再掛載,即上文中提到的shouldUpdateReactComponent(雖然當兩個
    component 是不同型別但結構相似時,React diff 會影響效能,但正如 React 官方部落格所言:不同型別的component 是很少存在相似 DOM tree 的機會,因此為這種極端情況而做太多比較是不值得的)
  • element diff: 當一組節點處於同一層級時,React 對於每個節點提供了三種操作,分別為INSERT_MARKUP(插入)、MOVE_EXISTING(移動)、 REMOVE_NODE(刪除)
  • 上文的程式碼中,除了關心 type,還關心 key,這也是 diff 演算法的關鍵,如圖
    這裡寫圖片描述

  • 首先對新集合的節點進行迴圈遍歷,for (name in nextChildren),如果存在相同節點,則進行操作,是否移動是通過比較child._mountIndex < lastIndex,符合則進行節點移動操作(即在老集合中的位置和 lastIndex 比較),lastIndex 表示訪問過的節點在老集合中最右的位置(即最大的位置)。這是一種順序優化手段,lastIndex 一直在更新,表示訪問過的節點在老集合中最右的位置,如果新集合中當前訪問的節點比 lastIndex大,說明當前訪問節點在老集合中就比上一個節點位置靠後,則該節點不會影響其他節點的位置,因此不用新增到差異佇列中,即不執行移動操作,只有當訪問的節點比lastIndex 小時,才需要進行移動操作。來看具體過程:

  • 從新集合中取得 B,判斷老集合中存在相同節點 B,通過對比節點位置判斷是否進行移動操作,B 在老集合中的位置 B._mountIndex = 1,此時 lastIndex = 0,不滿足 child._mountIndex < lastIndex 的條件,因此不對 B 進行移動操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),其中
    prevChild._mountIndex 表示 B 在老集合中的位置,則 lastIndex = 1,並將 B的位置更新為新集合中的位置prevChild._mountIndex = nextIndex,此時新集合中 B._mountIndex = 0,nextIndex++ 進入下一個節點的判斷
  • 從新集合中取得 A,判斷老集合中存在相同節點 A,通過對比節點位置判斷是否進行移動操作,A 在老集合中的位置 A._mountIndex = 0,此時 lastIndex = 1,滿足 child._mountIndex < lastIndex的條件,因此對 A 進行移動操作 enqueueMove(this, child._mountIndex, toIndex),其中 toIndex 其實就是nextIndex,表示 A 需要移動到的位置;更新 lastIndex =Math.max(prevChild._mountIndex, lastIndex),則 lastIndex = 1,並將 A的位置更新為新集合中的位置 prevChild._mountIndex = nextIndex,此時新集合中A._mountIndex = 1,nextIndex++ 進入下一個節點的判斷。
  • 從新集合中取得 D,判斷老集合中存在相同節點 D,通過對比節點位置判斷是否進行移動操作,D 在老集合中的位置 D._mountIndex= 3,此時 lastIndex = 1,不滿足 child._mountIndex < lastIndex的條件,因此不對 D 進行移動操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),則lastIndex = 3,並將 D 的位置更新為新集合中的位置 prevChild._mountIndex = nextIndex,此時新集合中D._mountIndex = 2,nextIndex++ 進入下一個節點的判斷。
  • 從新集合中取得 C,判斷老集合中存在相同節點 C,通過對比節點位置判斷是否進行移動操作,C 在老集合中的位置 C._mountIndex= 2,此時 lastIndex = 3,滿足 child._mountIndex < lastIndex 的條件,因此對 C 進行移動操作 enqueueMove(this, child._mountIndex, toIndex);更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),則 lastIndex = 3,並將 C的位置更新為新集合中的位置 prevChild._mountIndex = nextIndex,此時新集合中 C._mountIndex= 3,nextIndex++ 進入下一個節點的判斷,由於 C 已經是最後一個節點,因此 diff 到此完成。
  • 當有新的 Component 插入時,邏輯一致,不做具體分析了
  • 當完成集合中所有節點 diff,還需要遍歷老集合,如果存在新集合中沒有但老集合中有的節點,則刪除

程式碼(ReactMultiChild.js),針對 element diff(tree diff 和 component diff 在之前的程式碼中已經提到過):

_updateChildren: function (nextNestedChildrenElements, transaction, context) {
      var prevChildren = this._renderedChildren;
      var removedNodes = {};
      var mountImages = [];
      var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, mountImages, removedNodes, transaction, context);
      if (!nextChildren && !prevChildren) {
        return;
      }
      var updates = null;
      var name;
      // `nextIndex` will increment for each child in `nextChildren`, but
      // `lastIndex` will be the last index visited in `prevChildren`.
      var nextIndex = 0;
      var lastIndex = 0;
      // `nextMountIndex` will increment for each newly mounted child.
      var nextMountIndex = 0;
      var lastPlacedNode = null;
      for (name in nextChildren) {
        if (!nextChildren.hasOwnProperty(name)) {
          continue;
        }
        var prevChild = prevChildren && prevChildren[name];
        var nextChild = nextChildren[name];
        if (prevChild === nextChild) {
          updates = enqueue(updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex));
          lastIndex = Math.max(prevChild._mountIndex, lastIndex);
          prevChild._mountIndex = nextIndex;
        } else {
          if (prevChild) {
            // Update `lastIndex` before `_mountIndex` gets unset by unmounting.
            lastIndex = Math.max(prevChild._mountIndex, lastIndex);
            // The `removedNodes` loop below will actually remove the child.
          }
          // The child must be instantiated before it's mounted.
          updates = enqueue(updates, this._mountChildAtIndex(nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context));
          nextMountIndex++;
        }
        nextIndex++;
        lastPlacedNode = ReactReconciler.getHostNode(nextChild);
      }
      // Remove children that are no longer present.
      for (name in removedNodes) {
        if (removedNodes.hasOwnProperty(name)) {
          updates = enqueue(updates, this._unmountChild(prevChildren[name], removedNodes[name]));
        }
      }
      if (updates) {
        processQueue(this, updates);
      }
      this._renderedChildren = nextChildren;
    },

綜上,在開發中,保持穩定的結構有助於效能提升,當有一組節點時,除了要設定 key,也要避免將靠後的節點移動到靠前的位置

一些其他的點

interface(ReactClass.js)

var ReactClassInterface = {
  mixins: 'DEFINE_MANY',
  statics: 'DEFINE_MANY',
  propTypes: 'DEFINE_MANY',
  contextTypes: 'DEFINE_MANY',
  childContextTypes: 'DEFINE_MANY',
  // ==== Definition methods ====
  getDefaultProps: 'DEFINE_MANY_MERGED',
  getInitialState: 'DEFINE_MANY_MERGED',
  getChildContext: 'DEFINE_MANY_MERGED',
  render: 'DEFINE_ONCE',
  // ==== Delegate methods ====
  componentWillMount: 'DEFINE_MANY',
  componentDidMount: 'DEFINE_MANY',
  componentWillReceiveProps: 'DEFINE_MANY',
  shouldComponentUpdate: 'DEFINE_ONCE',
  componentWillUpdate: 'DEFINE_MANY',
  componentDidUpdate: 'DEFINE_MANY',
  componentWillUnmount: 'DEFINE_MANY',
  // ==== Advanced methods ====
  updateComponent: 'OVERRIDE_BASE'
};
function validateMethodOverride(isAlreadyDefined, name) {
  var specPolicy = ReactClassInterface.hasOwnProperty(name) ? ReactClassInterface[name] : null;
  // Disallow overriding of base class methods unless explicitly allowed.
  if (ReactClassMixin.hasOwnProperty(name)) {
    !(specPolicy === 'OVERRIDE_BASE') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to override `%s` from your class specification. Ensure that your method names do not overlap with React methods.', name) : _prodInvariant('73', name) : void 0;
  }
  // Disallow defining methods more than once unless explicitly allowed.
  if (isAlreadyDefined) {
    !(specPolicy === 'DEFINE_MANY' || specPolicy === 'DEFINE_MANY_MERGED') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.', name) : _prodInvariant('74', name) : void 0;
  }
}

可以看到,和後端中interface(或是抽象類)還是有區別的,但是可以起到規範和檢查的作用,實際專案中可以借鑑

相關文章