從代碼實踐潛入React內部,深入diff

本節是 stack reconciler程序的實現說明的集合。

本文有一定的技術含量,要對React公共API以及它如何分爲核心,渲染器和協調(和解,reconciler)程序有很深的理解。如果你對React代碼庫不是很熟悉,請首先閱讀代碼庫概述

它還假設你瞭解React組件的實例和元素之間的差異

stack reconciler用於15版本和早期. 它的代碼在 src/renderers/shared/stack/reconciler.

視頻:從頭開始構建React

Paul O’Shannessy談到了從頭開始構建react,這在很大程度上啓發了這個文檔。

本文檔和他的演講都是對實際代碼庫的簡化,因此你可以通過熟悉它們來獲得更好的理解。

概述

reconciler(協調,調解)本身不存在公共的API。像React DOM和React Native這樣的渲染器使用它根據用戶編寫的React組件有效地更新用戶界面。

掛載(mounting)作爲遞歸過程

讓我們考慮第一次掛載組件:

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

React DOM會將<App />傳遞給調節器(reconciler)。請記住,<App />是一個React元素,即對要呈現的內容的描述。你可以將其視爲普通對象(筆者:不瞭解的可以查看這篇文章):

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

調解器會檢查這個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)
  );
}

// 這個函數接受一個React元素 (例如 <App />)
// 並且返回一個已經掛載了樹的DOM或原生節點
function mount(element) {
  var type = element.type;
  var props = element.props;

  // 我們將確定渲染元素的類型
  // 函數就直接調用
  // 類就實例化後調用render().
  var renderedElement;
  if (isClass(type)) {
    // 類組件
    var publicInstance = new type(props);
    // 設置props
    publicInstance.props = props;
    // 必要時調用生命週期方法
    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的elements只是一個純對象,用來描述組件的類型(如:App)和他的props.
  • 用戶定義的組件(如:App)可以是函數或者類,但是他們都會渲染這些元素。
  • “Mounting”是一個遞歸過程,它在給定頂級React元素(例如<App />)的情況下創建DOM或Native樹。

Mounting計算機(Host)元素

如果我們沒有在屏幕上呈現某些內容,則此過程將毫無用處。

除了用戶定義的(“複合”)組件之外,React元素還可以表示特定於平臺的(“計算機”)組件。例如,Button可能會從其render方法返回<div />

如果element的type屬性是一個字符串,我們認爲正在處理一個計算機元素:

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

沒有與計算機元素關聯的用戶定義代碼。

當協調程序(調解器)遇到這些計算機元素時,它會讓渲染器(renderer)負責mounting它。例如,React DOM將創建一個DOM節點。

如果計算機元素具有子節點,則協調器以與上述相同的算法遞歸地mounts它們。子節點是否是計算機元素(<div><hr /></div>)或用戶合成的組件(<div><Button /></div>),都沒有關係,都會去讓渲染器去負責mounting它。

由子組件生成的DOM節點將附加到父DOM節點,並且將遞歸地組裝完整的DOM結構。

注意: 調解器本身與DOM無關。mounting(安裝)的確切結果(有時在源代碼中稱爲“mount image”)取決於渲染器,可以是DOM節點(React DOM),字符串(React DOM Server)或表示原生視圖(React Native)。

如果我們要擴展代碼來處理計算機元素,它將如下所示:

function isClass(type) {
  // 繼承自 React.Component 類有一個標籤 isReactComponent
  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);
    // 設置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);
}

// 這個函數僅僅處理計算機元素
// 例如它處理<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);

    
  // 這段代碼不應該在協調器中。
  // 不同的渲染器可能對節點進行不同的初始化。
  // 例如,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);

    // 下面這個也是一個特定於平臺的
    // 它會根據不同的渲染器來處理,這裏只是一個假設他是一個dom渲染器
    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 />)
    return mountHost(element);
  }
}

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

這是有效的,但仍遠未達到協調者的實際運行方式。關鍵的缺失部分是對更新的支持。

介紹內部實例

react的關鍵特點是你可以重新渲染所有東西,它不會重新創建DOM或重置狀態。

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

但是,我們上面的實現只知道如何掛載初始樹。它無法對其執行更新,因爲它不存儲所有必需的信息,例如所有publicInstances,或哪些DOM節點對應於哪些組件。

堆棧協調器代碼庫通過使mount函數成爲一個類上面的方法來解決這個問題。但是這種方法存在一些缺點,我們在正在進行的協調重寫任務中正朝着相反的方向去發展(筆者:目前fiber已經出來了)。不過 這就是它現在的運作方式。

我們將創建兩個類:DOMComponentCompositeComponent,而不是單獨的mountHostmountComposite函數。

兩個類都有一個接受元素的構造函數,以及一個返回已安裝節點的mount()方法。我們將用實例化類的工廠替換頂級mount()函數:

function instantiateComponent(element) {
  var type = element.type;
  if (typeof type === 'function') {
    // 用戶定義的組件
    return new CompositeComponent(element);
  } else if (typeof type === 'string') {
    // 特定於平臺的組件,如計算機組件(<div />)
    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)) {
      // Component class
      publicInstance = new type(props);
      // Set the props
      publicInstance.props = props;
      // Call the lifecycle if necessary
      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;

    // 根據元素實例化子內部實例
    // 他將是DOMComponent,例如<div />, <p />
    // 或者是CompositeComponent,例如<App />,<Button />
    var renderedComponent = instantiateComponent(renderedElement);
    this.renderedComponent = renderedComponent;

    // Mount the rendered output
    return renderedComponent.mount();
  }
}

這與我們之前的mountComposite()實現沒什麼不同,但現在我們可以存儲一些信息,例如this.currentElement,this.renderedComponentthis.publicInstance,在更新期間使用。

請注意,CompositeComponent的實例與用戶提供的element.type的實例不同。CompositeComponent是我們的協調程序的實現細節,永遠不會向用戶公開。用戶定義的類是我們從element.type讀取的,CompositeComponent會創建這個類的實例。

爲避免混淆,我們將CompositeComponentDOMComponent的實例叫做“內部實例”。 它們存在,因此我們可以將一些長期存在的數據與它們相關聯。只有渲染器和調解器知道它們存在。

相反,我們將用戶定義類的實例稱爲“公共實例(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]);
      }
    });

    // 創建並保存包含的子元素
    // 這些子元素,每個都可以是DOMComponent或CompositeComponent
    // 這些匹配是依賴於元素類型的返回值(string或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));

    // DOM節點作爲mount的節點返回
    return node;
  }
}

與上面的相比,mountHost()重構之後的主要區別是現在將this.nodethis.renderedChildren與內部DOM組件實例相關聯。我們會用他來用於在後面做非破壞性的更新。

因此,每個內部實例(複合或主機)現在都指向其子級內部實例。爲了幫助可視化,如果函數<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>。但是,內部實例樹包含複合和主機內部實例。

複合內部實例需要存儲:

  • 當前元素
  • 公共實例,如果當前元素類型是個類
  • 單個呈現的內部實例。它可以是DOMComponentCompositeComponent

計算機內部實例需要存儲:

  • 當前元素
  • DOM節點
  • 所有子級的內部實例,這些子級中的每一個都可以是DOMComponentCompositeComponent

如果你正在努力想象如何在更復雜的應用程序中構建內部實例樹,React DevTools可以給你一個近似的結果,因爲它突顯灰色的計算機實例,以及帶紫色的複合實例:

爲了完成這個重構,我們將引入一個將完整樹安裝到容器節點的函數,就像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);

卸載

既然我們有內部實例來保存它們的子節點和DOM節點,那麼我們就可以實現卸載。對於複合組件,卸載會調用生命週期方法並進行遞歸。

class CompositeComponent {

  // ...

  unmount() {
    // 必要的時候調用生命週期方法
    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() {
    // 卸載所有的子級
    var renderedChildren = this.renderedChildren;
    renderedChildren.forEach(child => child.unmount());
  }
}

實際上,卸載DOM組件也會刪除事件偵聽器並清除一些緩存,但我們將跳過這些細節。

我們現在可以添加一個名爲unmountTree(containerNode)的新頂級函數,它類似於ReactDOM.unmountComponentAtNode():

function unmountTree(containerNode) {
  // 從DOM節點讀取內部實例
  // (目前這個不會正常工作, 我們將需要改變mountTree()方法去存儲)
  var node = containerNode.firstChild;
  var rootComponent = node._internalInstance;

  // 清除容器並且卸載樹
  rootComponent.unmount();
  containerNode.innerHTML = '';
}

爲了讓他工作,我們需要從DOM節點讀取內部根實例。我們將修改mountTree()以將_internalInstance屬性添加到DOM根節點。我們還將讓mountTree()去銷燬任何現有樹,以便可以多次調用它:

function mountTree(element, containerNode) {
  // 銷燬存在的樹
  if (containerNode.firstChild) {
    unmountTree(containerNode);
  }

  // 創建頂層的內部實例
  var rootComponent = instantiateComponent(element);

  // 掛載頂層的組件到容器
  var node = rootComponent.mount();
  containerNode.appendChild(node);

  // 保存內部實例的引用
  node._internalInstance = rootComponent;

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

現在,重複運行unmountTree()或運行mountTree(),刪除舊樹並在組件上運行componentWillUnmount()生命週期方法。

更新

在上一節中,我們實現了卸載。但是,如果每個prop更改導致卸載並安裝整個樹,則React就會顯得不是很好用了。協調程序的目標是儘可能重用現有實例來保留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提供的描述保持同步。

這是經常被描述爲“虛擬DOM區別”的部分,儘管真正發生的是我們遞歸地遍歷內部樹並讓每個內部實例接收更新。

更新複合組件

當複合組件接收新元素時,我們運行componentWillUpdate()生命週期方法。

然後我們使用新的props重新渲染組件,並獲取下一個渲染元素:

class CompositeComponent {

  // ...

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

    // 更新自有的元素
    this.currentElement = nextElement;
    var type = nextElement.type;
    var nextProps = nextElement.props;

    // 弄清楚下一個render()的輸出是什麼
    var nextRenderedElement;
    if (isClass(type)) {
      // 類組件
      // 必要的時候調用生命週期
      if (publicInstance.componentWillUpdate) {
        publicInstance.componentWillUpdate(nextProps);
      }
      // 更新props
      publicInstance.props = nextProps;
      // Re-render
      nextRenderedElement = publicInstance.render();
    } else if (typeof type === 'function') {
      // 函數式組件
      nextRenderedElement = type(nextProps);
    }

    // ...

接下來,我們可以查看渲染元素的type。如果自上次渲染後type未更改,則下面的組件也可以在之前的基礎上更新。

例如,如果第一次返回<Button color =“red"/>,第二次返回<Button color =“blue"/>,我們可以告訴相應的內部實例receive()下一個元素:

    // ...

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

    // ...

但是,如果下一個渲染元素的類型與先前渲染的元素不同,我們無法更新內部實例。<button />不可能變成<input />

相反,我們必須卸載現有的內部實例並掛載與呈現的元素類型相對應的新實例。例如,當先前呈現<button />的組件呈現<input />時,會發生這種情況:

    // ...

    // 如果我們到達了這一點,那麼我們就需要卸載之前掛載的組件
    // 掛載新的一個,並且交換他們的節點

    // 找到舊的節點,因爲我們需要去替換他
    var prevNode = prevRenderedComponent.getHostNode();

    // 卸載舊的子級並且掛載新的子級
    prevRenderedComponent.unmount();
    var nextRenderedComponent = instantiateComponent(nextRenderedElement);
    var nextNode = nextRenderedComponent.mount();

    // 替換對子級的引用
    this.renderedComponent = nextRenderedComponent;

    // 新的節點替換舊的
    // 記住:下面的代碼是特定於平臺的,理想情況下是在CompositeComponent之外的
    prevNode.parentNode.replaceChild(nextNode, prevNode);
  }
}

總而言之,當複合組件接收到新元素時,它可以將更新委託給其呈現的內部實例,或者卸載它並在其位置安裝新的實例。

在另一個條件下,組件將重新安裝而不是接收元素,即元素的key已更改。我們不討論本文檔中的key處理,因爲它爲原本就很複雜的教程增加了更多的複雜性。

請注意,我們需要將一個名爲getHostNode()的方法添加到內部實例協定中,以便可以在更新期間找到特定於平臺的節點並替換它。它的實現對於兩個類都很簡單:

class CompositeComponent {
  // ...

  getHostNode() {
    // 請求渲染的組件提供他
    // 這將向下遞歸複合組件
    return this.renderedComponent.getHostNode();
  }
}

class DOMComponent {
  // ...

  getHostNode() {
    return this.node;
  }  
}

更換計算機組件

計算機組件實現,例如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;

    // 移除舊的屬性
    Object.keys(prevProps).forEach(propName => {
      if (propName !== 'children' && !nextProps.hasOwnProperty(propName)) {
        node.removeAttribute(propName);
      }
    });
    // 設置接下來的屬性
    Object.keys(nextProps).forEach(propName => {
      if (propName !== 'children') {
        node.setAttribute(propName, nextProps[propName]);
      }
    });

    // ...

然後,計算機組件需要更新他們的子組件。與複合組件不同,它們可能包含多個子組件。

在這個簡化的示例中,我們使用內部實例數組並對其進行迭代,根據接收的類型是否與之前的類型匹配來更新或替換內部實例。除了插入和刪除之外,真正的協調程序還會使用元素的鍵跟蹤移動,但我們將省略此邏輯。

我們在列表中收集子級的DOM操作,以便批量執行它們:

    // ...

    // 這個是React elements數組
    var prevChildren = prevProps.children || [];
    if (!Array.isArray(prevChildren)) {
      prevChildren = [prevChildren];
    }
    var nextChildren = nextProps.children || [];
    if (!Array.isArray(nextChildren)) {
      nextChildren = [nextChildren];
    }
    // 這是內部實例的數組:
    var prevRenderedChildren = this.renderedChildren;
    var nextRenderedChildren = [];

    // 當我們迭代子級的時候,我們將會添加操作到數組
    var operationQueue = [];

    //注意:下面的部分非常簡單!
    //它的存在只是爲了說明整個流程,而不是細節。

    for (var i = 0; i < nextChildren.length; i++) {
      // 嘗試獲取此子級的現有內部實例
      var prevChild = prevRenderedChildren[i];

      // 如果這個索引下不存在內部實例,那就把子級被追加到後面。
      // 創建一個新的內部實例,掛載他並使用他的節點
      if (!prevChild) {
        var nextChild = instantiateComponent(nextChildren[i]);
        var node = nextChild.mount();

        // 記錄我們需要追加的節點
        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();

        // 記錄我們需要交換的節點
        operationQueue.push({type: 'REPLACE', prevNode, nextNode});
        nextRenderedChildren.push(nextChild);
        continue;
      }

      // 如果我們可以更新一個存在的內部實例
      // 只需要讓他接收下一個元素並且處理他自己的更新
      prevChild.receive(nextChildren[i]);
      nextRenderedChildren.push(prevChild);
    }

    // 最後卸載不存在的元素的子級
    for (var j = nextChildren.length; j < prevChildren.length; j++) {
      var prevChild = prevRenderedChildren[j];
      var node = prevChild.getHostNode();
      prevChild.unmount();

      // 記錄我們需要移除的節點
      operationQueue.push({type: 'REMOVE', node});
    }

    // 將渲染的子級列表指到更新的版本里
    this.renderedChildren = nextRenderedChildren;

    // ...

作爲最後一步,我們執行DOM操作。同樣,真正的協調代碼更復雜,因爲它也處理移動:

    // ...

    // Process the operation queue.
    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;
      }
    }
  }
}

這就是更新計算機組件(DOMComponent)

頂層更新

現在CompositeComponentDOMComponent都實現了receive(nextElement)方法,我們可以更改頂級mountTree()函數,以便在元素類型與上次相同時使用它:

function mountTree(element, containerNode) {
  // 檢查存在的樹
  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);
// Reuses the existing DOM:
mountTree(<App />, rootEl);

這些是React內部工作原理的基礎知識。

我們遺漏了什麼

與真實代碼庫相比,本文檔得到了簡化。我們沒有解決幾個重要方面:

  • 組件可以呈現null,並且協調程序可以處理數組中的“空”並呈現輸出。
  • 協調程序還從元素中讀取key,並使用它來確定哪個內部實例對應於數組中的哪個元素。實際React實現中的大部分複雜性與此相關。
  • 除了複合和計算機內部實例類之外,還有“text”和“empty”組件的類。它們代表文本節點和通過呈現null獲得的“空槽”。
  • 渲染器使用注入將計算機內部類傳遞給協調程序。例如,React DOM告訴協調程序使用ReactDOMComponent作爲計算機內部實例實現。
  • 更新子項列表的邏輯被提取到名爲ReactMultiChildmixin中,它由React DOMReact Native中的計算機內部實例類實現使用。
  • 協調程序還在複合組件中實現對setState()的支持。事件處理程序內的多個更新將被批處理爲單個更新。
  • 協調器還負責將引用附加和分離到複合組件和計算機節點。
  • 在DOM準備好之後調用的生命週期方法(例如componentDidMount()componentDidUpdate())將被收集到“回調隊列”中並在單個批處理中執行。
  • React將有關當前更新的信息放入名爲“transaction”的內部對象中。transaction對於跟蹤待處理生命週期方法的隊列、警告當前DOM的嵌套以及特定更新的“全局”其他任何內容都很有用。事務還確保React在更新後“清理所有內容”。例如,React DOM提供的事務類在任何更新後恢復輸入選擇。

進入代碼

  • ReactMount是本教程中的mountTree()unmountTree()之類的代碼。他負責安裝和卸載頂層的組件。ReactNativeMount是React Native的模擬。
  • ReactDOMComponent等同於本教程中的DOMComponent。它實現了React DOM渲染器的計算機組件類。ReactNativeBaseComponent是對React Native的模擬。
  • ReactCompositeComponent是等同於本教程中的CompositeComponent。他處理用戶自定義的組件並維護狀態。
  • instantiateReactComponent用於選擇要爲元素構造的內部實例類。它等同於本教程中的instantiateComponent()
  • ReactReconciler裏是mountComponent(),receiveComponent(), unmountComponent()方法。它調用內部實例上的底層實現,但也包括一些由所有內部實例實現共享的代碼。
  • ReactChildReconciler實現獨立於渲染器處理子級的插入,刪除和移動的操作隊列。
  • 由於遺留原因,mount()receive()unmount()在React代碼庫中實際上稱爲mountComponent()receiveComponent()unmountComponent(),但它們接收元素。
  • 內部實例上的屬性以下劃線開頭,例如_currentElement。它們被認爲是整個代碼庫中的只讀公共字段。

未來的發展方向

堆棧協調器(stack reconciler)具有固有的侷限性,例如同步並且無法中斷工作或將其拆分爲塊。新的 Fiber reconciler正在進行中(筆:當然,大家都知道,目前已經完成了),他們有完全不同的架構。在未來,我們打算用它替換堆棧協調程序,但目前它遠非功能校驗。

下一步

閱讀下一節,瞭解我們用於React開發的指導原則。

原文: Implementation Notes

原譯文: react的實現記錄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章