什么是虚拟DOM?上图,有图有真相。构建DOM树。通过HTML解析器解析处理HTML标签,构建成DOM树(DOMtree),解析器遇到非阻塞资源(图片,css)会继续解析,但如果遇到script标签(尤其是没有async和defer属性),它会阻塞渲染并停止对html的解析,这就是为什么最好将script标签放在body下的原因。构建一个CSSOM树。与构建DOM类似,浏览器也将样式规则构建到CSSOM中。浏览器会遍历CSS中的规则集,根据CSS选择器创建一个具有父子、兄弟等关系的节点树。构建渲染树。此步骤将DOM与CSSOM相关联,确定应将哪些CSS规则应用于每个DOM元素。将所有相关样式匹配到DOM树中的每个可见节点,根据CSS级联确定每个节点的计算样式。不可见的节点(head,属性中包含display:none的节点)不会生成到Render树中。布局/回流。浏览器第一次确定节点的位置和大小称为布局。如果后续节点的位置和大小发生变化,这一步会触发布局调整,即回流。绘制/重绘(Paint/Repaint)。将元素的每个可见部分绘制到屏幕上,包括文本、颜色、边框、阴影和替换元素(如按钮和图像)。如果文本、颜色、边框、阴影等这些元素发生变化,就会触发重绘(Repaint)。为了确保重绘比初始绘制更快,屏幕上的绘制通常被分解成几层。将内容提升到GPU层(由transform、filter、will-change、opacity触发)可以提高绘制和重绘的性能。合成。此步骤合并了绘制过程中的图层,确保它们以正确的顺序绘制以在屏幕上显示正确的内容。为什么我们需要虚拟DOM?从上图我们可以很清楚的了解到一个真实DOM的渲染过程是什么样子的。很明显,如果DOM节点更新了,整个DOM都会重新渲染,但是DOM节点渲染是一件很耗性能的事情。那么有什么办法可以更新需要更新的节点而不是整个DOM树呢?这时候虚拟DOM就诞生了。VirtualDOM本质上是一个js对象,通过对象来表示真实的DOM结构。Tags用来描述标签,props用来描述属性,children用来表示嵌套的层级关系。虚拟DOM的优点:小的修改不需要频繁更新DOM,框架的diff算法会自动比较、分析需要更新的节点,按需更新。更新数据不会造成频繁的回流和重绘。表达力更强,数据更新更方便。保存的是js对象,具有跨平台能力。缺点:VirtualDOM也有缺点。第一次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。虚拟DOM实现原理明白了为什么需要虚拟DOM以及虚拟DOM的优势之后,我们现在就直接进入正题吧。什么是Diff算法,其实简单来说,Diff算法就是实现虚拟DOM的一个核心原理。Diff算法是一种比较算法。比较旧的虚拟DOM和新的虚拟DOM,比较哪个虚拟节点发生了变化,找出这个虚拟节点,只更新这个虚拟节点对应的真实节点,而不更新其他数据没有变化的节点。准确更新真实DOM,提高效率。Diff算法主要有3个过程:通过js创建节点描述对象;用diff算法比较分析新旧虚拟DOM的差异;patch差异到真正的DOM来实现更新。还是老规矩,直接上传图片就可以了,很好理解。Diff算法的比较过程就像React、vue2.X、vue3.X中使用的Diff算法。vue2.X中使用的Diff算法采用了深度优先、同层比较的策略。核心代码patch.jsfunctionpatch(oldVnode,newVnode){//比较是否是一类节点if(sameVnode(oldVnode,newVnode)){//是:继续进行深度比较patchVnode(oldVnode,newVnode)}else{//没有constoldEl=oldVnode.el//旧虚拟节点的真实DOM节点constparentEle=api.parentNode(oldEl)//获取父节点createEle(newVnode)//创建新对应的真实DOM节点virtualnodeif(parentEle!==null){api.insertBefore(parentEle,vnode.el,api.nextSibling(oEl))//向父元素添加新元素api.removeChild(parentEle,oldVnode.el)//移除旧元素elementnode//设置null释放内存oldVnode=null}}returnnewVnode}functionsameVnode(oldVnode,newVnode){return(oldVnode.key===newVnode.key&&//key值是否相同oldVnode.tagName===newVnode.tagName&&//标签名称是否相同?oldVnode.isComment===newVnode.isComment&&//都是注释节点isDef(oldVnode.data)===isDef(newVnode.data)&&//是否定义了所有数据sameInputType(oldVnode,newVnode)//输入标签时,type必须相同)}patchNode做了以下事情:创建新节点,删除废弃节点,更新已有节点节点,查找到对应的真实DOM,调用el判断newVnode和oldVnode是否指向同一个对象,如果是,则直接返回如果两者都有文本节点且不相等,则将el的文本节点设置为文本节点新Vnode。如果oldVnode有子节点而newVnode没有,则删除el的子节点如果oldVnode没有子节点,而newVnode有子节点,实现后将newVnode的子节点添加到el如果两者都有子节点,则执行updateChildren函数进行比较子节点,这一步很重要。functionpatchVnode(oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly){//判断vnode是否和oldVnode完全一样if(oldVnode===vnode){return}if(isDef(vnode.elm)&&isDef(ownerArray)){//克隆重用节点vnode=ownerArray[index]=cloneVNode(vnode)}constelm=vnode.elm=oldVnode.elmif(isTrue(oldVnode.isAsyncPlaceholder)){if(isDef(vnode.asyncFactory.resolved)){hydrate(oldVnode.elm,vnode,insertedVnodeQueue)}else{vnode.isAsyncPlaceholder=true}return}//是否为静态节点,key是否相同,是否为clone节点,once属性是否为设置if(isTrue(vnode.isStatic)&&isTrue(oldVnode.isStatic)&&vnode.key===oldVnode.key&&(isTrue(vnode.isCloned)||isTrue(vnode.isOnce))){vnode.componentInstance=oldVnode.componentInstancereturn}leticonstdata=vnode.dataif(isDef(data)&&isDef(i=data.hook)&&isDef(i=i.prepatch)){i(oldVnode,vnode)}constoldCh=oldVnode.childrenconstch=vnode.childrenif(isDef(data)&&isPatchable(vnode)){//调用更新回调和更新挂钩for(i=0;i
