当前位置: 首页 > Web前端 > vue.js

手写一个简单的VirtualDOM,加强源码阅读能力

时间:2023-04-01 01:57:36 vue.js

作者:Siddharth译者:前端小智来源:dev有梦想,有干货,微信搜索【大千世界】关注这个谁还在清晨洗碗擦碗。本文已收录到GitHubhttps://github.com/qq449245884/xiaozhi,里面有完整的测试站点、资料和我的一线厂商访谈系列文章。您可能听说过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

可以这样写如下VDOMobject: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函数,它的作用是将改变的部分追加到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)})挂载子元素,有两种情况需要处理:children只是文本children是vnode数组if(typeofvnode.children==='string'){element.textContent=vnode.children}else{vnode.children.forEach(child=>{mount(child,element)//递归挂载子节点})}最后,我们必须向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{//节点具有相同的标签//所以我们要检查两部分//-Props//-Children//这里不打算检查Props,因为会增加代码的复杂度,先看看如何检查Children//CheckChildren//如果新节点的子节点是字符串if(typeofVNode2.children=="string"){//如果两个子节点完全不同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;ichildren2.length){children1.slice(children2.length).forEach(child=>{unmount(child)})}//如果新节点比旧节点有更多的孩子if(children2.length>children1.length){children2.slice(children1.length).forEach(child=>{mount(child,element)})}}}}这是vdom实现的基础版本,可以让我们快速掌握概念。当然还有一些事情要做,包括检查道具和一些性能改进。现在让我们渲染一个vdom!回到generateList的例子。对于我们的vdom实现,我们可以执行以下操作:functiongenerateList(list){returncreateVNode("ul",{class:'fruits-ul'},children)}mount(generateList(["apple","banana","orange"]),document.querySelector("#app")/*anyselector*/)在线例子:https://codepen.io/SiddharthS...~终于来了,小智,我们去SPA吧,下期见!代码部署后可能存在的bug,无法实时获知。事后为了解决这些bug,花费了大量的时间在日志调试上。顺便推荐一个好用的bug监控工具Fundebug。原文:https://dev.to/siddharthshyni...交流有梦想,有干货,微信搜索【GreatMovetotheWorld】关注这位凌晨还在洗碗的洗碗智慧。本文GitHubhttps://github.com/qq44924588...已收录,有完整的测试站点、资料和我的一线厂商访谈系列文章。