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

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

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

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

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

視頻:從頭開始構建React

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

本文檔和他的演講都是對實際代碼庫的簡化,所以你能夠經過熟悉它們來得到更好的理解。算法

概述

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

掛載(mounting)做爲遞歸過程

讓咱們考慮第一次掛載組件:緩存

ReactDOM.render(<App />, rootEl); 複製代碼

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

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的實現記錄

相關文章
相關標籤/搜索