Virtual DOM的历史和未来

更新日期: 2022-05-07阅读: 861标签: dom

Virtual dom最初是由react的作者开创的,目的是使声明式UI的渲染速度更快。为了理解为什么声明式UI最初如此缓慢,我们首先需要了解过去是如何做声明式UI的。

声明式用户界面

编写声明式UI的传统方法是更改元素的innerhtml属性。例如,如果我想向<div>UI添加一个元素到,我将写如下:

document.body.innerHTML = '<div>Hello World!</div>';
// <body> now has a <div>Hello World!</div> child.

我们可以认识到innerHTML允许我们以声明地方式定义UI,但它的效率不高。

效率低下源于每次更改用户界面时的解析、破坏和重建innerHTML,都需要遵循四个步骤:

  1. 解析 innerHTML 字符串到DOM节点树中。
  2. 移除所有内容 <body> 元素。
  3. 将DOM节点树插入 <body> 元素。
  4. 执行布局计算和重绘屏幕。

这个过程在计算上非常昂贵,并且可能导致渲染速度显著降低。

命令式用户界面

那么,这个问题是如何解决的呢?那就是选择使用DOM, 这种方法要比innerHTML方法快3倍。

const div = document.createElement('div');
div.textContent = 'Hello World!';
document.body.appendChild(div);

然而,我们可以认识到,手动编写这个可能很麻烦,特别是当UI中有很多交互时,因为我们需要命令式地指定每个步骤。以声明的方式编写UI要优雅得多。

不过,React作者创建了VirtualDOM,允许我们以一种比innerHTML更快的呈现方式编写UI,而且是声明式的。

理解VirtualDOM

为了最好地了解VirtualDOM是如何工作的,让我们概述一下流程,然后构建一个示例。

VirtualDOM是一种呈现UI的方法。该方法利用模仿DOM树的JavaScript对象树(“虚拟”节点)。

// <div>Hello World!</div>
const div = document.createElement('div');
div.style = 'color: red';
div.textContent = 'Hello World!';

以上<div> 被模仿为以下JavaScript对象中的虚拟节点:

const divVNode = {
  type: 'div',
  props: {
    style: 'color: red'
  }
  children: ['Hello World!']
};

我们可以注意到虚拟节点有三个属性:

  • tag:将元素的标记名称存储为字符串。
  • props:将元素的属性和属性存储为对象。
  • children:将元素的虚拟节点子级存储为数组。

使用虚拟节点,我们可以对当前的UI进行建模,以及当我们更新UI时希望它改变成什么。

假设我想将<div>中的文本从 "Hello World!" 更改成 "Hello Universe!"。可以使用DOM进行强制修改:

// <div>Hello World!</div>
const div = document.createElement('div');
div.style = 'color: red';
div.textContent = 'Hello World!';

// Change from "Hello World!" to "Hello Universe!"
div.textContent = 'Hello Universe!';

但是使用VirtualDOM,我可以指定当前UI的外观(旧虚拟节点)和我希望它的外观(新虚拟节点)。

const oldVNode = {
  type: 'div',
  props: {
    style: 'color: red'
  }
  children: ['Hello World!']
};

const newVNode = {
  type: 'div',
  props: {
    style: 'color: red'
  }
  children: ['Hello Universe!']
};

然而要让Virtual DOM真正将更改应用到UI,还需要计算旧虚拟节点和新虚拟节点之间的差异。

{
  type: 'div',
  props: {
    style: 'color: red'
  }
-  children: ['Hello World!']
+  children: ['Hello Universe!']
};

当我们知道了二者之间的差别,就可以通过Virtual DOM改变UI。

div.replaceChild(newChild, oldChild);

Virtual DOM只是进行了必要的修改,并不是替换了整个UI。

构建自己的Virtual DOM

在本文中,我们将模仿Million.js的 Virtual DOM api。我们的API将包含三个主要功能:m, createElement, and patch。

m (tag, props, children)

m 函数是创建虚拟节点的辅助函数。虚拟节点包含三个属性:

  • tag:将虚拟节点的名称标记为字符串;
  • props:作为对象的节点的属性/属性;
  • children:虚拟节点的子节点作为数组。

m帮助程序函数的示例实现如下:

const m = (tag, props = {}, children = []) => ({
  tag,
  props,
  children,
});

这样创建虚拟节点就简单多了。

m('div', { style: 'color: red' }, ['Hello World!']);

#createElement(vnode)

该createElement函数将虚拟节点转换为真实的DOM元素。这很重要,因为我们将在patch函数中使用它。

实现如下:

  1. 如果虚拟节点是文本,则返回文本节点;
  2. tag使用虚拟节点的属性创建一个新的DOM节点;
  3. 遍历虚拟节点props 并将它们添加到DOM节点。
  4. 遍历children,在每个子级上递归调用createElement并将其添加到DOM节点。
const createElement = (vnode) => {
  if (typeof vnode === 'string') {
    return document.createTextNode(vnode);
  }
  const el = document.createElement(vnode.tag);
  for (const prop in vnode.props) {
    el[prop] = vnode.props[prop];
  }
  for (const child of vnode.children) {
    el.appendChild(createElement(child));
  }
  return el;
};

这样就可以轻松地将虚拟节点转变成DOM节点。

// <div style="color: red">Hello World!</div>
createElement(
  m('div', { style: 'color: red' }, ['Hello World!'])
);

#patch(el, newVNode, oldVNode)

该patch函数采用现有的DOM节点、旧的虚拟节点和新的虚拟节点。

实现如下:

  1. 计算两个虚拟节点之间的差异;
  2. 如果虚拟节点是字符串,则将DOM节点的文本内容替换为新节点;
  3. 如果虚拟节点是对象,且tag、props、 children不同,则更新节点。
const patch = (el, newVNode, oldVNode) => {
  if (!newVNode && newVNode !== '') return el.remove();
  if (
    typeof oldVNode === 'string' ||
    typeof newVNode === 'string'
  ) {
    if (oldVNode !== newVNode) {
      return el.replaceWith(createElement(newVNode));
    }
  } else {
    if (oldVNode.tag !== newVNode.tag) {
      return el.replaceWith(createElement(newVNode));
    }

    // patch props
    for (const prop in {
      ...oldVNode.props,
      ...newVNode.props,
    }) {
      if (newVNode.props[prop] === undefined) {
        delete el[prop];
      } else if (
        oldVNode.props[prop] === undefined ||
        oldVNode.props[prop] !== newVNode.props[prop]
      ) {
        el[prop] = newVNode.props[prop];
      }
    }

    // patch children
    for (let i = oldVNode.children.length - 1; i >= 0; --i) {
      patch(
        el.childNodes[i],
        newVNode.children[i],
        oldVNode.children[i]
      );
    }

    for (
      let i = oldVNode.children.length;
      i < newVNode.children.length;
      i++
    ) {
      el.appendChild(createElement(newVNode.children[i]));
    }
  }
};

这样就可以使用patch功能更新UI了。

const oldVNode = m('div', { style: 'color: red' }, [
  'Hello World!',
]);
const newVNode = m('div', { style: 'color: red' }, [
  'Hello Universe!',
]);
const el = createElement(oldVNode);

// <div style="color: red">Hello World!</div>
patch(el, oldVNode, newVNode);
// <div style="color: red">Hello Universe!</div>

Virtual DOM是纯开销

当前,Virtual DOM实现在计算新旧虚拟节点之间的差异时会产生计算成本。

即使使用非常有效的差分算法 (如list-diff2),当虚拟节点树大于虚拟节点的两位数时,差异成本也会变得显著。

树区分算法是出了名的慢。时间复杂度可以从O(n)转O(n ^ 3)取决于虚拟节点树的复杂性。这与DOM操纵相去甚远,后者是O(1)。

Virtual DOM的未来

编译器是新框架” -- 汤姆·戴尔

Ember的创建者汤姆是最早倡导为JavaScript UI库使用编译器开源狂热者之一。

现在,我们知道汤姆的赌注是正确的。JavaScript生态系统见证了Solid、Svelte等“已编译”库的兴起,它们放弃了Virtual DOM。这些库使用编译器预渲染,并在使用时生成代码来跳过不必要的渲染。

另一方面,Virtual DOM落后于这一趋势。当前的虚拟DOM库本质上与“按需” 编译器不兼容。因此,Virtual DOM的渲染速度通常是比现代“No Virtual DOM” UI库慢几个数量级。

如果我们希望Virtual DOM在未来的渲染速度上具有竞争力,那就需要重新设计Virtual DOM以允许编译器增强。

来自:https://developer.51cto.com/article/708217.html

链接: https://www.fly63.com/article/detial/11460

全面理解虚拟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 属性

点击更多...

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