手写一个简单的VirtualDOM,增强源码阅读能力
时间:2023-03-18 12:07:16
科技观察
你可能听说过VirtualDOM(和ShadowDOM)。甚至可能用过它(JSX基本上是VDOM的语法糖)。如果你想了解更多,那就看看今天的文章吧。什么是虚拟DOM?DOM操作是昂贵的。当您执行一次时,差异可能看起来很小(将属性分配给对象之间大约有0.4毫秒的差异),但它会随着时间的推移而增加。//给对象赋属性1000次letobj={};console.time("obj");for(leti=0;i<1000;i++){obj[i]=i;}console.timeEnd("obj");//操作dom1000次console.time("dom");for(leti=0;i<1000;i++){document.querySelector(".some-element").innerHTML+=i;}console.timeEnd("dom");当我运行上面的代码片段时,我看到第一个循环大约需要3毫秒,而第二个循环大约需要41毫秒。让我们举一个更现实的例子。functiongenerateList(list){letul=document.createElement('ul');document.getElementByClassName('.fruits').appendChild(ul);list.forEach(function(item){letli=document.createElement('li');ul.appendChild(li);li.innerHTML+=item;});returnul;}document.querySelector("ul.some-selector").innerHTML=generateList(["Banana","Apple","Orange"])到目前为止,一切都很好。现在,如果数组发生变化,我们需要重新渲染,我们这样做:document.querySelector("ul.some-selector").innerHTML=generateList(["Banana","Apple","Mango"])和看看会发生什么问题?即使只需要更改一个元素,我们也会更改整个元素,因为我们很懒惰。这就是创建虚拟DOM的原因。那么什么是虚拟Dom?虚拟DOM是DOM作为对象的表示。假设我们有如下HTML:Texthere
SomeotherBoldcontent
可以这样写VDOM对象:letvdom={tag:"div",props:{class:'contents'},children:[{tag:"p",children:"Texthere"},{tag:"p",children:["Someother",{tag:"b",children:"Bold"},"content"]}]}请注意实际开发中可能会有更多属性,此为简化版。VDOM是一个具有以下内容的对象:一个名为tag(有时也称为type)的属性,它表示标签的名称一个名为props的属性,包含所有props如果内容只是文本,则为字符串,如果内容包含元素,则为字符串,那么vdom数组我们这样使用VDOM:我们更改vdom而不是dom函数检查DOM和VDOM之间的所有差异,并且只更改更改的部分节省更多时间。有什么好处?知道了VDOM是什么,我们来完善一下之前的generateList函数。functiongenerateList(list){//VDOM生成过程,待补}patch(oldUL,generateList(["Banana","Apple","Orange"]));不要介意patch函数,它的作用是将改变的section追加到DOM中。稍后更改DOM时:patch(oldUL,generateList(["Banana","Apple","Mango"]));patch函数发现只有第三个li发生了变化,而不是所有三个元素。更改,因此只会操作第三个li元素。构建VDOM!我们需要做4件事:创建一个虚拟节点(vnode)mountVDOMunmountVDOMPatch(比较两个vnode,找出不同点,然后挂载)createvnodefunctioncreateVNode(tag,props={},children=[]){return{tag,props,children}}在Vue(以及许多其他地方)中,此函数称为h,hyperscript的缩写。挂载VDOM通过挂载,将vnode附加到任何容器,如#app或任何其他应该挂载的地方。此函数将递归遍历所有节点的子节点并将它们安装到各自的容器中。请注意,下面的所有代码都放在mount函数中。functionmount(vnode,container){...}创建DOM元素constelement=(vnode.element=document.createElement(vnode.tag))你可能会想这个vnode.element是什么。它只是一个内部设置的属性,我们可以根据它知道哪个元素是vnode的父元素。设置props对象的所有属性。我们可以循环Object.entries(vnode.props||{}).forEach([key,value]=>{element.setAttribute(key,value)})挂载子元素,有两种情况需要处理:孩子只是文本孩子是vnode数组if(typeofvnode.children==='string'){element.textContent=vnode.children}else{vnode.children.forEach(child=>{mount(child,element)//recursivemountchildnode})}最后,我们要将内容添加到DOM中:container.appendChild(element)最终结果:functionmount(vnode,container){constelement=(vnode.element=document.createElement(vnode.tag))Object.entries(vnode.props||{}).forEach([key,value]=>{element.setAttribute(key,value)})if(typeofvnode.children==='string'){element.textContent=vnode.children}else{vnode.children.forEach(child=>{mount(child,element)//递归挂载子节点})}container.appendChild(element)}卸载一个vnode卸载就像删除一个元素一样简单来自DOM:函数卸载(vnode){vnode.element.parentNode.removeChild(vnode.element)}补丁vnode。这是我们必须编写的(相对而言)最复杂的函数。要做的事情是找到两个vnode之间的差异,并且只修补更改的部分。functionpatch(VNode1,VNode2){//指定父元素constelement=(VNode2.element=VNode1.element);//现在我们要检查两个vnode之间的区别//如果节点有不同的标签,那么整个内容已更改。if(VNode1.tag!==VNode2.tag){//卸载旧节点并挂载新节点mount(VNode2,element.parentNode)unmount(Vnode1)}else{//节点有相同的tag//所以我们要检查两部分//-Props//-Children//我们这里不打算检查Props,因为会增加代码的复杂度,我们先看看如何检查Children。//检查Children//如果新节点children是字符串if(typeofVNode2.children==”string”){//如果两个children完全不同if(VNode2.children!==VNode1.children){element.textContent=VNode2.children;}}else{///如果新节点的子节点是数组//-子节点的长度相同//-旧节点的子节点多于新节点//-新节点的子节点多于旧节点//检查长度constchildren1=VNode1.children;constchildren2=VNode2.children;constcommonLen=Math.min(children1.length,children2.length)//递归调用patchfor(leti=0;i
children2.length){children1.slice(children2.length).forEach(child=>{unmount(child)})}//如果新节点的子节点比旧节点多if(children2.length>children1.length){children2.slice(children1.length).forEach(child=>{mount(child,element)})}}}}这是vdomi的基本版本实现,方便我们快速掌握概念。当然还有一些事情要做,包括检查道具和一些性能改进。现在让我们渲染一个vdom!回到generateList的例子。对于我们的vdom实现,我们可以做functiongenerateList(list){letchildren=list.map(child=>createVNode("li",null,child));returncreateVNode("ul",{class:'fruits-ul'},children)}mount(generateList(["apple","banana","orange"]),document.querySelector("#app")/*anyselector*/)在线示例:https://codepen.io/SiddharthShyniben/pen/MWpQrwM~完了,我是小智,我们去SPA吧,下期见!作者:Siddharth译者:前端小智来源:devthe-virtual-dom-let-s-build-it-5070