按部就班DIY一個react(四)

注意:下文中,反覆提到"實例"一詞,如無特別交代,它指的是第三篇章instance的這個概念react

在react中,「component」概念能夠理解爲一個身份的象徵。假如咱們將root virtual DOM節點比做virtual DOM世界的周天子的話,那麼「component」就是管轄着一塊或大或小疆域的分封諸侯。只不過,這塊疆域不是土地,而是「virtual DOM」。算法

在沒有引入「component」這個概念以前,咱們面臨這如下幾個問題:數組

  • state是全局的。
  • 每更新一次界面,都須要手動地調用一次個render函數。
  • 對render函數的調用,會致使整樹協調的發生,界面渲染性能不高。

所以,爲了解決以上幾個問題,咱們引入「component」這個概念。「component」經過自身的兩個特徵來解決以上問題:bash

  • local state。表明component所處的界面狀態。
  • setState API。經過調用它來修改local state,從而使協調在整顆子樹上發生。

經過調用setState來更新UI,就是咱們所要實現的子樹協調。babel

「component」概念的落地工做,首先從實現base class開始。熟悉react的讀者大概都知道,component的定義中,惟一不能缺乏的方法是render方法。因此這個父類的類圖大概是這樣的: 數據結構

好,下面咱們一塊兒來實現這個父類大概的shape:架構

class Component {
    constructor(props){
        this.props = props || {}
        this.state = this.state || {}
    }
    
    setState(partialState){
        this.state = Object.assign({},this.state,partialState)
    }
    
    render(){
        console.error('You should implement your own render method!')
    }
}
複製代碼

上面的代碼其實就是作了兩個事:app

  • 將用戶調用setState時傳入的局部state合併到已有的state當中去。
  • 提醒用戶,render方法必須本身實現。

爲了區分,react把DOM elment和text element分別稱爲「DOM Component」和「Text Component」,把咱們這裏的「component」稱之爲「Composite Component」。在引入「component」這個概念後,咱們也沿用這些叫法。dom

目前爲止,咱們還沒讓local state跟子樹的協調聯繫起來。咱們調用setState,界面將不會發生任何變化。要想local state改變能跟協調算法聯動起來,本質上就是要求咱們前後回答三個問題:函數

一. 如何對接jsx的編譯?

目前咱們createElement的實現是這樣的:

function createElement(type, props, ...childrens) {
  const newProps = Object.assign({}, props);
  const hasChildren = childrens.length > 0;
  const rawChildren = hasChildren ? [].concat(...childrens) : [];
  newProps.children = rawChildren
    .filter(child => !!child)
    .map(child => {
      return child instanceof Object ? child : createTextElement(child);
    });
  return {
    type,
    props: newProps
  };
}
複製代碼

這一次,咱們不動createElement的實現。由於咱們能夠經過改動轉換jsx的babel插件的具體實現來知足咱們的需求。大概原理是,讓babel插件將以大寫字母開頭的標籤識別爲Composite Component,而後原封不動地把用戶自定義的組件類傳給咱們。咱們經過在後續的實例化(面向對象意義上的實例化),來拿到咱們想要的數據。屆時,咱們寫的是這樣的jsx:

class App extends Component {
    render(){.......}
}

render(<App />,rootDOM)
複製代碼

轉換jsx的babel插件將結合咱們實現的createElment函數編譯爲:

class App extends Component {
    render(){.......}
}

render(createElement(App,{}),rootDOM)
複製代碼

對Composite Component調用createElement返回的virtual DOM的數據將會是這樣的:

{
    type:App,// 從ES6的角度看,APP是一個「類」;從ES5的角度來看,它仍是一個函數
    props:{
        children:[]
    }
}
複製代碼

如何改動jsx轉換babel插件不在咱們的討論範圍內,故略過不表。 凡是認真閱讀過第一篇章的讀者可能就注意到了,Composite Component跟DOM Component和Text Component所對應的virtual DOM結構不一樣的一點就是:type字段的值的類型是函數(提醒:ES6的類最後仍是會被編譯爲function),而不是string了。這一點很重要。Composite Component的實例化對接工做正是基於這一點。

二. 如何對接「實例」概念?

這個問題包含了兩個問題。

第一是:Composite Component所對應的instance的數據結構是如何?

第二是:如何被實例化?

這兩位問題對應兩個任務:

task1: 肯定Composite Component所對應實例的數據結構。

顯然,Composite Component跟DOM Component和Text Component都是屬於「組件」概念範疇的,它也須要被掛載到具體的DOM節點上,也有對應的element,也應該有childInstance。只不過不一樣於先前的DOM Component和Text Component,這些字段的取值基於Composite Component的特殊性,確定會有所不一樣。在react的源碼中,Composite Component所對應的instance確實有子實例的字段,只不過這個子實例的含義並不能跟咱們從jsx結構所看到的層級關係對應上。舉個例子,DOM component裏面,若是咱們看到

<div>
    我是文本節點
    <span>我是另一個節點</span>
</div>
複製代碼

這種結構,咱們就能夠說我是文本節點<span>我是另一個節點</span>所對應的實例是<div>組件的子實例。在Composite Component的概念裏,狀況就不同了。也就是說,若是咱們看到

<MyComponent>
    我是文本節點
    <span>我是另一個節點</span
<MyComponent>
複製代碼

這種結構,咱們不能說我是文本節點<span>我是另一個節點</span>所對應的實例是<MyComponent>組件的子實例。實際上,<MyComponent>組件的子實例是另有其人。它就是組件的render函數所返回的react element所對應的實例,爲何呢?緣由很簡單,有二:

  • <MyComponent>只是一個身份的象徵,象徵着組件render方法所返回的react element。這比如日本的天皇是沒有實權的,他只是一個國家的象徵而已,掌握實權的是日本的首相。
  • <MyComponent>的子組件(我是文本節點<span>我是另一個節點</span>)最後都是被render方法經過this.props.children消費掉,成爲它返回的react element的一部分。

因此,從實現的角度來講,render方法返回的element所對應的實例纔是<MyComponent>的子實例。

由於render方法只能返回一個element,因此Composite Component只有一個子實例,也就是說Composite Component所對應的的子實例的值並非由子實例組成的數組,只是單個實例而已。一樣的,由於組件名只是一個象徵而已,那麼Composite Component對應實例的dom節點的值應該是由子實例所對應的DOM節點來充當。最後一點,若是咱們想把Composite Component的實例和它的真正實例(這裏的真正實例就是指經過new操做符調用函數所返回的對象,react裏面稱之爲publicInstance。爲了區分,第三篇章所引入instance概念又稱之爲internalInstance)對應起來,那麼咱們都須要在彼此的身上保存對方的引用。綜上所述,Composite Component所對應的實例的數據結構以下:

const instance = {
    dom: DOMObject,
    element:reactElement,
    childInstance:childInstance,
    publicInstance:realInstance // 組件類經過new操做符運算所返回的真正意義上的實例
}
複製代碼

task2: 用代碼實現Composite Component的實例化。

既然上面已經弄清楚Composite Component所對應實例的數據結構(有什麼字段,字段的值是什麼),那麼實現它的實例化也是順水推舟的事了,咱們在原有的代碼上添加上Composite Component所對應的條件分支:

function instantiate(element) {
  const { type, props } = element;
  // 根據type字段值的類型來判斷是不是Composite Component
  const isDomElement = typeof type === "string";

  // 建立對應的DOM節點
  if(isDomElement){ // 實例化DOM Component 和 Text Component
    const isTextElement = type === "TEXT_ELEMENT";
    const domNode = isTextElement
      ? document.createTextNode("")
      : document.createElement(type);

    // 設置屬性
    updateDomProperties(domNode, {}, props);

    // 對children element遞歸調用instantiate函數
    const children = props.children;
    let childInstances = [];
    if (children && children.length) {
      childInstances = children.map(childElement => instantiate(childElement));
      const childDoms = childInstances.map(childInstance => childInstance.dom);
      childDoms.forEach(childDom => domNode.appendChild(childDom));
    }

    const instance = {
      dom: domNode,
      element,
      childInstances
    };

    return instance;
  }else { // 實例化Composite Component
     const { type, props } = element;
     const instance = {};
     // component類的真正實例化
     const publicInstance = new type(props);
     
     // 將render方法返回的element的this指向publicInstance
     // 結合「this關鍵字的指向是由它執行的上下文所決定的」這句話來理解一下
     const childElement = publicInstance.render(); 
     
     // 對於Composite Component來講,render方法返回element對應的instance的dom就是它對應實例的dom
     const childInstance = instantiate(childElement);
     const dom = childInstance.dom; 

     // 按照咱們在task1討論出的數據結構,組裝component element所對應的實例
     Object.assign(instance, { dom, element, childInstance, publicInstance });
     
     // 最後,把 Composite Component所對應實例的引用保存在publicInstance身上,打通二者之間的訪問
     publicInstance.__internalInstance = instance;
     return instance;
  }

}
複製代碼

三. 如何對接「協調」概念?

Composite Component在協調算法中,對應的「初始掛載」,「刪除」和「替換」的實現跟DOM component和Text component的實現也是同樣的,比較簡單。二者之間不一樣的是「更新」部分的實現邏輯。咱們先來看看reconcile函數的簽名:

reconcile:(instance, element, domContainer) => instance
複製代碼

在這系列接近尾聲的時候,你們可能也觀察出來了。reconcile函數的第一參數instance,第二參數element是reconcile函數語義上的標誌。換句話說,協調,協調,協調的對象是誰跟誰呢?答曰:正是instance和elment。咱們要記住,不管什麼時候何刻,傳入reconcile函數的element參數都是表明着咱們渲染界面的最新意圖。而instance從設計開始,它就被定義爲用於保存當前這一幀界面的相關信息。簡而言之,咱們能夠簡單地把instance理解爲「舊的」,而element理解爲「新的」。咱們須要實現的協調,本質上就是看看目前「舊的」有什麼東西是能夠複用的。回到「component」對接「協調」概念上來,大體步驟也是同樣,不過細節有所不一樣。概括起來能夠分爲三步走:

  1. 更新publicInstance的state。
  2. 更新publicInstance的props。
  3. 更新childInstance。

這裏值得一提的是,第一步的完成不是在reconcile函數的內部來完成的,而是在咱們提供給開發者的component父類中去完成。因此,咱們得更新一下父類的實現:

class Component {
  constructor(props) {
    this.props = props;
    this.state = this.state || {};
  }

  setState(partialState) {
   // 1. 更新publicInstance的state
    this.state = Object.assign({}, this.state, partialState);
    const {
      dom,
      element
    } = this.__internalInstance
    const parentDom = dom.parentNode;
    reconcile(this.__internalInstance, element,parentDom);
  }
}
複製代碼

第一步完成以後,咱們經過在setState內部調用reconcile函數進入第二和第三步:

function reconcile(instance, element, domContainer) {
  let newInstance = {};
  // 整樹的初始掛載
  if (instance === null) {
  // .......
  } else if (element === null) { // 整樹的刪除
  // .......
  }else if(element.type !== instance.element.type){ // 整樹的替換
    // .......
  } else { // 整樹的更新
    // DOM component或者Text component
    if(typeof element.type === 'string'){
    // .......
    }else { // composite component
    
    // 2.更新publicInstance的props
    instance.publicInstance.props = element.props;
      
    // 3.更新childInstance
    const childElement = instance.publicInstance.render();
    const oldChildInstance = instance.childInstance;
    const childInstance = reconcile(oldChildInstance, childElement,domContainer);
    
    // 跟實例化過程同樣, 更新後的childInstance就是Composite Component所對應instance的childInstance;
    // 更新後的childInstance的dom就是Composite Component所對應instance的dom。
    // element原封不動地掛載上去便可
    instance.dom = childInstance.dom;
    instance.childInstance = childInstance;
    instance.element = element;
    return instance;
    }
  } 
  return newInstance;
}
複製代碼

至此,咱們已經完成了「component」概念和「協調」概念的對接工做。也就是說,如今若是咱們想要局部更新UI的話,只須要定義本身的component,而後調用setState API,這個局部UI所對應的子樹的協調就會發生了。

《按部就班DIY一個react》系列到此結束。雖然,這是一個玩具版的react,可是經過這個DIY過程,我加深了對react思想,概念和基本原理的理解。固然,還有不少基本react feature沒有實現,好比:ref,key,生命週期函數等等,更不用說改用Fiber架構以後所帶來的新feature啦。最後,真心但願這個系列能對你理解react世界帶來些許幫助,至於完整的代碼,我稍後再整理,放到codepen或者codesandbox供你們玩弄玩弄。

再見!

相關文章
相關標籤/搜索