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

Vue.js源码学习(完)——虚拟DOM补丁算法

时间:2023-03-31 21:43:44 vue.js

完整代码(https://github.com/mfaying/si...VirtualDOM虚拟DOM比较虚拟节点vnode和旧虚拟节点oldVnode,获取真正需要更新的节点来执行DOM操作。比较两个虚拟节点是虚拟DOM中的核心算法(即补丁)。由于Vue.js的变化检测粒度非常细,你可以知道哪些状态到一个一定程度上发生了变化,所以可以通过细粒度的绑定来更新视图,这在Vue.js1.0中实现了。但是因为粒度太细了,会有很多观察者同时观察某个状态,会有一些内存开销和一些依赖跟踪的开销,所以Vue.js采用了中粒度的方案,状态检测不再细化到具体的节点,而是细化到一个组件,视图是通过里面的虚拟DOM来渲染的组件,可以大大减少de的数量挂起率和观察者的数量。VNode在Vue.js中有一个VNode类,可以用来实例化不同类型的vnode实例,不同类型的vnode实例代表不同类型的DOM元素。Vnode可以理解为DOM元素的JavaScript对象版本。VNode的作用Vue.js可以缓存上次渲染视图时创建的vnode,将新创建的vnode与缓存的vnode进行比较,找出不同的方法并修改基于此的真实DOM。VNode注释节点的类型文本节点元素节点组件节点功能组件克隆节点注释节点exportconstcreateEmptyVNode=text=>{constnode=newVNode();node.text=文本;node.isComment=true;returnnode;}textnodeexportfunctioncreateTextVNode(val){returnnewVNode(undefined,undefined,undefined,String(val))}克隆一个节点克隆一个节点就是把一个已经存在的节点的属性复制到一个新的节点上,所以即新创建的节点和克隆的节点连接的属性一致,从而达到克隆的效果。它的作用是优化静态节点和槽节点(slotnodes)。以静态节点为例,因为静态节点的内容是不会改变的,除了第一次渲染需要执行渲染函数获取VNode外,后续更新不需要执行渲染函数重新生成vnode.因此,创建克隆节点的方法将用于克隆vnode的副本,并使用克隆节点进行渲染。exportfunctioncloneVNode(vnode,deep){constcloned=newVNode(vnode.tag,vnode.data,vnode.children,vnode.text,vnode.elm,vnode.context,vnode.componentOptions,vnode.asyncFactory)cloned.ns=vnode.nscloned.isStatic=vnode.isStaticcloned.key=vnode.keycloned.isComment=vnode.isCommetcloned.isCloned=trueif(deep&&vnode.children){cloned.children=cloneVNodes(vnode.children)}returncloned;}克隆节点和克隆节点的唯一区别是isCloned属性。元素节点一个元素节点通常有以下四个有效属性:tag:节点名称,如p、ul、li等。data:节点上的数据,如attrs、class、style。children:当前节点的子节点列表context:当前组件的Vue.js实例组件节点类似于元素节点,也有以下两个特有的属性componentOptions:组件节点的选项参数,包括propsData、tag和children等信息。componentInstance:组件的实例也是Vue.js的一个实例。功能组件类似于元素节点。此外,它们还有以下两个独特的属性:functionalOptionsfunctionalContextpatch虚拟DOM的核心部分是patch,可以将vnode渲染成真实的DOM。补丁也可以称为补丁算法。Patch并不是节点的暴力替换,而是对现有DOM的修改,以达到渲染视图的目的。修改现有DOM需要做三件事:创建新节点删除过时节点修改需要更新的节点添加节点一个明显的添加节点场景是,当oldVnode不存在但vnode存在时,需要使用vnode来生成真正的DOM元素并将它们插入到视图中。当vnode和oldVnode根本不是同一个节点时(存在相同位置的节点),就需要用vnode生成真正的DOM元素插入到视图中。删除节点当一个节点只存在于oldVnode中时,我们需要将其从DOM中删除。当vnode和oldVnode根本不是同一个节点时,oldVnode对应的旧节点需要替换为vnode在DOM中创建的新节点,替换过程是将新创建的DOM节点插入到旧节点的旁边,然后插入旧节点Node删除。更新节点当新旧节点是同一个节点时,我们需要详细比较两个节点,然后更新视图中oldVnode对应的真实节点。创建节点只有三种类型的节点被创建并插入到DOM中:元素节点、注释节点和文本节点。判断一个vnode是否为元素节点,只需要判断它是否有tag属性即可。我们可以调用当前环境下的createElement方法(浏览器环境下是document.createElement)来创建元素节点。渲染一个元素到视图,只需要在当前环境调用appendChild方法(浏览器环境调用parendNode.appendChild)。元素通常都有子节点(children),所以当一个元素节点被创建时,我们需要添加它的子节点也被创建并插入到新创建的节点下。这是一个递归过程。只需要循环遍历vnode中的children属性,为每个子虚拟节点执行创建元素的逻辑即可。如果vnode没有标签属性,那么它可能是另外两个节点:注释节点和文本节点。当isComment属性为真时,vnode为注释节点,否则为文本节点。如果是文本节点,调用当前环境下的createTextNode方法(浏览器环境下是document.createTextNode)创建一个真正的文本节点,插入到指定的父节点中。如果是评论节点,调用当前环境下的createComment方法(浏览器环境下是document.createComment)创建一个真正的评论节点,插入到指定的父节点中。删除节点函数removeVnodes(vnodes,startIdx,endIdx){for(;startIdx<=endIdx;++startIdx){constch=vnodes[startIdx]if(isDef(ch)){removeNode(ch.elm)}}}constnodeOps={removeChild(node,child){node.removeChild(child)}}functionremoveNode(el){constparent=nodeOps.parentNode(el)if(isDef(parent)){nodeOps.removeChild(parent,el)}}将节点操作封装成函数放在nodeOps中,就是为了预留跨平台的渲染接口。更新节点1.静态节点如果新旧虚拟节点都是静态节点,则不需要更新节点,直接跳过更新节点的过程。2.新的虚拟节点有一个文本属性。根据新节点(vnode)是否具有文本属性,更新节点可以分为两种不同的情况。如果有text属性,不管旧节点的子节点是什么,直接调用setTextContent方法(浏览器是node.textContent),将view中DOM节点的内容改为text属性保存的文本的虚拟节点。如果新旧节点都是文本,且文本相同,则不会执行。3.新虚拟节点没有文本属性如果新虚拟节点没有文本属性,那么它是一个元素节点。3.1子节点的情况如果旧虚拟节点也有子节点属性,那么就需要更详细地比较和更新新旧虚拟节点的子节点。更新子节点可能会移动子节点的位置,或者删除或添加节点。children的具体更新后文会介绍。如果没有children属性,旧的虚拟节点要么是一个空标签,要么是一个文本节点。如果是文本节点,先清除文本,使其成为空标签,然后将新虚拟节点中的children逐一创建为真正的DOM元素节点,插入到视图中的DOM节点下。3.1没有子节点的情况是指新创建的节点是一个空节点,旧的虚拟节点有子节点和文本需要删除,以达到视图为空标签的目的。更新子节点如上所述,当新节点的子节点和旧节点的子节点都存在且不同时,将执行子节点的更新操作。我们在下面详细讨论这个案例。更新子节点大致可以分为四种操作:更新节点、添加节点、删除节点和移动节点。要比较两个子节点(children)列表,首先要做的就是循环。循环newChildren,每循环一个新的子节点,就去oldChildren中寻找与当前节点相同的old子节点。如果没有找到,则添加、创建节点并插入到视图中;如果找到,更新;如果位置不同,则移动节点。更新策略1、创建子节点如果在oldChildren中没有找到与本次循环指向的新子节点相同的节点,则需要执行创建节点操作,将新创建的节点插入到oldChildren中所有未处理的节点中(notprocessing是在没有任何更新操作的节点前面)。不能插入到处理过的节点之后,因为旧虚拟节点的处理过的节点不包括新插入的节点。如果连续插入多个新节点,则新节点的顺序将颠倒。2.更新子节点节点在newChildren和oldChildren中都存在,位置相同,需要更新节点的操作。之前介绍过更新节点操作。3、移动子节点节点在newChildren和oldChildren中都存在,只是位置不同。这个节点的位置需要根据新的虚拟节点的位置进行移动。通过Node.insertBefore()方法,我们可以将一个已经存在的节点移动到指定的位置。那么如何知道新的虚拟节点位于何处呢?其实并没有那么难。比较两个子节点列表是通过从左到右循环newChildren列表,然后每循环一个节点,就去oldChildren中寻找和这个节点相同的节点进行处理。也就是说newChildren中当前循环到的节点左侧已经处理完毕。不难发现,这个节点的位置是所有未处理节点中的第一个节点。因此,只需将需要移动的节点移动到所有未处理节点的前面即可。如何区分哪些节点被处理,哪些节点没有被处理,后面会介绍。4、删除子节点newChildren中的所有节点循环完毕后,如果oldChildren中还有未处理的节点,则需要删除这些节点。当优化策略通过后,并不是所有的子节点都会移动,列表中总会有几个节点的位置保持不变。对于这些位置不变或者位置可以预测的节点,我们不需要循环寻找。我们有4种捷径搜索方式:newfront,oldfrontnewpost,oldpostnewpost,oldfrontnewfrontandoldpostnewbefore:newChildren中所有未处理的第一个节点newafter:newChildren中所有未处理的最后一个节点oldbefore:allunprocessedfirstnodesinoldChildrenoldafter:所有未处理的lastnodesinoldChildrennew如果新前和旧前是同一个节点before和oldfront,因为位置相同,就更新节点。如果没有,请切换到另一个快捷方式。新皇后和旧皇后如果新皇后和旧皇后是同一个节点,由于位置相同,更新节点即可。新帖与旧帖如果新帖与旧帖是同一个节点,由于位置不同,除了更新节点外,还需要执行移动节点的操作,将节点移动到oldChildren中所有未处理节点的末尾。新前台和旧帖如果新前台和旧帖是同一个节点,由于位置不同,除了更新节点外,还需要执行移动节点的操作,将节点移动到oldChildren中所有未处理节点的前面。如果前面4种方法比较后都没有找到相同的节点,那就去oldChildren通过循环的方式详细找一个圆。哪些节点是未处理的,因为我们的逻辑是在循环体中处理的,所以只要循环条件保证只有未处理的节点才能进入循环体。一个正常的循环可以达到这个效果,但是由于我们的优化策略,有可能后面会比较节点,如果比较成功就会执行更新过程。也就是说,循环不再只处理未处理节点中的第一个,而可能处理最后一个。在这种情况下,它不能从前向后循环,而是从两侧向中间循环。那么,如何实现从两边到中间的循环呢?首先,我们准备4个变量:oldStartIdx、oldEndIdx、newStartIdx、newEndIdx。在循环体中,每处理一个节点,下标就向指定方向移动一个位置。通常,更新操作是对新旧节点进行的,相当于一次处理两个节点,将新旧节点的下标按指定方向移动一个位置。起始位置代表的节点处理完后,向后移动一个位置;结束位置的节点处理完后,向前移动一个位置。即oldStartIdx和newStartIdx只能向后移动,oldEndIdx和newEndIdx只能向前移动。当起始位置大于等于结束位置时,表示遍历完所有节点,循环结束。while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){//dosomething}你可能会发现,不管newChildren还是oldChildren,只要完成一个循环,循环就会退出。那么,当新的子节点和旧的子节点的节点数不一致时,循环结束后仍然会有未处理的节点,也就是说本次循环不需要遍历所有节点。因为如果先完成oldChildren的循环,如果此时newChildren中还有剩余的节点,就说明这些节点都是新的节点,直接将这些节点插入到DOM中就可以了。如果newChildren循环先完成,如果oldChildren中还有剩余节点,则说明这些节点是废弃节点,可以将这些节点从DOM中移除。在patch中,还有一部分逻辑是建立key和index的对应关系。在Vue.js的模板中,可以在渲染列表的时候为节点设置一个属性键,这个属性可以标记一个节点的唯一ID。更新子节点时,需要遍历oldChildren寻找节点。如果我们为子节点设置属性key,并建立key和索引index的对应关系,就会生成一个对象,比如key对应一个节点下标。那么在oldChildren中寻找同一个节点时,可以直接通过key获取下标,从而得到节点,不需要通过循环寻找节点。完整代码(https://github.com/mfaying/si...参考《深入浅出Vue.js》