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

logo-og

上一篇文章中咱們實現了節點建立和渲染,可是忽略組件的狀況,這一篇,咱們來講說組件如何渲染,並實現一個setState,來初步完成咱們本身的Reactnode

React組件

在react中組件大致分爲兩種,一種是一個純函數,沒有生命週期的。另外一個經過繼承自React.Component的類來實現。react

咱們先來寫一個Component類。git

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咱們下面會實現它。github

createNode函數

咱們前面提到過虛擬節點的概念,可是咱們可是直接使用Element下面這種形式來做爲咱們的虛擬節點的。算法

{
  type: 'div'
  props: {}
}
複製代碼

可是但咱們要更新咱們的組件是,咱們須要記錄Element和DOM節點之間的關係,爲了避免污染Element,咱們引入咱們新的虛擬節點的概念,我這裏稱之爲Node(實際狀況並非如此,這裏只是爲了方便命名)。以後相關的虛擬幾點命名也會採用xxxNode的命名方式。下面咱們來改造咱們之前的render函數,將其從新命名爲createNode而且只接受一個類型爲Element的參數,其返回值爲一個Node類型的節點。app

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

{ 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函數中,咱們所須要作的就是獲取虛擬節點並直接渲染它,而且須要同時渲染其孩子節點,最後掛載到根元素就完成了咱們的渲染過程。post

到這裏咱們已經能夠渲染組件了,咱們還有一個setState須要實現,實現setState就須要咱們上面提到的updateComponent函數了。ui

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系列列表:

相關文章
相關標籤/搜索