当前位置: 首页 问答 > 正文

Virtual DOM的历史和未来

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

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


【资料图】

声明式用户界面

编写声明式UI的传统方法是更改元素的innerHTML属性。例如,如果我想向

UI添加一个元素到,我将写如下:

document.body.innerHTML = "
Hello World!
";// now has a
Hello World!
child.

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

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

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

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

命令式用户界面

那么,这个问题是如何解决的呢?那就是选择使用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对象树(“虚拟”节点)。

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

以上

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

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

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

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

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

假设我想将

中的文本从 “Hello World!” 更改成 “Hello Universe!”。可以使用DOM进行强制修改:

// 
Hello World!
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函数中使用它。

实现如下:

如果虚拟节点是文本,则返回文本节点;tag使用虚拟节点的属性创建一个新的DOM节点;遍历虚拟节点props 并将它们添加到DOM节点。遍历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节点。

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

#patch(el, newVNode, oldVNode)

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

实现如下:

计算两个虚拟节点之间的差异;如果虚拟节点是字符串,则将DOM节点的文本内容替换为新节点;如果虚拟节点是对象,且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);// 
Hello World!
patch(el, oldVNode, newVNode);//
Hello Universe!

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以允许编译器增强。

分类:

2023-05-07 19:22:13

免责声明

本站所载的各种信息和数据等仅供参考,本站不承担由此引起的法律责任; 如有侵权,请及时联系本站,我们会及时处理,但不承担相应的法律责任。 案例、资讯、问答内容均由其他用户注册上传分享, 丝路整形网不对其版权性、真实性、完整性、及时性等 作出任何保证, 本站内容不作为诊断及整形依据,具体以面诊为主。

以上资料来自互联网,如果侵犯版权请 联系我们 删除

本站内容仅供参考,不作为诊断及医疗依据 豫ICP备2022016423号-2

美丽网,随时随地为您提供专业整形美 容咨询预约服务