从零开始一步一步写一个简单的Virtual DOM实现

原文地址html

Github 系列文章地址前端

在阅读此文以前,你要明确两个概念。这篇文章不会长篇大论地跟你介绍React中的源代码实现或者其余一些相似的Virtual DOM的实现。它们过于复杂了,其实一个Virtual DOM的实现只要不超过50行代码便可。好了,下面便是你要了解的两个概念:node

  • Virtual DOM是真正DOM的一种表现git

  • 当Virtual DOM Tree发生变化时,算法会自动比较新旧两棵树,找出其中的差别,而且只对真实的DOM树作最小化改变github

本文便是按部就班地阐述这两个概念。web

DOM树的表示

首先,咱们须要将DOM树存放于内存中,最简单的,咱们能够将DOM树表示为一个JavaScript的Object对象,假设咱们有一棵这样的DOM树:算法

<ul class=”list”>

  <li>item 1</li>

  <li>item 2</li>

</ul>

而该DOM树对应的JS对象以下:app

{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [

  { type: ‘li’, props: {}, children: [‘item 1’] },

  { type: ‘li’, props: {}, children: [‘item 2’] }

] }

两相比较,咱们能够发现,咱们将DOM中的任一元素表示为:frontend

{ type: ‘…’, props: { … }, children: [ … ] }

而DOM中的纯文本节点会被表示为普通的JavaScript中的字符串。不过这仍是一个简单的DOM树,若是是一个较大型的树,咱们就须要一个辅助函数来构造结构:dom

function h(type, props, …children) {

  return { type, props, children };

}

基于这个辅助函数,咱们能够把上面那个简单的DOM树用以下方式表示:

h(‘ul’, { ‘class’: ‘list’ },

  h(‘li’, {}, ‘item 1’),

  h(‘li’, {}, ‘item 2’),

);

看上去是否是清晰了不少呀?这种结构和转化方程看上去很像大名鼎鼎的JSX啊,以Babel解释器为例,它会把上面说起的DOM树转化为以下结构:

React.createElement(‘ul’, { className: ‘list’ },

  React.createElement(‘li’, {}, ‘item 1’),

  React.createElement(‘li’, {}, ‘item 2’),

);

总结而言,咱们能够按照以下JSX的语法编写DOM树:

/** @jsx h */

const a = (
 <ul className=”list”>
   <li>item 1</li>
   <li>item 2</li>
 </ul>
);

而Babel会将JSX转化为以下格式:

const a = (

  h(‘ul’, { className: ‘list’ },

    h(‘li’, {}, ‘item 1’),

    h(‘li’, {}, ‘item 2’),

  );

);

h函数执行以后,整个对象会转化为基本的JS对象:

const a = (

  { type: ‘ul’, props: { className: ‘list’ }, children: [

    { type: ‘li’, props: {}, children: [‘item 1’] },

    { type: ‘li’, props: {}, children: [‘item 2’] }

  ] }

);

本部分在JSFiddle上的地址是:这里

完整的Babel可编译的源代码为:

/** @jsx h */

function h(type, props, ...children) {
  return { type, props, children };
}

const a = (
  <ul class="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

console.log(a);

Applying our DOM Representation

如今已经能够将DOM树用纯粹的JS对象进行表示,那么下一步咱们就是须要将自定义的虚拟DOM结构体转化到真实的DOM树中。首先阐述下下文会用到的一些术语表达式:

  • 全部真实DOM节点,譬如元素与文本节点,都以$开头描述,譬如$parent就是一个真实的DOM元素

  • 全部的Virtual DOM将会用变量node描述

  • 跟React中相似,只能够有一个根节点存在,其余全部的节点都会包含在该根节点内

那么下面咱们就要来编写函数createElement,负责将输入的虚拟DOM转化为一个真实的DOM,这里暂时不考虑propschildren,那么最简单的函数实现是:

function createElement(node) {

  if (typeof node === ‘string’) {

    return document.createTextNode(node);

  }

  return document.createElement(node.type);

}

由于咱们须要考虑到同时处理文本节点与元素节点的须要,因此进行了一个简单的分支判断,这是最简单的实现,下面咱们要考虑怎么对子元素进行渲染。每一个节点的子节点多是个文本节点,也多是元素节点,换言之,咱们要沿着虚拟节点的树一直从根节点处理到叶子节点,差很少就是要用迭代的思想进行构造,而后用appendChild()函数将产生的子节点挂载到父节点上。最终实现的函数差很少这个样子:

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

本部分的JSFiddle调试在这里,完整的JSX代码为:

/** @jsx h */

function h(type, props, ...children) {
  return { type, props, children };
}

function createElement(node) {
  if (typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

const a = (
  <ul class="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

const $root = document.getElementById('root');
$root.appendChild(createElement(a));

Handling Changes

如今咱们已经成功地将Virtual DOM转化为了真实的DOM节点,下面就要考虑下Virtual DOM核心的算法,即差别检测。咱们先来写一个最简单的Virtual DOM比较算法,保证只会对真实的DOM节点作最小改动。首先咱们仍是来看下可能有几种发生改变的状况:

(1)添加了部分节点,须要调用appendChild()函数进行添加

(2)移除了部分节点,须要调用removeChild()函数进行删除

(3)部分节点变成了其余节点,须要调用replaceChild()进行替换

(4)某个节点的标签发生了变化,或者被挂载到了其余地方

对于以上这几种状况,咱们统一使用updateElement()函数对DOM树进行更新,该函数会传入三个参数:

  • $parent 表明Virtual DOM挂载在DOM树上的根节点

  • newNode 新的Virtual DOM

  • oldNode 老的Virtual DOM

初始化时候没有老的Virtual DOM状况

若是oldNode直接为空,那么咱们只要简单地建立新的节点便可:

function updateElement($parent, newNode, oldNode) {

  if (!oldNode) {

    $parent.appendChild(

      createElement(newNode)

    );

  }

}

整个newNode被置空,即从DOM树中移除了

若是newNode为空,即整个Virtual DOM树上没有挂载任何节点,那么咱们须要将VirtualDOM对应的节点树从DOM中移除,最简单的方法就是调用$parent.removeChild()函数,而后传入整个真实DOM元素的引用。不过实际上,咱们在内存里只有Virtual DOM而没有真实DOM的引用。那咱们换个思路,若是咱们知道Virtual DOM对应处于真实DOM中的第几个子节点,就能够根据下标删除了,大概是这个样子:

function updateElement($parent, newNode, oldNode, index = 0) {

  if (!oldNode) {

    $parent.appendChild(

      createElement(newNode)

    );

  } else if (!newNode) {

    $parent.removeChild(

      $parent.childNodes[index]

    );

  }

}

节点发生了变化

首先咱们须要写一个简单的比较方程比较两个虚拟节点是否发生了变化,相似于建立元素的函数,咱们一样须要考虑文本节点与元素节点:

function changed(node1, node2) {

  return typeof node1 !== typeof node2 ||

         typeof node1 === ‘string’ && node1 !== node2 ||

         node1.type !== node2.type

}

有了这个比较函数和当前Virtual DOM映射的真实DOM在父节点中的序号,咱们就能够将更新函数完善成以下介个样子:

function updateElement($parent, newNode, oldNode, index = 0) {

  if (!oldNode) {

    $parent.appendChild(

      createElement(newNode)

    );

  } else if (!newNode) {

    $parent.removeChild(

      $parent.childNodes[index]

    );

  } else if (changed(newNode, oldNode)) {

    $parent.replaceChild(

      createElement(newNode),

      $parent.childNodes[index]

    );

  }

}

注意,上面比较函数中,在节点发生变化的状况,只考虑了Virtual DOM中根节点发生了变化的状况,比较的方式也是直接比较内存地址,是不是新对象,从这一点也能够看出Immutable的重要意义。

Diff children

上面说起的算法里并无对子节点进行检查,而在实际状况下,咱们不只要检查根节点,还要递归检查子节点是否发生了变化,即递归找到变化的那个节点,在编写代码以前,咱们脑中要清楚如下几点:

  • 只有对元素节点才须要进行子节点对比,文本节点是没有子节点的

  • 递归过程当中,会不断传入当前节点做为子节点对比的根节点处理

  • 上面说的index,这里就能够看出了,只是子节点在父节点中的序号

function updateElement($parent, newNode, oldNode, index = 0) {

  if (!oldNode) {

    $parent.appendChild(

      createElement(newNode)

    );

  } else if (!newNode) {

    $parent.removeChild(

      $parent.childNodes[index]

    );

  } else if (changed(newNode, oldNode)) {

    $parent.replaceChild(

      createElement(newNode),

      $parent.childNodes[index]

    );

  } else if (newNode.type) {

    const newLength = newNode.children.length;

    const oldLength = oldNode.children.length;

    for (let i = 0; i < newLength || i < oldLength; i++) {

      updateElement(

        $parent.childNodes[index],

        newNode.children[i],

        oldNode.children[i],

        i

      );

    }

  }

}

最终代码的调试地址是JSFiddle,其效果为:

到这里咱们就完成了一个最简单的Virtual DOM算法,不过其与真正可以投入实战的Virtual DOM算法仍是有很大距离,进一步阅读推荐:

相关文章
相关标签/搜索