React學習之相關堆棧調解器的實現(三十七)

這一部分講述的是堆棧調解器的實現

ReactAPI可以被分爲三部分,核心渲染器調解器,如果你對代碼庫可能有點不瞭解的話,可以看我的博客

其中堆棧調解器是React產品中最重要的部分,被React DOMReact Native渲染器共同使用,它的代碼地址src/renderers/shared/stack/reconciler

1.從零開始構建React的歷史

Paul O'Shannessy給予React開發一個非常大的靈感,它對最終的代碼庫的文檔和解說可以得到更好的理解,所以有時間可以去看看。

2.概要

調解器自身並不是一個公共API,而渲染器則作爲一個接口,通過用戶寫的React組件來被React DOMReact Native兩個渲染器有效的更新。

3.綁定就是遞歸

下面是一個綁定組件的實例

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

<App/>是一個記錄了該如何去渲染的React對象元素,React DOM<App />作爲對象傳遞給調解器,你可以認爲是如下這樣的對象

console.log(<App />);
// { type: App, props: {} }
  1. 如果App是一個組件類或者函數類,調解器就會檢查他們。

  2. 如果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元素
// 返回一個代表需要被綁定的樹的DOM或者Native節點
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 />
  // 而不能處理host組件,比如<div />或<p />
}

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

注意

上述的真是一段僞代碼,和真實的實現相差太遠,直接一看都知道問題,遞歸無法截止,所以上述只是一個簡單的思路,代碼需要完善。

我們先總結一下上述代碼的幾點關鍵點:

  1. React元素表現爲一個對象的話,可以用組件的typeprops來表示,這就是React.createElement的任務了

  2. 自定義的組件(比如App)可以是類或者是函數,他們都會返回需要渲染的元素

  3. 綁定是一個創建DOM或者Native節點樹的遞歸過程

4.綁定Host元素

如果我們不在視圖中顯示什麼的話,那麼上述的那些代碼都是沒有用的,顯示出結果是我們的目的。

除了用戶自定義的組合組件,React元素還可能是Host組件,比如Buttonrender方法中可能會返回<div/>

如果元素的type是一個字符串,我們就需要按照host元素來處理:

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

當調解器檢測到是一個host元素後,它會讓渲染器去關心如何綁定它,例如,React DOM渲染器可能就創建一個DOM節點,而其它的渲染器又會創建其它,而這些都不是調解器關心的,這裏大家可以留個心思,真是的渲染過程永遠都是渲染器的事,跟調解器半毛錢的關係都沒有。

如果一個host元素有孩子,調解器就會用同樣的方法遞歸去綁定他們(記住是綁定而不是渲染),孩子是hosthost處理,孩子是組合就按照組合的方式處理。

DOM節點被孩子組件創建出來加入父親DOM節點中,不斷遞歸的處理,最後完整的DOM結構就組裝而成了。

注意

調解器自己並不會依賴DOM,綁定的最終結果取決於渲染器,如果是DOM節點就是React DOM渲染器處理的,如果是字符串則是React DOM Server渲染器處理的,如果是一個代表着Native視圖的數字則是React Native渲染器處理的,這些都不是調解器需要關心的。

如果你擴展之前講的代碼處理host元素,那麼僞代碼就變爲了如下形式:

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);
    // 設置props
    publicInstance.props = props;
    //調用生命週期函數
    if (publicInstance.componentWillMount) {
      publicInstance.componentWillMount();
    }
    renderedElement = publicInstance.render();
  } else if (typeof type === 'function') {
    // 函數式組件
    renderedElement = type(props);
  }

  // 當我們遇到的元素是個Host組件而不是一個組合組件時
  // 這個遞歸過程就會停止
  return mount(renderedElement);
}
// 這個函數只是用來處理host組件的
// 比如 <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 => {
    // 孩子可能是host或者組合組件
    // 然後就是遞歸處理他們
    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') {
    // host組件
    return mountHost(element);
  }
}

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

這一部分代碼裏真正的調解器相差依舊是非常遙遠的,它連更新功能都沒有實現。

5.內部實例

React的一個非常關鍵的特點就是你可以重複渲染任何東西,這個重複渲染的過程並不會重新創建DOM或者是重設置state,這是非常有意思的,而是不渲染,有趣。

ReactDOM.render(<App />, rootEl);
// 下面的代碼並不會有什麼性能損失,因爲下面的代碼相當於沒有執行,有趣
ReactDOM.render(<App />, rootEl);

然而,我們之前的代碼真是一個最簡單的構造初始綁定樹的方式了,因爲我們在前面的代碼沒有進行更新階段的數據儲備,所以根本無法實行更新的操作,比如說publicInstances,或者哪個組件對應着哪個DOM節點,這些數據在初始化綁定後統統不知道,這就非常尷尬了。

這個堆棧調解器代碼庫就通過一個在類中的mount()函數方法來解決它,當然這個方法有一定缺陷,我們會嘗試重寫調解器,不過,它是怎麼工作的呢?

我們不再使用mountHostmountComposite函數,我們用兩個類來取代他們:DOMComponentCompositeComponent,因爲對象就可以存儲數據,函數一般都是純函數不影響數據儲備。

這兩個類都有一個接受element爲參數的構造函數和一個mount方法去返回被綁定的節點,而全局的mount()函數被如下的代碼替換掉了:

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

首先我們先編寫CompositeComponent類的實現:

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

  getPublicInstance() {
    // For composite components, expose the class instance.針對組合組件暴露出它的類實例
    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);
      // 設置props
      publicInstance.props = props;
      // 調用生命週期函數
      if (publicInstance.componentWillMount) {
        publicInstance.componentWillMount();
      }
      renderedElement = publicInstance.render();
    } else if (typeof type === 'function') {
      // 函數式組件沒有實例
      publicInstance = null;
      renderedElement = type(props);
    }

    // 保存實例
    this.publicInstance = publicInstance;

    // 根據元素實例化孩子內部實例
    // 這個實例可能是DOMComponent
    // 也可能是CompositeComponent
    var renderedComponent = instantiateComponent(renderedElement);
    this.renderedComponent = renderedComponent;

    // 將綁定的數據輸出
    return renderedComponent.mount();
  }
}

這個和前面的mountComposite的實現沒有多少不同,但是我們保存了一些對我們更新數據有用的信息,比如this.currentElementthis.renderedComponentthis.publicInstance

有一點要注意,我們的CompositeComponent實例和用戶自己實例化element.type是不相同的,CompositeComponenet是調解器內部的實現無法被外界使用,或者是沒有暴露出來,你並不知道它,而用戶自己通過new element.type()是不一樣的,你可以直接對他進行處理,換一句話說的是我們在類中實現的getPublicInstance()函數就是讓我們得到一個公共操作的實例,但是更加底層內部的實例呢,我們很明顯不能操作,也就只能操作當前這一層當做公共接口暴露出來的實例了。

爲了避免出現混亂,我將CompositeComponentDOMComponent稱爲內部實例,他們的存在可以長久的保存着數據,而只有渲染器和調解器能夠直接處理他們。

與此相反,我們將用戶自定義類的實例稱爲公共實例(外部實例),這個公共實例你可以直接操作。

mountHost函數被重構成了DOMComponent中的mount函數。

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

  getPublicInstance() {
    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];
    }

    // 創建並保存節點
    var node = document.createElement(type);
    this.node = node;

    // 設置節點屬性
    Object.keys(props).forEach(propName => {
      if (propName !== 'children') {
        node.setAttribute(propName, props[propName]);
      }
    });

    // 創建和保存被包含的孩子
    // 他們可能是DOMComponent或者是CompositeCompoennt
    // 這取決與他們的type是字符串還是function
    var renderedChildren = children.map(instantiateComponent);
    this.renderedChildren = renderedChildren;

    // 收集要綁定的節點
    var childNodes = renderedChildren.map(child => child.mount());
    childNodes.forEach(childNode => node.appendChild(childNode));

    // 將DOM節點作爲綁定的結果返回
    return node;
  }
}

這個和之前的代碼主要的不同就是我們重構了moutHost()並且保存了當前的noderenderedChildren來關聯內部的DOM組件實例,以後我們就可以無需聲明就可以使用他們。

總的來說,每一個內部實例,不管是組合也好,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>,然而內部實例樹既包括組合內部實例又包括host內部實例。

組合內部實例需要保存如下一些東西:

  1. 當前的元素

  2. 如果元素的type是一個類,那麼要保存公共實例

  3. 當前的元素不是DOMComponent就是CompositeComponent,我們需要保存他們渲染的內部實例。

host內部實例需要保存的:

  1. 當前的元素

  2. DOM節點

  3. 所有的孩子內部實例,他們可能是DOMComponent也可能是CompositeComponent

你可以想象一個內部實例樹怎麼去構建一個複雜的應用呢,React DevTools可以給你一個非常直觀的結果,它可以用灰色高亮host實例,用紫色來高亮組合實例

這裏寫圖片描述

然而如果要完成最終的重構,我們還要設計一個函數去進行真實的綁定操作像是ReactDOM.render(),它還會返回一個公共實例,前面的mount得到的只是要進行綁定的節點,而沒有進行真實的綁定。

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

  // 將頂部組件加入最終DOM容器中實現真正的綁定
  var node = rootComponent.mount();
  containerNode.appendChild(node);

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

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

6.卸載

現在我們已經有了保存着孩子和DOM節點的內部實例,接下來我們就可以實現卸載,對於Composite Component,卸載會遞歸的調用生命週期函數。

class CompositeComponent {

  // ...

  unmount() {
    // 調用生命週期函數
    var publicInstance = this.publicInstance;
    if (publicInstance) {
      if (publicInstance.componentWillUnmount) {
        publicInstance.componentWillUnmount();
      }
    }

    // 卸載渲染
    var renderedComponent = this.renderedComponent;
    renderedComponent.unmount();
  }
}

對於DOMComponent,需要告訴每一個孩子都要卸載

class DOMComponent {

  // ...

  unmount() {
    // 卸載所有的孩子
    var renderedChildren = this.renderedChildren;
    renderedChildren.forEach(child => child.unmount());
  }
}

實際上,卸載DOM組件也需要移除事件監聽器,清除緩衝,不過這些細節我先暫時跳過。

我們現在再增加一個全新的全局函數unmountTree(containerNode),他的功能和ReactDOM.unmountComponentAtNode()類似。與mountTree功能相反

function unmountTree(containerNode) {
  // 從DOM節點中讀取一個內部實例
  // 這一個_internalInstance內部實例我們會在mountTree給它增加
  var node = containerNode.firstChild;
  var rootComponent = node._internalInstance;

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

爲了讓上述的代碼可以正常的工作了,我們需要爲DOM節點讀取一個root內部實例,我們修改mountTree()增加一個_internalInstance屬性爲rootDOM節點,我們要告訴mountTree應該摧毀已經存在的樹,這樣纔可以多次調用:

function mountTree(element, containerNode) {
  // 摧毀已經存在的樹
  if (containerNode.firstChild) {
    unmountTree(containerNode);
  }

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

  // 將頂級內部實例的渲染結果加入DOM中
  var node = rootComponent.mount();
  containerNode.appendChild(node);

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

  // 返回一個公共實例
  var publicInstance = rootComponent.getPublicInstance();
  return publicInstance;
}

7.更新

在上面,我們實現了卸載,然而如果每一次prop改變都卸載舊的樹,構造性的樹,React就沒有存在的必要了,而調解器的作用就是爲了重複使用已經存在實例,從而達到性能的提升。

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

mountTree(<App />, rootEl);
// 下面的語句相當於沒有執行
mountTree(<App />, rootEl);

我們將擴展我們的內部實例實現一個方法,DOMComponentCompositeComponent都需要實現一個新的函數叫做receive(nextElement)

class CompositeComponent {
  // ...

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

class DOMComponent {
  // ...

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

這個函數的工作就是讓組件和它的孩子可以及時的瞭解到nextElement的信息,進行更新。

這一部分在前面被描述爲“虛擬DOM diff”,通過我們沿着內部實例樹遞歸往下走,讓每一個內部實例都可以接受到更新。

8.更新組合組件

當一個組合組件接受到一個新的元素的時候,我們會運行componeentWillUpdate()生命週期函數,然後會通過新的porps去重渲染組件,得到一個新的渲染元素。

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;
      // 重新渲染
      nextRenderedElement = publicInstance.render();
    } else if (typeof type === 'function') {
      nextRenderedElement = type(nextProps);
    }

    // ...

得到了nextRenderedElement我們就可以查看渲染的元素的type,跟我之前將更新的時候的判斷是一樣的,如果type沒有改變,那麼就向下遞歸,而不改變當前組件。

比如說,如果render第一次返回了<Button color="red"/>,第二次返回<Button color="blue">,我們就可以告訴相應的內部實例receive新元素。

   // ...

    // 如果渲染的元素type沒有變化
    // 則重使用已經存在的實例,不去新創建實例
    if (prevRenderedElement.type === nextRenderedElement.type) {
      prevRenderedComponent.receive(nextRenderedElement);
      return;
    }

    // ...

但是,如果新渲染的元素和之前的元素type不一樣的話,那麼我們就無法進行更新操作了,因爲<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;
  }  
}

9.更新Host組件

Host組件的實現就像是DOMComponent,和CompositeComponent更新截然不同,當他們接收到一個元素時,他們需要更新底層的DOM節點,就ReactDOM而言,他們會更新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]);
      }
    });

    // ...

然後,host組件就開始更新他們的孩子,不像組合組件那樣,他們只會包含最多一個孩子。

在下面這個例子中,我是用一個數組的內部實例,然後遍歷它,或者是進行更新還是替換內部實例取決於新的元素和舊的元素的type是否相同。真正的調解器還會使用元素的key去處理,當然我們暫時不會涉及這個處理邏輯。

我們可以收集DOM節點需要操作的孩子然後批量的處理他們來節省時間:

// ...

    // 在這裏React元素是一個數組
    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 = [];

    // Note: 這一份代碼是極度簡化了的
    // 他沒有處理渲染以及key的問題
    // 他只是用來說明整個處理流程而不是細節

    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.node;
        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.node;
     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;
      }
    }
  }
}

10.頂級更新

現在CompositeComponentDOMComponent都實現了receive(nextElement)方法,我們需要改變全局函數mountTree()增加在元素type相同的時候的處理。

function mountTree(element, containerNode) {
  // 檢查已經存在的樹
  if (containerNode.firstChild) {
    var prevNode = containerNode.firstChild;
    var prevRootComponent = prevNode._internalInstance;
    var prevElement = prevRootComponent.currentElement;

    // 如果可以,重利用已經存在的root組件
    if (prevElement.type === element.type) {
      prevRootComponent.receive(element);
      return;
    }

    // 否則,卸載已經存在的樹
    unmountTree(containerNode);
  }

  // ...

}

如今,兩次調用mountTree(),他能充分的利用已經存在的資源

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

mountTree(<App />, rootEl);

mountTree(<App />, rootEl);

11.我們遺漏了什麼

我的這篇博客寫的內容和真實的代碼相差甚遠,這裏我提幾個沒有涉及到的但又非常重要的幾點。

  1. 組件是可以返回null的,調解器可以處理空的數組和渲染出空的元素

  2. 調解器也能從元素中讀取key屬性,使用它們來建立內部實例對應的數組中的元素,這樣可以提高性能

  3. 除了是組合和host內部實例外,還可以是text或者空的組件,他們表示text節點和我們render返回null

  4. 渲染器使用注入方式將host內部類傳遞給調解器,比如,React DOM渲染器會告訴調解器使用ReactDOMComponent作爲host的內部實例的實現。

  5. 更新的列表孩子的邏輯被提取到了一個叫做ReactMultiChildmixin中,這個東西可以被在React DOMReact Native渲染器中的host內部實例所使用。

  6. 調解器也實現了對setState()的支持,多個更新事件可以批量處理

  7. 調解器還對組合組件和host組件的ref進行了處理

  8. DOM準備好之後,生命週期鉤子componentDidMount()componentDidUpdate()也會調用,他們被加入到了一個“回調隊列”中,去順序的批量的處理他們

  9. React將當前信息更新到內部實例中去的過程叫做“事務”,事務方式處理對於等待生命週期鉤子的隊列的跟蹤非常有用。事務能保證React在更新成功後可以清除所有東西。比如,事務類提供ReactDOM更新出現問題時可以恢復到更新之前的狀態。

12.代碼比較

  1. ReactMount的代碼有點像mountTreeunmountTree用於組件的綁定和卸載,在React Native中是這個ReactNativeMount

  2. ReactDOMComponentDOMComponent是類似的,它就是React DOM渲染器host組件類的實現,ReactNativeBaseComponent則是React Native的。

  3. ReactCompositeComponentCompositeComponent是類似的,它用來處理用戶自定義組件和他們的狀態

  4. instantiateReactComponent則構造一個元素實例,和instantiateComponent相似

  5. ReactReconciler則是mountComponent,receiveComponent,和unmountComponent的集合,它在底層實現了內部實例,但是也包括所有內部實例共享的代碼。

  6. ReactChildReconciler則是根據孩子元素的key去綁定孩子,更新孩子和卸載孩子。

  7. ReactMultiChild作爲一個獨立渲染器來處理孩子的插入和刪除工作。

  8. 因爲歷史遺留問題,在React代碼中,mount(), receive(), 和unmount()名字改爲了 mountComponent(), receiveComponent(), 和 unmountComponent(),但是他們依舊可以接受元素

  9. 內部實例由於是私有的,一般都以下劃線命名開頭。

12.將來的發展方向

在前一章中我提到過一個調解器叫做纖維調解器,它就是用來取代堆棧調解器的,因爲堆棧調解器具有一定的侷限性,同步問題,或者是無法分離代碼,耦合度比較大,等等等,個人想法而已。

下一篇將講什麼暫時不知道,嘿

發佈了447 篇原創文章 · 獲贊 471 · 訪問量 51萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章