如何快速实现一个虚拟 DOM 系统

更新日期: 2021-07-13阅读: 1.5k标签: dom

虚拟 dom 是目前主流前端框架技术核心之一,本文阐述如何实现一个简单的虚拟 DOM 系统。

为什么需要虚拟 DOM?

虚拟 DOM 就是一棵由虚拟节点组成的树,这棵树展现了真实 DOM 的结构。这些虚拟节点是轻量的、无状态的,一般是字符串或者仅仅包含必要字段的 JavaScript 对象。虚拟节点可以被组装成节点树树,通过特定的 "diff" 算法对两个节点树进行对比,找出其中细微的变更点,然后更新到真实 DOM 上去。

之所以会有虚拟 DOM,是因为直接更新真实 DOM 非常昂贵。通过新比对虚拟 DOM,然后只将变化的部分更新到真实 DOM 上去。这么做都是操作纯 JavaScript 对象,尽量避免了直接操作 DOM,读写成本低很多。

如何实现虚拟 DOM

在开始之前,我们需要明确一个虚拟 DOM 系统应该包含哪些必要的组成部分?

首先,我们要定义清楚什么是虚拟节点。一个虚拟节点可以是一个普通 JavaScript 对象,也可以是一个字符串。

我们定义一个函数 createNode 来创建虚拟节点。一个虚拟节点至少包含三个信息:

  • tag:保存虚拟节点的标签名,字符串
  • props:保存虚拟节点的 properties/attributes,普通对象
  • children:保存虚拟节点的子节点,数组

下面的代码是 createNode 实现样例:

const createNode = (tag, props, children) => ({
  tag,
  props,
  children,
});

我们通过 createNode 可以轻松的创建虚拟节点:

createNode('div', { id: 'app' }, ['Hello World']);

// 返回如下:
{
  tag: 'div',
  props: { id: 'app' },
  children: ['Hello World'],
}

现在,我们需要定义一个 createElement 函数来根据虚拟节点创建真实的 DOM 元素。

在 createElement 中,我们需要创建一个新的 DOM 元素,然后遍历虚拟节点的 props 属性,将其中的属性添加到 DOM 元素上去,之后再遍历 children 属性。如下代码是一个实现样例:

const createElement = vnode => {
  if (typof vnode === 'string') {
    return document.createTextNode(vnode); // 如果是字符串就直接返回文本元素
  }
  const el = document.createElement(vnode.tag);
  if (vnode.props) {
    Object.entries(vnode.props).forEach(([name, value]) => {
      el[name] = value;
    });
  }
  if (vnode.children) {
    vnode.children.forEach(child => {
      el.appendChild(createElement(child));
    });
  }
  return el;
}

现在,我们可以通过 createElement 将虚拟节点转变成真实 DOM 了。

createElement(createNode("div", { id: "app" }, ["Hello World"]));

// 输出: <div id="app">Hello World</div>

我们再来定义一个 diff 函数来实现 'diff' 算法。这个 diff 函数接收三个参数,一个是已经存在的 DOM 元素,一个是旧的虚拟节点,一个是新的虚拟节点。在这个函数中,我们将对比两个虚拟节点,在需要的时候,将旧的元素替换掉。

const diff = (el, oldVNode, newVNode) => {
  const replace = () => el.replaceWith(createElement(newVNode));
  if (!newVNode) return el.remove();
  if (!oldVNode) return el.appendChild(createElement(newVNode));
  // 处理纯文本的情况
  if (typeof oldVNode === 'string' || typeof newVNode === 'string') {
    if (oldVNode !== newVNode) return replace();
  } else {
    // 对比标签名
    if (oldVNode.tag !== newVNode.tag) return replace();
    // 对比 props
    if (!oldVNode.props?.some((prop) => oldVNode.props?.[prop] === newVNode.props?.[prop])) return replace();
    // 对比 children
    [...el.childNodes].forEach((child, i) => {
      diff(child, oldVNode.children?.[i], newVNode.children?.[i]);
    });
  }
}

在这个函数中,我们先处理纯文本的情况,如果新旧两个字符串不相同,则直接替换。之后,我们就可以假定两个虚拟节点都是对象了。我们先对比两个节点的标签名是否相同,不同则直接替换。之后对比两个节点的 props 是否相同,不同也直接替换。最后我们在递归的使用 diff 函数对比两个虚拟节点的 children。

至此,我们就实现了一个简版虚拟 DOM 系统所必须的所有功能。下面是使用样例:

const oldVNode = createNode("div", { id: "app" }, ["Hello World"]);
const newVNode = createNode("div", { id: "app" }, ["Goodbye World"]);
const el = createElement(oldVNode);
// <div id="app">Hello World</div>

diff(el, oldVNode, newVNode);
// el will become: <div id="app">Goodbye World</div>
文中的实现侧重于展示虚拟 DOM 的实现原理,在实现代码中并未考虑性能等其他因素。

原文来自:https://everfind.github.io/posts/2021/07/06/virtual-dom.html


链接: https://fly63.com/article/detial/10528

全面理解虚拟DOM,实现虚拟DOM

DOM是很慢的,其元素非常庞大,页面的性能问题鲜有由JS引起的,大部分都是由DOM操作引起的。虚拟的DOM的核心思想是:对复杂的文档DOM结构,提供一种方便的工具,进行最小化地DOM操作。

HTML文档解析和DOM树的构建

浏览器解析HTML文档生成DOM树的过程,以下是一段HTML代码,以此为例来分析解析HTML文档的原理.解析HTML文档构建DOM树的理解过程可分为两个主要模块构成,即标签解析、DOM树构建

原生js获取DOM对象的几种方法

javascript获取DOM对象的多种方法:通过id获取 、通过class获取、通过标签名获取、通过name属性获取、通过querySelector获取、通过querySelectorAll获取等

js实现DOM遍历_遍历dom树节点方法

遍历DOM节点常用一般用节点的 childNodes, firstChild, lastChild, nodeType, nodeName, nodeValue属性。在获取节点nodeValue时要注意,元素节点的子文本节点的nodeValue才是元素节点中文本的内容。

如何编写自己的虚拟DOM

要构建自己的虚拟DOM,需要知道两件事。你甚至不需要深入 React 的源代码或者深入任何其他虚拟DOM实现的源代码,因为它们是如此庞大和复杂——但实际上,虚拟DOM的主要部分只需不到50行代码。

归纳DOM事件中各种阻止方法

事件冒泡: 即事件开始时由最具体的元素(文档中嵌套层数最深的那个点)接收,事件捕获:不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件.与此同时,我们还需要了解dom事件绑定处理的几种方式:

关于DOM操作是异步的还是同步的相关理解

先列出我的理解,然后再从具体的例子中说明:DOM操作本身应该是同步的(当然,我说的是单纯的DOM操作,不考虑ajax请求后渲染等);DOM操作之后导致的渲染等是异步的(在DOM操作简单的情况下,是难以察觉的)

JavaScript DOM事件模型

早期由于浏览器厂商对于浏览器市场的争夺,各家浏览器厂商对同一功能的JavaScript的实现都不进相同,本节内容介绍JavaScript的DOM事件模型及事件处理程序的分类。

vuejs2.0如何获取dom元素自定义属性值

设置定义属性值 :data-value=.., 2.直接获取 3.通过this.$refs.***获取 1.目标DOM定义ref值: 2.通过 【this.$refs.***.属性名】 获取相关属性的值: this.$refs.*** 获取到对应的元素 ...

整理常见 DOM 操作

框架用多了,你还记得那些操作 DOM 的纯 JS 语法吗?看看这篇文章,来回顾一下~ 操作 className,addClass给元素增加 class,使用 classList 属性

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!