React從零實現-元件渲染和setState

Xiaowei發表於2018-07-27

logo-og

上一篇文章中我們實現了節點建立和渲染,但是忽略元件的情況,這一篇,我們來說說元件如何渲染,並實現一個setState,來初步完成我們自己的React

React元件

在react中元件大體分為兩種,一種是一個純函式,沒有生命週期的。另一個通過繼承自React.Component的類來實現。

我們先來寫一個Component類。

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

  setState(partialState) {
    this.state = Object.assign({}, this.state, partialState);
    updateComponent(this);
  }
}
複製程式碼

我們完成了一個Component類,同時該類的例項有一個setState函式,用來更新該元件。updateComponent我們下面會實現它。

createNode函式

我們前面提到過虛擬節點的概念,但是我們但是直接使用Element下面這種形式來作為我們的虛擬節點的。

{
  type: 'div'
  props: {}
}
複製程式碼

但是但我們要更新我們的元件是,我們需要記錄Element和DOM節點之間的關係,為了不汙染Element,我們引入我們新的虛擬節點的概念,我這裡稱之為Node(實際情況並不是如此,這裡只是為了方便命名)。之後相關的虛擬幾點命名也會採用xxxNode的命名方式。下面我們來改造我們以前的render函式,將其重新命名為createNode並且只接受一個型別為Element的引數,其返回值為一個Node型別的節點。

function createNode(element) {
    const { type, props } = element;
    // 是文字節點則建立文字節點,這裡建立一個空的文字節點,後面利用nodeValue直接給該節點賦值
    const isTextNode = type === 'TEXT ELEMENT';
    const isComponent = typeof type === 'function';

    // 元件情況
    if (isComponent) {
      const instance = new type(props);
      let childElement = null;
      if (instance.render) {
        // 類情況
        childElement = instance.render();
      } else {
        // 函式情況,直接執行
        childElement = type(props);
      }

      // 建立Node節點
      const childNode = createNode(childElement);
      const dom = childNode.dom;
      const node = { dom, element, childNodes: childNode.childNodes || [] };

      // 在例項中記錄舊的node節點,以便之後進行更新
      instance._internalNode = node;
      return node;
    }

    // dom情況
    const childElements = props.children || [];
    const childDom = isTextNode
      ? document.createTextNode('')
      : document.createElement(type);

    const isEvent = name => name.startsWith('on');
    const isAttribute = name => !isEvent(name) && name !== 'children';

    // 繫結事件
    Object.keys(props).filter(isEvent).forEach(name => {
      const eventName = name.toLowerCase().substring(2);
      childDom.addEventListener(eventName, props[name]);
    });

    // 新增屬性
    Object.keys(props).filter(isAttribute).forEach(name => {
      childDom[name] = props[name];
    });

    // 遞迴建立
    const childNodes = childElements.map(createNode);

    // 掛載到父節點
    return { dom: childDom, element, childNodes }
  }
複製程式碼

從上面可以看到,我們的虛擬節點Node記錄我們需要的資訊,如element、dom、childrenNodes等,它是一個物件,結構是:

{ dom, element, childNodes }
複製程式碼

render函式

有了createNode函式,現在再寫我們的render函式:

function render(element, containerDom) {
    // 獲取虛擬節點
    const node = createNode(element);
    // 獲取對應的dom元素
    const childDom = node.dom;

    // 獲取子虛擬節點
    const childNodes = node.childNodes || [];
    // 渲染子虛擬節點
    childNodes.forEach(childNode => render(childNode.element, childDom));

    // 掛載至容器dom節點
    containerDom.appendChild(childDom);
  }
複製程式碼

render函式中,我們所需要做的就是獲取虛擬節點並直接渲染它,並且需要同時渲染其孩子節點,最後掛載到根元素就完成了我們的渲染過程。

到這裡我們已經可以渲染元件了,我們還有一個setState需要實現,實現setState就需要我們上面提到的updateComponent函式了。

updateComponent函式

updateComponent接收一個元件例項,它需要做哪些事情那?我們想一下,其實很簡單,它只需要拿到舊的dom節點,然後渲染新的dom節點,最後將舊的替換為新的就能夠實現重新整理的效果了。在這裡我們上面在例項中儲存的_internalNode就能發揮作用了。它記錄了舊節點的所有資訊。下面來實現吧:

function updateComponent(instance) {
    // 執行render函式,得到要渲染的element
    const childElement = instance.render();
    // 舊的虛擬節點
    const internalNode = instance._internalNode;

    // 獲取要掛載的父親節點
    const parentDom = internalNode.dom.parentNode;

    // 獲取新的虛擬節點
    const newNode = createNode(childElement);
    // 更新虛擬幾點
    instance._internalNode = newNode;

    // 渲染孩子節點
    const newDom = newNode.dom;
    (newNode.childNodes || []).forEach(childNode => render(childNode.element, newDom));

    // 將舊dom節點替換為新的dom節點
    parentDom.replaceChild(newDom, internalNode.dom);
  }
複製程式碼

實現完成,現在我們已經可以更新我們的元件了。這是codepen中的例項

在元件的實現過程中為了簡化,我們去掉了元件的生命週期,它需要作為一個個鉤子掛載在不同的位置。如果你使用了例項,或者自己跑過之後會發現,我們每次更新都要重新渲染整個dom樹,這樣代價很大,在React中使用了一種diff演算法來重用不需要更新的節點和屬性,下一節我們就來實現React中的diff演算法reconciliation

這是github原文地址。接下來我會持續更新,歡迎star,歡迎watch。

實現React系列列表:

相關文章