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

xiaohesong發表於2019-03-02

本節是 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可以給你一個近似的結果,因為它突顯灰色的計算機例項,以及帶紫色的複合例項:

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

為了完成這個重構,我們將引入一個將完整樹安裝到容器節點的函式,就像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的實現記錄

相關文章