撸一个简易Virtual DOM

栏目: IT技术 · 发布时间: 4年前

内容简介:上一篇我们讲了一下首先我们要明白什么是这里我们不使用

上一篇我们讲了一下 Vue虚拟DOM ,从创建到更新整个流程。今天带大家撸一个简易的 虚拟DOM ,本文的大部分借鉴 Vue 源码

准备

首先我们要明白什么是 虚拟DOM ,为什么要用到它, 虚拟DOM 是使用 javascript 对象来描述 DOM 树,一切的更新修改都是在更改这个对象,然后反应到真实的 DOM 下。要说为什么要用到它,肯定是因为性能好,因为直接操作 DOM 的代价是很大的,比如,一次操作中有 10 次更新 DOM 的动作, 虚拟DOM 不会立即操作 DOM ,而是将这 10 次更新的 diff 内容保存到本地一个 js 对象中,最终将这个 js 对象一次性 反应到 DOM 树上,再进行后续操作,避免大量无谓的计算。

实现

使用js对象模拟DOM树

这里我们不使用 Vue 的那种根据 template 生成 虚拟DOM 的方法,那样太复杂了,这里只讲简单的方法,我们直接使用 js 对象来描述 DOM

比如如下 的 html 代码

<div id="app">
  <h1 data-title="header">Virtual Dom</h1>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul>
</div>

使用 js 对象表示

function vnode(tag, data, children) {
  return {
    tag,
    data,
    children,
  };
}
function createElement(vNode) {
  let el;
  if (vNode.tag === 'textNode') {
    el = document.createTextNode(vNode.children[0]);
  } else {
    el = document.createElement(vNode.tag);
    for (const key in vNode.data) {
      if (vNode.data.hasOwnProperty(key)) {
        el.setAttribute(key, vNode.data[key]);
      }
    }
    if (Array.isArray(vNode.children) && vNode.children.length > 0) {
      vNode.children.forEach(val => {
        el.appendChild(createElement(val));
      });
    }
  }
  vNode.$el = el;
  return el;
}
  • tag :表示当前元素的标签名,后面会看到有 textNode 的标签名,实际上没有,在这里特指文本节点
  • data :表示当前元素上的 attribute
  • children :表示当前元素的子元素

  • createElement :使用该方法渲染到页面上

生成 vNode

const vNode1 = vnode('div', { id: 'app' }, [
  vnode('h1', { 'data-title': 'header' }, [
    vnode('textNode', {}, ['Virtual Dom']),
  ]),
  vnode('ul', { class: 'ul1' }, [
    vnode('li', {}, [vnode('textNode', {}, ['1'])]),
    vnode('li', {}, [vnode('textNode', {}, ['2'])]),
    vnode('li', {}, [vnode('textNode', {}, ['3'])]),
  ]),
]);

撸一个简易Virtual DOM

反应到页面上,把结果放到 body

document.body.appendChild(createElement(vNode1))

效果

撸一个简易Virtual DOM

更新Virtual Dom

更新之前我们把之前的旧的 虚拟DOM li 列表上的 key 值加上

oldVNode

const vNode1 = vnode('div', { id: 'app' }, [
  vnode('h1', { 'data-title': 'header' }, [
    vnode('textNode', {}, ['Virtual Dom']),
  ]),
  vnode('ul', { class: 'ul1' }, [
    vnode('li', {key: 1}, [vnode('textNode', {}, ['1'])]),
    vnode('li', {key: 2}, [vnode('textNode', {}, ['2'])]),
    vnode('li', {key: 3}, [vnode('textNode', {}, ['3'])]),
  ]),
]);

newVNode

const vNode2 = vnode('div', { id: 'app' }, [
  vnode('h4', { id: 'header'}, [
    vnode('textNode', {}, ['元素标签改变了']),
  ]),
  vnode('ul', { class: 'ul2' }, [
    vnode('li', {key: 5}, [vnode('textNode', {}, ['5'])]),
    vnode('li', {key: 2}, [vnode('textNode', {}, ['2'])]),
    vnode('li', {key: 1}, [vnode('textNode', {}, ['1'])]),
    vnode('li', {key: 4}, [vnode('textNode', {}, ['4'])]),
  ]),
]);

你会发现为把 h1 标签改成了 h4 ,文本那样也改变了,在 ul 列表里,把他们的顺序改变了,并插入删除了部分元素。当然你也可以定制更复杂的结构

开始 diff ,调用 patchVnode 方法

patchVnode(vNode1, vNode2);
function patchVnode(oldVnode, vnode) {
  // 1. 文本节点都一样
  if (oldVnode.tag === 'textNode' || vnode.tag === 'textNode') {
    if (oldVnode.children[0] !== vnode.children[0]) {
      oldVnode.$el.textContent = vnode.children[0];
    }
    return;
  }

  // 2. data是否被改变
  if (dataChanged(oldVnode.data, vnode.data)) {
    const oldData = oldVnode.data;
    const newData = vnode.data;
    const oldDataKeys = Object.keys(oldData);
    const newDataKeys = Object.keys(newData);
    if (oldDataKeys.length === 0) {
      for (let i = 0; i < oldDataKeys.length; i++) {
        oldVnode.$el.removeAttribute(oldData[i]);
      }
    } else {
      const filterKeys = new Set([...oldDataKeys, ...newDataKeys]);
      for (let key of filterKeys) {
        if (isUndef(newData[key])) {
          oldVnode.$el.removeAttribute(oldData[key]);
        } else if (newData[key] !== oldData[key]) {
          oldVnode.$el.setAttribute(key, newData[key]);
        }
      }
    }
  }
  var oldCh = oldVnode.children;
  var ch = vnode.children;
  // 如果新旧子节点仍然存在,则继续diff它的子节点
  if (oldCh.length || ch.length) {
    updateChildren(oldVnode.$el, oldCh, ch);
  }
}

上面先对最外层进行比较,

  1. 如果是文本,说明已经是当前最后的一个元素了,后面不需要继续执行,发现不一样则改变他们
  2. 判断 data 是否变更,如果变更则进行插入或者删除修改操作
  3. 继续找它的子元素是否操作,如果存在进行子元素的 diff ,使用 updateChildren

updateChildren

function updateChildren(parentElm, oldCh, newCh) {
  if (oldCh) var oldStartIdx = 0;
  var newStartIdx = 0;
  var oldEndIdx = oldCh.length - 1;
  var oldStartVnode = oldCh[0];
  var oldEndVnode = oldCh[oldEndIdx];
  var newEndIdx = newCh.length - 1;
  var newStartVnode = newCh[0];
  var newEndVnode = newCh[newEndIdx];
  var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx];
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      patchVnode(oldStartVnode, newEndVnode);
      parentElm.insertBefore(oldStartVnode.$el, oldEndVnode.$el.nextSibling);
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      patchVnode(oldEndVnode, newStartVnode);
      parentElm.insertBefore(oldEndVnode.$el, oldStartVnode.$el);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      if (isUndef(oldKeyToIdx)) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      }
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
      if (isUndef(idxInOld)) {
        parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.$el);
      } else {
        vnodeToMove = oldCh[idxInOld];
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode);
          oldCh[idxInOld] = undefined;
          parentElm.insertBefore(vnodeToMove.$el, oldStartVnode.$el);
        } else {
          parentElm.insertBefore(
            createElement(newStartVnode),
            oldStartVnode.$el,
          );
        }
      }

      newStartVnode = newCh[++newStartIdx];
    }
  }
  if (oldStartIdx > oldEndIdx) {
    addVnodes(parentElm, newCh, newStartIdx, newEndIdx);
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
  }
}

上面代码基本参照 Vue 源码的方法,如果你阅读过我之前发的文章,肯定有印象,代码内就不多作解释了,上一篇文章的注释说得很明白,感兴趣可以去翻阅一下。

看一下结果,一切都正常

撸一个简易Virtual DOM

相关源码已经放在 GitHub: https://github.com/wclimb/simple-virtual-dom


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

集体智慧编程

集体智慧编程

TOBY SEGARAN / 莫映、王开福 / 电子工业出版社 / 2009-1 / 59.80元

本书以机器学习与计算统计为主题背景,专门讲述如何挖掘和分析Web上的数据和资源,如何分析用户体验、市场营销、个人品味等诸多信息,并得出有用的结论,通过复杂的算法来从Web网站获取、收集并分析用户的数据和反馈信息,以便创造新的用户价值和商业价值。全书内容翔实,包括协作过滤技术(实现关联产品推荐功能)、集群数据分析(在大规模数据集中发掘相似的数据子集)、搜索引擎核心技术(爬虫、索引、查询引擎、Page......一起来看看 《集体智慧编程》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具