react源碼解析之stack reconciler

關於源碼解讀的系列文章,能夠關注個人github的這個倉庫, 如今纔剛剛寫,後續有空就寫點。爭取把react源碼剖析透學習透。有不正確的地方但願你們幫忙指正。你們互相學習,共同進步。html

本篇文章是官方文檔的翻譯,英文原文請訪問官網node

這個章節是stack reconciler的一些實現說明.react

它的技術性很強並假定你能徹底理解React的公開API,以及它是如何劃分爲核心、渲染器和協調器的。若是你對React代碼不是很熟悉,請先閱讀代碼概覽ios

它還假定你可以理解React組件、實例和元素的區別。git

Stack reconciler 被用在React 15 以及更早的版本中, 它在源代碼中的位置是src/renderers/shared/stack/reconciler.github

視頻:從零開始構建React

Paul O'Shannessy給出了一個關於從零開始構建React的討論,在很大程度上對本文檔給予了啓發。算法

本文檔與上邊的視頻都是對實際代碼庫的簡化,所以你能夠經過熟悉二者來更好地理解。數組

概述

協調器自己沒有公共 API. 可是諸如React DOM 和React Native的渲染器使用它依據用戶所編寫的React組件來有效地更新用戶界面.瀏覽器

以遞歸過程的形式裝載

讓咱們考慮首次裝載組件的情形:緩存

ReactDOM.render(<App />, rootEl);

React DOM會將 <App />傳遞給協調器。請記住, <App />是一個React元素,也就是說是對哪些要渲染的東西的說明。你能夠把它當作一個普通的對象:

console.log(<App />);
// { type: App, props: {} }

協調器(reconciler)會檢查 App是類仍是函數。若是 App 是函數,協調器會調用App(props)來獲取所渲染的元素。若是App是類,協調器則會使用new App(props)建立一個App實例,調用 componentWillMount() 生命週期方法,進而調用 render() 方法來獲取所渲染的元素。不管如何,協調器都會學習App元素的「渲染行爲」。

此過程是遞歸的。App 可能渲染爲<Greeting />,而<Greeting />可能渲染爲 <Button />,如此類推。由於協調器會依次學習他們各自將如何渲染,因此協調器會遞歸地「向下鑽取」全部用戶定義組件。

你能夠經過以下僞代碼來理解該過程:

function isClass(type) {
  // React.Component的子類都會含有這一標誌
  return (
    Boolean(type.prototype) &&
    Boolean(type.prototype.isReactComponent)
  );
}

// This function takes a React element (e.g. <App />)
// and returns a DOM or Native node representing the mounted tree.
// 此函數讀取一個React元素(例如<App />)
// 並返回一個表達所裝載樹的DOM或內部節點。
function mount(element) {
  var type = element.type;
  var props = element.props;

  // 咱們以此判斷所渲染元素:
  // 是以函數型運行該類型
  // 仍是建立新實例並調用render()。
  var renderedElement;
  if (isClass(type)) {
    // Component class
    var publicInstance = new type(props);
    // Set the props
    publicInstance.props = props;
    // Call the lifecycle if necessary
    if (publicInstance.componentWillMount) {
      publicInstance.componentWillMount();
    }
    // 調用render()以獲取所渲染元素
    renderedElement = publicInstance.render();
  } else {
    // 組件函數
    renderedElement = type(props);
  }

  // 該過程是遞歸實現,緣由在於組件可能返回一個其它組件類型的元素。
  return mount(renderedElement);

  // 注意:該實現不完整,且將無窮遞歸! 它只處理<App />或<Button />等元素。尚不處理<div />或<p />等元素。
}

var rootEl = document.getElementById('root');
var node = mount(<App />);
rootEl.appendChild(node);
注意:

這的確是一段僞代碼。它與真實的實現不一樣。它會致使棧溢出,由於咱們尚未討論什麼時候中止遞歸。

讓咱們回顧一下上面示例中的幾個關鍵概念:

  • React元素是表示組件類型(例如 App)與屬性的普通對象。
  • 用戶定義組件(例如 App)能夠爲類或者函數,但它們都會被「渲染爲」元素。
  • 「裝載」(Mounting)是一個遞歸過程,當給定頂級React元素(例如<App />)時建立DOM或內部節點樹。

裝載主機元素(Mounting Host Elements)

該過程將沒有任何意義,若是最終沒有渲染內容到屏幕上。

除了用戶定義的(「複合」)組件外, React元素還可能表示特定於平臺的(「主機」)組件。例如,Button可能會從其渲染方法中返回 <div />

若是元素的type屬性是一個字符串,即表示咱們正在處理一個主機元素(host element):

console.log(<div />);
// { type: 'div', props: {} }

主機元素(host elements)不存在關聯的用戶定義代碼。

當協調器遇到主機元素(host element)時,它會讓渲染器(renderer)裝載它(mounting)。例如,React DOM將會建立一個DOM節點。

若是主機元素(host element)有子級,協調器(reconciler)則會用上述相同算法遞歸地將它們裝載。而無論子級是主機元素(如<div><hr /></div>)仍是混合元素(如<div><Button /></div>)或是二者兼有。

由子級組件生成的DOM節點將被追加到DOM父節點,同時整的DOM結構會被遞歸裝配。

注意:

協調器自己(reconciler)並不與DOM捆綁。裝載(mounting)的具體結果(有時在源代碼中稱爲「裝載映像」)取決於渲染器(renderer),可能爲 DOM節點(React DOM)、字符串(React DOM服務器)或表示本機視圖的數值(React Native)。

咱們來擴展一下代碼,以處理主機元素(host elements):

function isClass(type) {
  //  React.Component 子類含有這一標誌
  return (
    Boolean(type.prototype) &&
    Boolean(type.prototype.isReactComponent)
  );
}

// 該函數僅處理含複合類型的元素。  例如,它處理<App />和<Button />,但不處理<div />。
function mountComposite(element) {
  var type = element.type;
  var props = element.props;

  var renderedElement;
  if (isClass(type)) {
    // 組件類
    var publicInstance = new type(props);
    // 設置屬性
    publicInstance.props = props;
    // 若必要,則調用生命週期函數
    if (publicInstance.componentWillMount) {
      publicInstance.componentWillMount();
    }
    renderedElement = publicInstance.render();
  } else if (typeof type === 'function') {
    // 組件函數
    renderedElement = type(props);
  }

  // 該過程是遞歸,一旦該元素爲主機(如<div />}而非複合(如<App />)時,則逐漸結束
  return mount(renderedElement);
}

// 該函數僅處理含主機類型的元素(handles elements with a host type)。 例如,它處理<div />和<p />但不處理<App />。
function mountHost(element) {
  var type = element.type;
  var props = element.props;
  var children = props.children || [];
  if (!Array.isArray(children)) {
    children = [children];
  }
  children = children.filter(Boolean);

  // 該代碼塊不可出如今協調器(reconciler)中。
  // 不一樣渲染器(renderers)可能會以不一樣方式初始化節點。
  // 例如,React Native會生成iOS或Android視圖。
  var node = document.createElement(type);
  Object.keys(props).forEach(propName => {
    if (propName !== 'children') {
      node.setAttribute(propName, props[propName]);
    }
  });

  // 裝載子節點
  children.forEach(childElement => {
    // 子節點有多是主機元素(如<div />)或複合元素(如<Button />).
    // 因此咱們應該遞歸的裝載
    var childNode = mount(childElement);

    // 此行代碼還是特定於渲染器的。不一樣的渲染器則會使用不一樣的方法
    node.appendChild(childNode);
  });

  // 返回DOM節點做爲裝載結果
  // 此處即爲遞歸結束.
  return node;
}

function mount(element) {
  var type = element.type;
  if (typeof type === 'function') {
    // 用戶定義的組件
    return mountComposite(element);
  } else if (typeof type === 'string') {
    // 平臺相關的組件,好比說瀏覽器中的div,ios和安卓中的視圖
    return mountHost(element);
  }
}

var rootEl = document.getElementById('root');
var node = mount(<App />);
rootEl.appendChild(node);

該代碼可以工做但仍與協調器(reconciler)的真正實現相差甚遠。其所缺乏的關鍵部分是對更新的支持。

介紹內部實例

React 的關鍵特徵是您能夠從新渲染全部內容, 它不會從新建立 DOM 或重置狀態:

ReactDOM.render(<App />, rootEl);
// 應該從新使用現存的 DOM:
ReactDOM.render(<App />, rootEl);

可是, 上面的實現只知道如何裝載初始樹。它沒法對其執行更新, 由於它沒有存儲全部必需的信息, 例如全部 publicInstance ,
或者哪一個 DOM 節點 對應於哪些組件。

堆棧協調(stack reconciler)的基本代碼是經過使 mount () 函數成爲一個方法並將其放在類上來解決這一問題。
這種方式有一些缺陷,可是目前代碼中仍然使用的是這種方式。不過目前咱們也正在重寫協調器(reconciler)

咱們將建立兩個類: DOMComponent 和 CompositeComponent , 而不是單獨的 mountHost 和 mountComposite 函數。

兩個類都有一個接受 element 的構造函數, 以及一個能返回已裝入節點的 mount () 方法。咱們將用一個能實例化正確類的工廠函數替換掉以前
例子裏的mount函數:

function instantiateComponent(element) {
  var type = element.type;
  if (typeof type === 'function') {
    // 用戶自定義組件
    return new CompositeComponent(element);
  } else if (typeof type === 'string') {
    // 特定於平臺的組件
    return new DOMComponent(element);
  }  
}

首先, 讓咱們考慮如何實現 CompositeComponent:

class CompositeComponent {
  constructor(element) {
    this.currentElement = element;
    this.renderedComponent = null;
    this.publicInstance = null;
  }

  getPublicInstance() {
    // 針對複合組合, 返回類的實例.
    return this.publicInstance;
  }

  mount() {
    var element = this.currentElement;
    var type = element.type;
    var props = element.props;

    var publicInstance;
    var renderedElement;
    if (isClass(type)) {
      // 組件類
      publicInstance = new type(props);
      // 設置屬性
      publicInstance.props = props;
      // 若是有必要,調用生命週期
      if (publicInstance.componentWillMount) {
        publicInstance.componentWillMount();
      }
      renderedElement = publicInstance.render();
    } else if (typeof type === 'function') {
      // Component function
      publicInstance = null;
      renderedElement = type(props);
    }

    // Save the public instance
    this.publicInstance = publicInstance;

    // 經過element實例化內部的child實例,這個實例有多是DOMComponent,好比<div /> or <p />
    // 也多是CompositeComponent 好比說<App /> or <Button />
    var renderedComponent = instantiateComponent(renderedElement);
    this.renderedComponent = renderedComponent;

    // 增長渲染輸出
    return renderedComponent.mount();
  }
}

這與咱們之前的 mountComposite() 實現沒有太大的不一樣, 但如今咱們能夠保存一些信息,
好比this.currentElementthis.renderedComponentthis.publicInstance ,這些保存的信息會在更新期間被使用。

請注意, CompositeComponent的實例與用戶提供的 element.type 的實例不是一回事。
CompositeComponent是咱們的協調器(reconciler)的一個實現細節, 從不向用戶公開。
用戶自定義類是咱們從 element.type 讀取的,而且經過 CompositeComponent 建立它的一個實例。

爲避免混亂,咱們將CompositeComponentDOMComponent的實例稱爲「內部實例」。
因爲它們的存在, 咱們能夠將一些長壽數據(ong-lived)與它們關聯起來。只有渲染器(renderer)和協調器(reconciler)知道它們的存在。

另外一方面, 咱們將用戶定義的類的實例稱爲 "公共實例"(public instance)。公共實例是您在 render() 和自定義組件的其餘方法中看到的 this

mountHost() 函數被重構爲 DOMComponent 類上的 mount()方法, 也看起來很熟悉:

class DOMComponent {
  constructor(element) {
    this.currentElement = element;
    this.renderedChildren = [];
    this.node = null;
  }

  getPublicInstance() {
    // For DOM components, only expose the DOM node.
    return this.node;
  }

  mount() {
    var element = this.currentElement;
    var type = element.type;
    var props = element.props;
    var children = props.children || [];
    if (!Array.isArray(children)) {
      children = [children];
    }

    // Create and save the node
    var node = document.createElement(type);
    this.node = node;

    // Set the attributes
    Object.keys(props).forEach(propName => {
      if (propName !== 'children') {
        node.setAttribute(propName, props[propName]);
      }
    });

    // Create and save the contained children.
    // Each of them can be a DOMComponent or a CompositeComponent,
    // depending on whether the element type is a string or a function.
    var renderedChildren = children.map(instantiateComponent);
    this.renderedChildren = renderedChildren;

    // Collect DOM nodes they return on mount
    var childNodes = renderedChildren.map(child => child.mount());
    childNodes.forEach(childNode => node.appendChild(childNode));

    // Return the DOM node as mount result
    return node;
  }
}

從 mountHost () 重構後的主要區別在於, 咱們如今將 this.nodethis.renderedChildren 與內部 DOM 組件實例相關聯。
咱們還將使用它們在未來應用非破壞性更新。

所以, 每一個內部實例 (複合實例或主機實例)(composite or host) 如今都指向內部的子實例。爲幫助可視化, 若是功能 <App> 組件呈現 <Button> 類組件, 而且 <Button> 類呈現<div>, 則內部實例樹將以下所顯示:

[object CompositeComponent] {
  currentElement: <App />,
  publicInstance: null,
  renderedComponent: [object CompositeComponent] {
    currentElement: <Button />,
    publicInstance: [object Button],
    renderedComponent: [object DOMComponent] {
      currentElement: <div />,
      node: [object HTMLDivElement],
      renderedChildren: []
    }
  }
}

在 DOM 中, 您只會看到<div> 。可是, 內部實例樹同時包含複合和主機內部實例(composite and host internal instances)。

內部的複合實例須要存儲下面的信息:

  • 當前元素(The current element).
  • 若是元素類型是類, 則將類實例化並存爲公共實例(The public instance if element type is a class).
  • 一個經過運行render()以後並傳入工廠函數而獲得的內部實例(renderedComponent)。它能夠是一個DOMComponent或一個CompositeComponent

內部的主機實例須要存儲下面的信息:

  • 當前元素(The current element).
  • DOM 節點(The DOM node).
  • 全部的內部子實例,他們能夠是 DOMComponent or a CompositeComponent。(All the child internal instances. Each of them can be either a DOMComponent or a CompositeComponent).

若是你很難想象一個內部的實例樹是如何在更復雜的應用中構建的, React DevTools)能夠給出一個很是接近的近似,由於它突出顯示了帶有灰色的主機實例,以及用紫色表示的組合實例:

<img src="../images/implementation-notes-tree.png" width="500" style="max-width: 100%" alt="React DevTools tree" />

爲了完成這個重構,咱們將引入一個函數,它將一個完整的樹掛載到一個容器節點,就像ReactDOM.render()。它返回一個公共實例,也相似於 ReactDOM.render():

function mountTree(element, containerNode) {
  // 建立頂級內部實例
  var rootComponent = instantiateComponent(element);

  // 將頂級組件裝載到容器中
  var node = rootComponent.mount();
  containerNode.appendChild(node);

  // 返回它所提供的公共實例
  var publicInstance = rootComponent.getPublicInstance();
  return publicInstance;
}

var rootEl = document.getElementById('root');
mountTree(<App />, rootEl);

卸載(Unmounting)

如今,咱們有了保存有它們的子節點和DOM節點的內部實例,咱們能夠實現卸載。對於一個複合組件(composite component),卸載將調用一個生命週期鉤子而後遞歸進行。

class CompositeComponent {

  // ...

  unmount() {
    // Call the lifecycle hook if necessary
    var publicInstance = this.publicInstance;
    if (publicInstance) {
      if (publicInstance.componentWillUnmount) {
        publicInstance.componentWillUnmount();
      }
    }

    // Unmount the single rendered component
    var renderedComponent = this.renderedComponent;
    renderedComponent.unmount();
  }
}

對於DOMComponent,卸載操做讓每一個孩子進行卸載:

class DOMComponent {

  // ...

  unmount() {
    // Unmount all the children
    var renderedChildren = this.renderedChildren;
    renderedChildren.forEach(child => child.unmount());
  }
}

在實踐中,卸載DOM組件也會刪除事件偵聽器並清除一些緩存,爲了便於理解,咱們暫時跳過這些細節。

如今咱們能夠添加一個頂級函數,叫做unmountTree(containerNode),它與ReactDOM.unmountComponentAtNode()相似:

function unmountTree(containerNode) {
  // Read the internal instance from a DOM node:
  // (This doesn't work yet, we will need to change mountTree() to store it.)
  var node = containerNode.firstChild;
  var rootComponent = node._internalInstance;

  // Unmount the tree and clear the container
  rootComponent.unmount();
  containerNode.innerHTML = '';
}

爲了使其工做,咱們須要從一個DOM節點讀取一個內部根實例。咱們將修改 mountTree() 以將 _internalInstance 屬性添加到DOM 根節點。
咱們也將教mountTree()去銷燬任何現存樹,以便未來它能夠被屢次調用:

function mountTree(element, containerNode) {
  // Destroy any existing tree
  if (containerNode.firstChild) {
    unmountTree(containerNode);
  }

  // Create the top-level internal instance
  var rootComponent = instantiateComponent(element);

  // Mount the top-level component into the container
  var node = rootComponent.mount();
  containerNode.appendChild(node);

  // Save a reference to the internal instance
  node._internalInstance = rootComponent;

  // Return the public instance it provides
  var publicInstance = rootComponent.getPublicInstance();
  return publicInstance;
}

如今,能夠反覆運行unmountTree()或者 mountTree(),清除舊樹而且在組件上運行 componentWillUnmount() 生命週期鉤子。

更新(Updating)

在上一節中,咱們實現了卸載。然而,若是每一個組件的prop的變更都要卸載並掛載整個樹,這是不可接受的。幸虧咱們設計了協調器。
協調器(reconciler)的目標是重用已存在的實例,以便保留DOM和狀態:

var rootEl = document.getElementById('root');

mountTree(<App />, rootEl);
// 應該重用現有的DOM:
mountTree(<App />, rootEl);

咱們將用一種方法擴展咱們的內部實例。
除了 mount()unmount()DOMComponentCompositeComponent將實現一個新的方法,它叫做 receive(nextElement):

class CompositeComponent {
  // ...

  receive(nextElement) {
    // ...
  }
}

class DOMComponent {
  // ...

  receive(nextElement) {
    // ...
  }
}

它的工做是作任何須要的工做,以使組件(及其任何子節點) 可以根據 nextElement 提供的信息保持信息爲最新狀態。

這是常常被描述爲"virtual DOM diffing"的部分,儘管真正發生的是咱們遞歸地遍歷內部樹,並讓每一個內部實例接收到更新指令。

更新複合組件(Updating Composite Components)

當一個複合組件接收到一個新元素(element)時,咱們運行componentWillUpdate()生命週期鉤子。

而後,咱們使用新的props從新render組件,並得到下一個render的元素(rendered element):

class CompositeComponent {

  // ...

  receive(nextElement) {
    var prevProps = this.currentElement.props;
    var publicInstance = this.publicInstance;
    var prevRenderedComponent = this.renderedComponent;
    var prevRenderedElement = prevRenderedComponent.currentElement;

    // Update *own* element
    this.currentElement = nextElement;
    var type = nextElement.type;
    var nextProps = nextElement.props;

    // Figure out what the next render() output is
    var nextRenderedElement;
    if (isClass(type)) {
      // Component class
      // Call the lifecycle if necessary
      if (publicInstance.componentWillUpdate) {
        publicInstance.componentWillUpdate(nextProps);
      }
      // Update the props
      publicInstance.props = nextProps;
      // Re-render
      nextRenderedElement = publicInstance.render();
    } else if (typeof type === 'function') {
      // Component function
      nextRenderedElement = type(nextProps);
    }

    // ...

下一步,咱們能夠看一下渲染元素的type。若是自從上次渲染,type 沒有被改變,組件接下來能夠被適當更新。

例如,若是它第一次返回 <Button color="red" />,而且第二次返回 <Button color="blue" />,咱們能夠告訴內部實例去 receive() 下一個元素:

// ...

    // 若是被渲染元素類型沒有被改變,
    // 重用現有的組件實例.
    if (prevRenderedElement.type === nextRenderedElement.type) {
      prevRenderedComponent.receive(nextRenderedElement);
      return;
    }

    // ...

可是,若是下一個被渲染元素和前一個相比有一個不一樣的type ,咱們不能更新內部實例。由於一個 <button> 不「能變」爲一個 <input>.

相反,咱們必須卸載現有的內部實例並掛載對應於渲染的元素類型的新實例。
例如,這就是當一個以前被渲染的元素<button />以後又被渲染成一個 <input /> 的過程:

// ...

    // If we reached this point, we need to unmount the previously
    // mounted component, mount the new one, and swap their nodes.

    // Find the old node because it will need to be replaced
    var prevNode = prevRenderedComponent.getHostNode();

    // Unmount the old child and mount a new child
    prevRenderedComponent.unmount();
    var nextRenderedComponent = instantiateComponent(nextRenderedElement);
    var nextNode = nextRenderedComponent.mount();

    // Replace the reference to the child
    this.renderedComponent = nextRenderedComponent;

    // Replace the old node with the new one
    // Note: this is renderer-specific code and
    // ideally should live outside of CompositeComponent:
    prevNode.parentNode.replaceChild(nextNode, prevNode);
  }
}

總而言之,當一個複合組件(composite component)接收到一個新元素時,它可能會將更新委託給其渲染的內部實例((rendered internal instance),
或者卸載它,並在其位置上掛一個新元素。

另外一種狀況下,組件將從新掛載而不是接收一個元素,而且這發生在元素的key變化時。本文檔中,咱們不討論key 處理,由於它將使本來複雜的教程更加複雜。

注意,咱們須要添加一個叫做getHostNode()的新方法到內部實例(internal instance),以即可以定位特定於平臺的節點並在更新期間替換它。
它的實現對兩個類都很簡單:

class CompositeComponent {
  // ...

  getHostNode() {
    // 請求渲染的組件提供它(Ask the rendered component to provide it).
    // 這將遞歸地向下鑽取任何組合(This will recursively drill down any composites).
    return this.renderedComponent.getHostNode();
  }
}

class DOMComponent {
  // ...

  getHostNode() {
    return this.node;
  }  
}

更新主機組件(Updating Host Components)

主機組件實現(例如DOMComponent), 是以不一樣方式更新.當它們接收到一個元素時,它們須要更新底層特定於平臺的視圖。在 React DOM 中,這意味着更新 DOM 屬性:

class DOMComponent {
  // ...

  receive(nextElement) {
    var node = this.node;
    var prevElement = this.currentElement;
    var prevProps = prevElement.props;
    var nextProps = nextElement.props;    
    this.currentElement = nextElement;

    // Remove old attributes.
    Object.keys(prevProps).forEach(propName => {
      if (propName !== 'children' && !nextProps.hasOwnProperty(propName)) {
        node.removeAttribute(propName);
      }
    });
    // Set next attributes.
    Object.keys(nextProps).forEach(propName => {
      if (propName !== 'children') {
        node.setAttribute(propName, nextProps[propName]);
      }
    });

    // ...

接下來,主機組件須要更新它們的子元素。與複合組件不一樣的是,它們可能包含多個子元素。

在這個簡化的例子中,咱們使用一個內部實例的數組並對其進行迭代,是更新或替換內部實例,這取決於接收到的type是否與以前的type匹配。
真正的調解器(reconciler)同時在賬戶中獲取元素的key而且追蹤變更,除了插入與刪除,可是咱們如今先忽略這一邏輯。

咱們在列表中收集DOM操做,這樣咱們就能夠批量地執行它們。

// ...

    // // 這些是React元素(element)數組:
    var prevChildren = prevProps.children || [];
    if (!Array.isArray(prevChildren)) {
      prevChildren = [prevChildren];
    }
    var nextChildren = nextProps.children || [];
    if (!Array.isArray(nextChildren)) {
      nextChildren = [nextChildren];
    }
    // 這些是內部實例(internal instances)數組:
    var prevRenderedChildren = this.renderedChildren;
    var nextRenderedChildren = [];

    // 當咱們遍歷children時,咱們將向數組中添加操做。
    var operationQueue = [];

    // 注意:如下章節大大減化!
    // 它不處理reorders,空children,或者keys。
    // 它只是用來解釋整個流程,而不是具體的細節。

    for (var i = 0; i < nextChildren.length; i++) {
      // 嘗試爲這個子級獲取現存內部實例。
      var prevChild = prevRenderedChildren[i];

      // 若是在這個索引下沒有內部實例,那說明是一個child被添加了末尾。
      // 這時應該去建立一個內部實例,掛載它,並使用它的節點。
      if (!prevChild) {
        var nextChild = instantiateComponent(nextChildren[i]);
        var node = nextChild.mount();

        // 記錄一下咱們未來須要append一個節點(node)
        operationQueue.push({type: 'ADD', node});
        nextRenderedChildren.push(nextChild);
        continue;
      }

      // 若是它的元素類型匹配,咱們只須要更新該實例便可  
      // 例如, <Button size="small" /> 能夠更新爲
      // <Button size="large" /> 可是不能被更新爲 <App />.
      var canUpdate = prevChildren[i].type === nextChildren[i].type;

      // 若是咱們不能更新現有的實例,咱們就必須卸載它。而後裝一個新的替代它。
      if (!canUpdate) {
        var prevNode = prevChild.getHostNode();
        prevChild.unmount();

        var nextChild = instantiateComponent(nextChildren[i]);
        var nextNode = nextChild.mount();

        // 記錄一下咱們未來須要替換這些nodes
        operationQueue.push({type: 'REPLACE', prevNode, nextNode});
        nextRenderedChildren.push(nextChild);
        continue;
      }

      // 若是咱們能夠更新現存的內部實例(internal instance),
      // 咱們僅僅把下一個元素傳入其receive便可,讓其receive函數處理它的更新便可
      prevChild.receive(nextChildren[i]);
      nextRenderedChildren.push(prevChild);
    }

    // 最後,卸載(unmount)哪些不存在的children
    for (var j = nextChildren.length; j < prevChildren.length; j++) {
      var prevChild = prevRenderedChildren[j];
      var node = prevChild.getHostNode();
      prevChild.unmount();

      // 記錄一下咱們未來須要remove這些node
      operationQueue.push({type: 'REMOVE', node});
    }

    // Point the list of rendered children to the updated version.
    this.renderedChildren = nextRenderedChildren;

    // ...

做爲最後一步,咱們執行DOM操做。仍是那句話,真正的協調器(reconciler)代碼更復雜,由於它還能處理移動:

// ...

    // 處理隊列裏的operation。
    while (operationQueue.length > 0) {
      var operation = operationQueue.shift();
      switch (operation.type) {
      case 'ADD':
        this.node.appendChild(operation.node);
        break;
      case 'REPLACE':
        this.node.replaceChild(operation.nextNode, operation.prevNode);
        break;
      case 'REMOVE':
        this.node.removeChild(operation.node);
        break;
      }
    }
  }
}

這是用來更新主機組件(host components)的。

頂級更新(Top-Level Updates)

如今 CompositeComponent 與 DOMComponent 都實現了 receive(nextElement) 方法,
咱們如今能夠改變頂級 mountTree() 函數了,當元素(element)的type相同時,咱們可使用receive了。

function mountTree(element, containerNode) {
  // Check for an existing tree
  if (containerNode.firstChild) {
    var prevNode = containerNode.firstChild;
    var prevRootComponent = prevNode._internalInstance;
    var prevElement = prevRootComponent.currentElement;

    // 若是能夠,使用現存根組件
    if (prevElement.type === element.type) {
      prevRootComponent.receive(element);
      return;
    }

    // 不然,卸載現存樹
    unmountTree(containerNode);
  }

  // ...

}

如今調用 mountTree()兩次,一樣的類型不會先卸載再裝載了:

var rootEl = document.getElementById('root');

mountTree(<App />, rootEl);
// 複用現存 DOM:
mountTree(<App />, rootEl);

These are the basics of how React works internally.

咱們遺漏的還有什麼?

與真正的代碼庫相比,這個文檔被簡化了。有一些重要的方面咱們沒有提到:

  • 組件能夠渲染null,並且,協調器(reconciler)能夠處理數組中的「空槽(empty slots)」並顯示輸出。
  • 協調器(reconciler)能夠從元素中讀取 key ,而且用它來創建在一個數組中內部實例與元素的對應關係。實際的 React 實現的大部分複雜性與此相關。
  • 除了複合和主機內部實例類以外,還存在用於「文本」和「空」組件的類。它們表示文本節點和經過渲染 null獲得的「空槽」。
  • 渲染器(Renderers)使用injection

將主機內部類傳遞給協調器(reconciler)。例如,React DOM 告訴協調器使用 ReactDOMComponent 做爲主機內部實現實例。

  • 更新子列表的邏輯被提取到一個名爲 ReactMultiChild 的mixin中,它被主機內部實例類實如今 React DOM和 React Native時都使用。
  • 協調器也實現了在複合組件(composite components)中支持setState()。事件處理程序內部的多個更新將被打包成一個單一的更新。
  • 協調器(reconciler)還負責複合組件和主機節點的refs。
  • 在DOM準備好以後調用的生命週期鉤子,例如 componentDidMount() 和 componentDidUpdate(),收集到「回調隊列」,並在單個批處理中執行。
  • React 將當前更新的信息放入一個名爲「事務」的內部對象中。事務對於跟蹤掛起的生命週期鉤子的隊列、

爲了warning而嵌套的當前DOM(the current DOM nesting for the warnings)以及任何「全局」到特定的更新都是有用的。
事務還能夠確保在更新後「清除全部內容」。例如,由 React DOM提供的事務類在任何更新以後恢復input的選中與否。

直接查看代碼(Jumping into the Code)

  • ReactMount 中能夠查看此教程中相似 mountTree()unmountTree() 的代碼.

它負責裝載(mounting)和卸載(unmounting)頂級組件。
ReactNativeMount is its React Native analog.

在教程中與DOMComponent等同. 它實現了 React DOM渲染器(renderer)的主機組件類(host component class。
ReactNativeBaseComponent is its React Native analog.

在教程中與 CompositeComponent 等同. 它處理調用用戶定義的組件並維護它們的狀態。

是一個具備 mountComponent(), receiveComponent(), 和 unmountComponent() 方法的封裝.
它調用內部實例的底層實現,但也包含了全部內部實例實現共享的代碼。

根據元素的 key ,實現了mounting、updating和unmounting的邏輯.

獨立於渲染器的操做隊列,實現了處理child的插入、刪除和移動

  • 因爲遺留的緣由 mount(), receive(), and unmount() 被稱做 mountComponent(),

receiveComponent(), and unmountComponent() 可是他們卻接收elements

  • 內部實例的屬性以一個下劃線開始, 例如, _currentElement. 在整個代碼庫中,它們被認爲是隻讀的公共字段。

將來方向(Future Directions)

堆棧協調器具備固有的侷限性, 如同步和沒法中斷工做或分割成區塊。
咱們正在實現一個新的協調器Fiber reconciler,
你能夠在這裏看它的具體思路
未來咱們會用fiber協調器代替stack協調器(譯者注:其實如今react16已經發布,在react16中fiber算法已經取代了stack算法)

下一步(Next Steps)

閱讀next section以瞭解有關協調器的當前實現的詳細信息。

相關文章
相關標籤/搜索