React从零实现-组件渲染和setState

上一篇文章中我们实现了节点创建和渲染,但是忽略组件的情况,这一篇,我们来说说组件如何渲染,并实现一个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系列列表:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值