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

相关文章
相关标签/搜索