当前位置: 首页 > 科技观察

VirtualDOM的历史和未来

时间:2023-03-14 11:50:49 科技观察

VirtualDOM最初是由React的作者开创的,目的是使声明式UI渲染更快。为了理解为什么声明式UI最初如此缓慢??,我们首先需要了解过去声明式UI是如何完成的。声明式用户界面编写声明式UI的传统方法是更改??元素的innerHTML属性。例如,如果我想向

UI添加一个元素,我会编写以下内容:document.body.innerHTML='
HelloWorld!
';//现在有一个
你好,世界!
孩子。我们可以意识到innerHTML允许我们以声明的方式定义UI,但效率不高。效率低下源于每次更改UI时解析、销毁和重建innerHTML,分为四个步骤:将innerHTML字符串解析到DOM节点树中。从元素中删除所有内容。将DOM节点树插入到元素中。执行布局计算并重绘屏幕。此过程在计算上非常昂贵,并且可能导致渲染速度显着变慢。命令式用户界面那么,这个问题是如何解决的呢?即选择使用DOM,比innerHTML方式快3倍。constdiv=document.createElement('div');div.textContent='HelloWorld!';document.body.appendChild(div);但是,我们可以认识到,手动编写这些内容可能很麻烦,尤其是当UI交互很多时,因为我们需要命令式地指定每个步骤。以声明方式编写UI要优雅得多。然而,React作者创建了VirtualDOM,它允许我们以比innerHTML渲染更快的方式编写UI,并且是声明式的。了解VirtualDOM为了最好地了解VirtualDOM的工作原理,让我们概述一下流程,然后构建一个示例。VirtualDOM是一种渲染UI的方式。此方法利用模仿DOM树的JavaScript对象树(“虚拟”节点)。//
HelloWorld!
constdiv=document.createElement('div');div.style='color:red';div.textContent='HelloWorld!';上面的
是模仿下面的JavaScript对象中的虚拟节点:constdivVNode={type:'div',props:{style:'color:red'}children:['HelloWorld!']};我们可以注意到虚拟节点具有三个属性:tag:将元素的标签名称存储为字符串。props:将元素的属性和属性存储为一个对象。children:将元素的虚拟节点children存储为一个数组。使用虚拟节点,我们可以对当前的UI进行建模,以及我们希望在更新UI时将其更改为什么。假设我想将
中的文本从“HelloWorld!”更改为到“你好宇宙!”。DOM可用于强制修改://
HelloWorld!
constdiv=document.createElement('div');div.style='color:red';div.textContent='HelloWorld!';//从“HelloWorld!”改变到“你好宇宙!”div.textContent='你好宇宙!';但是使用VirtualDOM,我可以指定当前UI的外观(旧虚拟节点)以及我希望它的外观(新虚拟节点)。constoldVNode={type:'div',props:{style:'color:red'}children:['HelloWorld!']};constnewVNode={type:'div',props:{style:'color:red'}children:['HelloUniverse!']};然而,要让VirtualDOM真正将更改应用到UI,它还需要计算旧虚拟节点和新虚拟节点之间的差异。{type:'div',props:{style:'color:red'}-children:['HelloWorld!']+children:['HelloUniverse!']};当我们知道了两者的区别之后,就可以通过VirtualDOM来改变UI了。div.replaceChild(newChild,oldChild);VirtualDOM只做必要的修改,不会替换整个UI。构建您自己的虚拟DOM在本文中,我们将模仿Million.js的虚拟DOMAPI。我们的API将包含三个主要函数:m、createElement和patch。m(tag,props,children)m函数是创建虚拟节点的辅助函数。一个虚拟节点包含三个属性:tag:将虚拟节点的名称标记为字符串;props:节点作为对象的属性/属性;children:作为数组的虚拟节点的孩子。m辅助函数的示例实现如下:constm=(tag,props={},children=[])=>({tag,props,children,});这使得创建虚拟节点变得更加简单。m('div',{style:'color:red'},['HelloWorld!']);#createElement(vnode)createElement函数将虚拟节点转换为真实的DOM元素。这很重要,因为我们将在补丁功能中使用它。实现如下:如果虚拟节点是文本,则返回文本节点;标签使用虚拟节点的属性创建一个新的DOM节点;遍历虚拟节点props并将它们添加到DOM节点。遍历子项,递归地调用每个子项的createElement并将其添加到DOM节点。constcreateElement=(vnode)=>{if(typeofvnode==='string'){returndocument.createTextNode(vnode);}constel=document.createElement(vnode.tag);for(constpropinvnode.props){el[prop]=vnode.props[prop];}for(constchildofvnode.children){el.appendChild(createElement(child));}返回el;};这使得将虚拟节点转换为DOM节点变得容易。//HelloWorld!
createElement(m('div',{style:'color:red'},['HelloWorld!']));#patch(el,newVNode,oldVNode)patch函数采用一个现有的DOM节点、一个旧的虚拟节点和一个新的虚拟节点。实现如下:计算两个虚拟节点的差值;如果虚拟节点是字符串,则用新节点替换DOM节点的文本内容;如果虚拟节点是一个对象,tag、props、children不同,则更新节点。constpatch=(el,newVNode,oldVNode)=>{if(!newVNode&&newVNode!=='')returnel.remove();if(typeofoldVNode==='string'||typeofnewVNode==='string'){if(oldVNode!==newVNode){returnel.replaceWith(createElement(newVNode));}}else{if(oldVNode.tag!==newVNode.tag){returnel.replaceWith(createElement(newVNode));}//patchpropsfor(constpropin{...oldVNode.props,...newVNode.props,}){if(newVNode.props[prop]===undefined){deleteel[prop];}elseif(oldVNode.props[prop]===undefined||oldVNode.props[prop]!==newVNode.props[prop]){el[prop]=newVNode.props[prop];}}//patchchildrenfor(leti=oldVNode.children.length-1;i>=0;--i){patch(el.childNodes[i],newVNode.children[i],oldVNode.children[i]]);}for(让我=oldVNode.childr长度;iHelloWorld!
patch(el,oldVNode,newVNode);//HelloUniverse!
VirtualDOM是纯粹的开销目前,VirtualDOM实现在计算时会产生计算成本新旧虚拟节点的区别。即使使用像list-diff2这样非常有效的差分算法,当虚拟节点树大于虚拟节点的两位数时,差异成本也会变得很大。树差异算法是出了名的慢。时间复杂度可以从O(n)到O(n^3),具体取决于虚拟节点树的复杂度。这与复杂度为O(1)的DOM操作相去甚远。虚拟DOM编译器的未来是新的框架”——TomDaleEmber的创建者Tom是最早倡导JavaScriptUI库编译器的开源爱好者之一。现在,我们知道Tom的赌注是正确的。JavaScript生态系统见证Solid、Svelte等“编译型”库的兴起,它们放弃了VirtualDOM。这些库使用编译器预渲染,并在使用时生成代码以跳过不必要的渲染。另一方面,VirtualDOM落后于这种趋势。当前的虚拟DOM库与“按需”编译器天生不兼容。因此,虚拟DOM的渲染速度通常比现代“无虚拟DOM”UI库慢几个数量级。如果我们希望VirtualDOM在未来的渲染速度方面具有竞争力,那么VirtualDOM需要重新设计以允许编译器增强。