手把手教你写前端框架(二):自定义组件

986 阅读3分钟

本系列文章是分享我自己一步一步编写整个框架的过程,有兴趣的xdm可以参考源代码阅读。git仓库:github.com/sullay/art-…

自定义组件

通过前面努力,我们已经成功完成了dom渲染,并且借助jsx语法简化了我们虚拟dom的创建工作。这篇文中我们将开始构建自己的自定义组件。

首先要了解一件事情,就是jsx是如何处理自定义组件的。这里我直接说结论,对自定义组件h函数的第一个参数type应该是该组件对应的构造器。所以我们只需要判断出type是否为自定义组件的构造器,然后使用new type()便可以创建出组件实例。

首先我写一个组件类Component,后面所有自定义组件都继承这个类。并且在vNode中标识出来。

// 自定义组件父类
export class Component {
} 

export class vNode {
  constructor(type = '', allProps = {}, children = []) {
    // 标识自定义组件
    this.isComponent = (type.prototype instanceof Component);
    this.type = type;
    this.props = {};
    ...
  }
}

然后我们规定自定义组件都要包含一个render方法,用于自定义组件虚拟dom的创建。例如:

class App extends Component {
  render() {
    return (
      <div>
        <p style="color: red;">11111111111111111111</p>
        <p>222222222222222222</p>
      </div>
    )
  }
}

再次改造render方法,区分自定义组件。

function render(node, parentDom) {
  if (!vNode.isVNode(node)) throw new Error("渲染元素类型有误");
  const { type, props, events, children } = node;
  if (node.isComponent) {
      let newComponent = new type(props, events, children).render();
      render(newComponent, parentDom);
  } else {
      // 根据元素类型创建对应的dom
      const dom = vTextNode.isVNode(node) ? document.createTextNode('') : document.createElement(type);
      // 设置属性
      for (let key in props) dom[key] = props[key];
      // 渲染子元素
      for (let child of children) render(child, dom);
      // 监听事件
      for (let event in events) dom.addEventListener(event, events[event])
      // 绑定到父元素
      parentDom.appendChild(dom);
  }
}

此时我们已经可以正常渲染出页面。但是考虑到自定义组件的数据更新的话就会发现,上面的做法完全没办法更新组件数据。

整改代码

整改代码之前我们先梳理一下上面代码中存在的问题:

  • 虚拟dom树上没有自定义组件实例,无法实现更新
  • 每次渲染时都要去重新创建dom
  • render中每次都是使用appendChild追加dom

新增根节点

新增根节点,用来追踪整棵虚拟dom树

import { vNode } from './VNode'

const ROOT = Symbol('root');

class Root extends vNode {
}

if (!window[ROOT]) {
  window[ROOT] = new Root();
}

export default window[ROOT]; 

整改vNode

vNode类中新增了getDom、createDom两个方法,帮助我们获取和创建对应的dom元素。

新增vComponentNode类。

  • $instance用来引用自定义组件的实例,组件实例中也可以通过$vNode获取到对应的虚拟dom节点
  • vComponentNode的组件实例render渲染出来的虚拟dom树作为vComponentNode的子节点,并且vComponentNode并没有自己的dom,它的dom,它的dom只是其子节点的$dom的引用。
// 普通元素
export class vNode {
  constructor(type = '', allProps = {}, children = []) {
    this.$type = type;
    this.$props = {};
    this.$events = {};
    for (let prop in allProps) {
      if (isEvent(prop)) {
        // 从props中过滤出事件监听
        this.$events[getEventName(prop)] = allProps[prop];
      } else {
        this.$props[prop] = allProps[prop];
      }
    }
    // 处理子元素中的文字类元素
    this.$children = children.map(child => {
      return vNode.isVNode(child) ? child : new vTextNode(child);
    });
  }
  // 判断是否属于虚拟Dom元素
  static isVNode(node) {
    return node instanceof this;
  }
  getDom() {
    if (!this.$dom) this.createDom();
    return this.$dom;
  }
  createDom() {
    this.$dom = document.createElement(this.$type);
    // 设置属性
    for (let key in this.$props) this.$dom[key] = this.$props[key];
    // 监听事件
    for (let event in this.$events) this.$dom.addEventListener(event, this.$events[event]);
  }
}

// 文字元素
export class vTextNode extends vNode {
  constructor(text) {
    super(vTextNode.type, { nodeValue: text })
  }
  static type = Symbol('TEXT_ELEMENT');
  createDom() {
    this.$dom = document.createTextNode('');
    // 设置属性
    for (let key in this.$props) this.$dom[key] = this.$props[key];
  }
}

export class vComponentNode extends vNode {
  constructor(type = '', allProps = {}, slots = []) {
    super(type, allProps);
    this.$isComponent = true;
    this.$slots = slots;
    this.$instance = new type(this.$props, this.$events, slots);
    this.$instance.$vNode = this;
  }
  createDom() {
    let child = this.$instance.render();
    this.$children = [child];
    this.$dom = child.getDom();
  }
}

整改render

render方法中创建根节点。 renderDomTree方法:

  • getDom来获取虚拟Dom节点对应的真实dom。
  • 渲染的过程中赋值虚拟dom的父节点,使得整个dom树挂载到根节点上。
  • 增加第三个参数oldDom,后续更新节点时传入旧的dom节点进行替换。
// 渲染domTree
export function renderDomTree(node, parentNode, oldDom) {
  if (!vNode.isVNode(node)) throw new Error("渲染元素类型有误");

  // 设置父节点
  node.$parentNode = parentNode;
  let parentDom = parentNode.getDom();
  let dom = node.getDom();
  // 判断是否为自定义组件
  if (node.$isComponent) {
    for (let child of node.$children[0].$children) renderDomTree(child, node.$children[0]);
  } else {
    for (let child of node.$children) renderDomTree(child, node);
  }
  // 绑定到父元素
  if (oldDom) {
    parentDom.replaceChild(dom, oldDom)
  } else {
    parentDom.appendChild(dom);
  }
}

// 渲染方法
export function render(node, parentDom) {
  $root.$dom = parentDom;
  $root.$children = [node];
  renderDomTree(node, $root);
}

// 创建元素
export function h(type, props, ...children) {
  if (type.prototype instanceof Component) {
    return new vComponentNode(type, props, children);
  } else {
    return new vNode(type, props, children);
  }
}

整改完毕,经过上述的整改后,我们的框架从一个demo变成了一个真正的框架,可以更好的支持我们后续功能扩展。

下一篇文章中将完成框架数据更新,感兴趣的xdm可以关注我后续的更新。