Virtual DOM最初是史和由React的作者开创的,目的史和是使声明式UI的渲染速度更快。为了理解为什么声明式UI最初如此缓慢,史和我们首先需要了解过去是史和如何做声明式UI的。 编写声明式UI的史和传统方法是更改元素的innerHTML属性。例如,史和如果我想向<div>UI添加一个元素到,史和我将写如下: document.body.innerHTML = // now has a 我们可以认识到innerHTML允许我们以声明地方式定义UI,史和但它的史和效率不高。 效率低下源于每次更改用户界面时的史和解析、破坏和重建innerHTML,史和都需要遵循四个步骤: 这个过程在计算上非常昂贵,并且可能导致渲染速度显著降低。 那么,这个问题是如何解决的呢?那就是云服务器提供商选择使用DOM, 这种方法要比innerHTML方法快3倍。 const div = document.createElement(div); div.textContent = Hello World!; document.body.appendChild(div); 然而,我们可以认识到,手动编写这个可能很麻烦,特别是当UI中有很多交互时,因为我们需要命令式地指定每个步骤。以声明的方式编写UI要优雅得多。 不过,React作者创建了VirtualDOM,允许我们以一种比innerHTML更快的呈现方式编写UI,而且是声明式的。 为了最好地了解VirtualDOM是如何工作的,让我们概述一下流程,然后构建一个示例。 VirtualDOM是一种呈现UI的方法。该方法利用模仿DOM树的JavaScript对象树(“虚拟”节点)。 // 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!] }; 我们可以注意到虚拟节点有三个属性: 使用虚拟节点,我们可以对当前的UI进行建模,以及当我们更新UI时希望它改变成什么。 假设我想将<div>中的文本从 "Hello World!" 更改成 "Hello Universe!"。可以使用DOM进行强制修改: // 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。 在本文中,我们将模仿Million.js的 Virtual DOM API。我们的API将包含三个主要功能:m, createElement, and patch。 m 函数是创建虚拟节点的辅助函数。虚拟节点包含三个属性: m帮助程序函数的示例实现如下: const m = (tag, props = { }, children = []) => ({ tag, props, children, }); 这样创建虚拟节点就简单多了。 m(div, { style: color: red }, [Hello World!]); 该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节点。 // createElement( m(div, { style: color: red }, [Hello World!]) ); 该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); // patch(el, oldVNode, newVNode); // 当前,Virtual DOM实现在计算新旧虚拟节点之间的差异时会产生计算成本。 即使使用非常有效的差分算法 (如list-diff2),当虚拟节点树大于虚拟节点的两位数时,差异成本也会变得显著。 树区分算法是出了名的慢。时间复杂度可以从O(n)转O(n ^ 3)取决于虚拟节点树的复杂性。这与DOM操纵相去甚远,后者是O(1)。 编译器是新框架”-- 汤姆·戴尔 Ember的创建者汤姆是最早倡导为JavaScript UI库使用编译器开源狂热者之一。 现在,我们知道汤姆的赌注是正确的。JavaScript生态系统见证了Solid、Svelte等“已编译”库的兴起,它们放弃了Virtual DOM。这些库使用编译器预渲染,并在使用时生成代码来跳过不必要的渲染。 另一方面,Virtual DOM落后于这一趋势。当前的虚拟DOM库本质上与“按需” 编译器不兼容。因此,Virtual DOM的渲染速度通常是比现代“No Virtual DOM” UI库慢几个数量级。 如果我们希望Virtual DOM在未来的渲染速度上具有竞争力,那就需要重新设计Virtual DOM以允许编译器增强。声明式用户界面
命令式用户界面
理解VirtualDOM
构建自己的Virtual DOM
Virtual DOM是纯开销
Virtual DOM的未来