100行代码实现Virtual DOM和render

·  阅读 239

基本原理

在开始实现之前,我们先来了解一下Virtual DOM是怎么样构建,并渲染到浏览器的:

  • 首先,常见的构建Virtual DOM的方法有两种,一种是jsx(react),另一种是template(vue),react的jsx一般是通过babel的jsx插件编译成createElement(type, config, children)源码, vue的template也可以通过vue-loader编译成对应的createElement(context: Component,tag: any,data: any,children: any,normalizationType: any,alwaysNormalize: boolean): VNode | Array<VNode>源码

  • 然后,在组件被创建并初始化state之后,执行render,将组件的props和state根据jsx(react)或template(vue),生成一个虚拟DOM树

  • 最后所有的VDOM生成完毕之后,将所有VDOM都mount上真实HTML DOM。

Component -> render -> VDOM -> mount -> HTML

VDOM

VDOM的节点是一个简化版的DOM对象,只存储了我们关心的属性,大大提高了DOM操作时的性能。通常一个虚拟节点(VNode)包含:标签名、子节点数组、属性、事件、key和对应的真实DOM。

export class VNode {
  type: string | Function;
  children: VNode[] = [];
  props: { key: string; value: any }[] = [];
  handles: { key: string; value: () => void }[] = [];
  key: any;
  el: Node;
}
复制代码

这时候再定义一个创建虚拟DOM对象的方法,用于jsx调用,该方法接收三个参数:标签名,标签属性,子标签。返回一个虚拟DOM对象

function createElement(type: string, props, ...children) {
  props = props || {};
  const vnode = new VNode();
  vnode.type = type;
  vnode.handles = Object.keys(props)
    .filter(value => value.startsWith('on'))
    .map(value => {
      return {
        propName: value,
        propValue: props[value]
      }
    });

  vnode.props = Object.keys(props)
    .filter(value => !value.startsWith('on') && value !== 'key')
    .map(value => {
      return {
        propName: value,
        propValue: props[value]
      }
    });
  vnode.key = props['key'];
  vnode.children = children;
  return vnode;
}

复制代码

虚拟DOM挂载到HTML DOM,根据根节点的ID,使用document.createElement将虚拟DOM转成HTML DOM,并挂载到rootElement

function mount(rootElement, vNode: VNode) {
  const el = createVNode(vNode);
  if (el != null) {
    rootElement.appendChild(el);
  }
}
复制代码

根据虚拟DOM生成HTML DOM

function createVNode(vNode: VNode): HTMLElement | Text {
  if (vNode == null) {
    return null;
  }
  if (vNode instanceof VNode) {
    let el: HTMLElement;
    el = document.createElement(vNode.tagName);
    vNode.props.forEach(value => {
      el.setAttribute(value.propName, value.propValue)
    });
    vNode.events.forEach(value => {
      el.addEventListener(value.propName.replace(/^on/, ''), value.propValue);
    });
    vNode.children.forEach(value => {
      const subEl = createVNode(value);
      if (subEl != null) {
        el.appendChild(subEl);
      }
    });
    vNode.el = el;
    return el;
  } else {
    return document.createTextNode(String(vNode));
  }
}
复制代码

定义一个所有组件的抽象父组件,实现组件共有的基础功能


export abstract class Component {
    
  readonly el: HTMLElement;

  constructor(props: ComponentProps) {
    if (props) {
      Object.assign(this, props);
    }
  }

  protected mount() {
    this.vNode = this.render();
    const node = this.dom.createElement(this.vNode, this.update.bind(this));
    this.appendToEl(node);
  }

  appendToEl(node: Node) {
    this.el && node && this.dom.appendChild(this.el, node);
  }
}
复制代码

最后再用定义一个挂载方法,创建组件并挂载到真实DOM,并且按顺序执行生命周期即可

export function renderDOM(componentType: Function, props, el?: HTMLElement) {
    const component = new componentType({...props, el});
    component.beforeMount && component.beforeMount();
    component.mount();
    component.mounted && component.mounted();
    return component;
  }

复制代码

Virtual DOM到render的过程基本就完成了,接下来我们定义一个组件,测试调用一下,看看结果


export class TestComponent extends Component {
  buttonText = 'buttonText';
  clickCount = 1;
  
  constructor(props: ComponentProps) {
    super(props);
  }
  render() {
    return (<div>
      <span >hello world</span>
      <button onclick={() => {console.log('Hello World!', ++this.clickCount)}}>{this.buttonText}</button>
    </div>);
  }
}


复制代码

{% asset_img mvvm.png 运行结果 %}

分类:
前端
标签: