安庆大理运城常德铜陵江西
投稿投诉
江西南阳
嘉兴昆明
铜陵滨州
广东西昌
常德梅州
兰州阳江
运城金华
广西萍乡
大理重庆
诸暨泉州
安庆南充
武汉辽宁

我让虚拟DOM的diff算法过程动起来了

3月23日 喵小咪投稿
  去年写了一篇文章手写一个虚拟DOM库,彻底让你理解diff算法介绍虚拟DOM的patch过程和diff算法过程,当时使用的是双端diff算法,今年看到了Vue3使用的已经是快速diff算法,所以也想写一篇来记录一下,但是肯定已经有人写过了,所以就在想能不能有点不一样的,上次的文章主要是通过画图来一步步展示diff算法的每一种情况和过程,所以就在想能不能改成动画的形式,于是就有了这篇文章。当然目前的实现还是基于双端diff算法的,后续会补充上快速diff算法。
  传送门:双端Diff算法动画演示。
  界面就是这样的,左侧可以输入要比较的新旧VNode列表,然后点击启动按钮就会以动画的形式来展示从头到尾的过程,右侧是水平的三个列表,分别代表的是新旧的VNode列表,以及当前的真实DOM列表,DOM列表初始和旧的VNode列表一致,算法结束后会和新的VNode列表一致。
  需要说明的是这个动画只包含diff算法的过程,不包含patch过程。
  先来回顾一下双端diff算法的函数:constdiff(el,oldChildren,newChildren){指针letoldStartIdx0letoldEndIdxoldChildren。length1letnewStartIdx0letnewEndIdxnewChildren。length1节点letoldStartVNodeoldChildren〔oldStartIdx〕letoldEndVNodeoldChildren〔oldEndIdx〕letnewStartVNodenewChildren〔newStartIdx〕letnewEndVNodenewChildren〔newEndIdx〕while(oldStartIdxoldEndIdxnewStartIdxnewEndIdx){if(oldStartVNodenull){oldStartVNodeoldChildren〔oldStartIdx〕}elseif(oldEndVNodenull){oldEndVNodeoldChildren〔oldEndIdx〕}elseif(newStartVNodenull){newStartVNodeoldChildren〔newStartIdx〕}elseif(newEndVNodenull){newEndVNodeoldChildren〔newEndIdx〕}elseif(isSameNode(oldStartVNode,newStartVNode)){头头patchVNode(oldStartVNode,newStartVNode)更新指针oldStartVNodeoldChildren〔oldStartIdx〕newStartVNodenewChildren〔newStartIdx〕}elseif(isSameNode(oldStartVNode,newEndVNode)){头尾patchVNode(oldStartVNode,newEndVNode)把oldStartVNode节点移动到最后el。insertBefore(oldStartVNode。el,oldEndVNode。el。nextSibling)更新指针oldStartVNodeoldChildren〔oldStartIdx〕newEndVNodenewChildren〔newEndIdx〕}elseif(isSameNode(oldEndVNode,newStartVNode)){尾头patchVNode(oldEndVNode,newStartVNode)把oldEndVNode节点移动到oldStartVNode前el。insertBefore(oldEndVNode。el,oldStartVNode。el)更新指针oldEndVNodeoldChildren〔oldEndIdx〕newStartVNodenewChildren〔newStartIdx〕}elseif(isSameNode(oldEndVNode,newEndVNode)){尾尾patchVNode(oldEndVNode,newEndVNode)更新指针oldEndVNodeoldChildren〔oldEndIdx〕newEndVNodenewChildren〔newEndIdx〕}else{letfindIndexfindSameNode(oldChildren,newStartVNode)newStartVNode在旧列表里不存在,那么是新节点,创建插入if(findIndex1){el。insertBefore(createEl(newStartVNode),oldStartVNode。el)}else{在旧列表里存在,那么进行patch,并且移动到oldStartVNode前letoldVNodeoldChildren〔findIndex〕patchVNode(oldVNode,newStartVNode)el。insertBefore(oldVNode。el,oldStartVNode。el)将该VNode置为空oldChildren〔findIndex〕null}newStartVNodenewChildren〔newStartIdx〕}}旧列表里存在新列表里没有的节点,需要删除if(oldStartIdxoldEndIdx){for(letioldStartIioldEndIi){removeEvent(oldChildren〔i〕)oldChildren〔i〕el。removeChild(oldChildren〔i〕。el)}}elseif(newStartIdxnewEndIdx){letbeforenewChildren〔newEndIdx1〕?newChildren〔newEndIdx1〕。el:nullfor(letinewStartIinewEndIi){el。insertBefore(createEl(newChildren〔i〕),before)}}}
  该函数具体的实现步骤可以参考之前的文章,本文就不再赘述。
  我们想让这个diff过程动起来,首先要找到动画的对象都有哪些,从函数的参数开始看,首先oldChildren和newChildren两个VNode列表是必不可少的,可以通过两个水平的列表表示,然后是四个指针,这是双端diff算法的关键,我们通过四个箭头来表示,指向当前所比较的节点,然后就开启循环了,循环中新旧VNode列表其实基本上是没啥变化的,我们实际操作的是VNode对应的真实DOM元素,包括patch打补丁、移动、删除、新增等等操作,所以我们再来个水平的列表表示当前的真实DOM列表,最开始肯定是和旧的VNode列表是对应的,通过diff算法一步步会变成和新的VNode列表对应。
  再来回顾一下创建VNode对象的h函数:exportconsth(tag,data{},children){lettextletelletkey文本节点if(typeofchildrenstringtypeofchildrennumber){textchildrenchildrenundefined}elseif(!Array。isArray(children)){childrenundefined}if(datadata。key){keydata。key}return{tag,元素标签children,子元素text,文本节点的文本el,真实domkey,data}}
  我们输入的VNode列表数据会使用h函数来创建成VNode对象,所以可以输入的最简单的结构如下:〔{tag:p,children:文本节点的内容,data:{key:a}}〕
  输入的新旧VNode列表数据会保存在store中,可以通过如下方式获取到:输入的旧VNode列表store。oldVNode输入的新VNode列表store。newVNode
  接下来定义相关的变量:指针列表constoldPointerListref(〔〕)constnewPointerListref(〔〕)真实DOM节点列表constactNodeListref(〔〕)新旧节点列表constoldVNodeListref(〔〕)constnewVNodeListref(〔〕)提示信息constinforef()
  指针的移动动画可以使用css的transition属性来实现,只要修改指针元素的left值即可,真实DOM列表的移动动画可以使用Vue的列表过渡组件TransitionGroup来轻松实现,模板如下:!指针{{item。name}}{{item。value}}imgsrcc2021imgdataimg。jpgdatasrcimgq01。71396。combkahe617e762922f97f8。jpgalt!旧节点列表0旧的VNode列表TransitionGroupnamelist{{item?item。children:空}}TransitionGroup!新节点列表0新的VNode列表TransitionGroupnamelist{{item。children}}TransitionGroup!提示信息{{info}}!指针imgsrcc2021imgdataimg。jpgdatasrcimgq01。71396。combkahaaea1f3ea833cd01。jpgalt{{item。value}}{{item。name}}!真实DOM列表0真实DOM列表TransitionGroupnamelist{{item。children}}TransitionGroup
  双端diff算法过程中是不会修改新的VNode列表的,但是旧的VNode列表是有可能被修改的,也就是当首尾比较没有找到可以复用的节点,但是通过直接在旧的VNode列表中搜索找到了,那么会移动该VNode对应的真实DOM,移动后该VNode其实就相当于已经被处理过了,但是该VNode的位置又是在当前指针的中间,不能直接被删除,所以只好置为空null,所以可以看到模板中有处理这种情况。
  另外我们还创建了一个info元素用来展示提示的文字信息,作为动画的描述。
  但是这样还是不够的,因为每个旧的VNode是有对应的真实DOM元素的,但是我们输入的只是一个普通的json数据,所以模板还需要新增一个列表,作为旧的VNode列表的关联节点,这个列表只要提供节点引用即可,不需要可见,所以把它的display设为none:根据输入的旧VNode列表创建元素constoldVNodeListcomputed((){returnJSON。parse(store。oldVNode)})引用DOM元素constoldNoderef(null)constoldNodeListref(〔〕)!隐藏{{item。children}}
  然后当我们点击启动按钮,就可以给我们的三个列表变量赋值了,并使用h函数创建新旧VNode对象,然后传递给打补丁的patch函数就可以开始进行比较更新实际的DOM元素了:conststart(){nextTick((){表示当前真实的DOM列表actNodeList。valueJSON。parse(store。oldVNode)表示旧的VNode列表oldVNodeList。valueJSON。parse(store。oldVNode)表示新的VNode列表newVNodeList。valueJSON。parse(store。newVNode)nextTick((){letoldVNodeh(p,{key:1},JSON。parse(store。oldVNode)。map((item,index){创建VNode对象letvnodeh(item。tag,item。data,item。children)关联真实的DOM元素vnode。eloldNodeList。value〔index〕returnvnode}))列表的父节点也需要关联真实的DOM元素oldVNode。eloldNode。valueletnewVNodeh(p,{key:1},JSON。parse(store。newVNode)。map(item{returnh(item。tag,item。data,item。children)}))调用patch函数进行打补丁patch(oldVNode,newVNode)})})}
  可以看到我们输入的新旧VNode列表是作为一个节点的子节点的,这是因为只有当比较的两个节点都存在非文本节点的子节点时才需要使用diff算法来高效的更新他们的子节点,当patch函数运行完后你可以打开控制台查看隐藏的DOM列表,会发现是和新的VNode列表保持一致的,那么你可能要问,为什么不直接用这个列表来作为真实DOM列表呢,还要自己额外创建一个actNodeList列表,其实是可以,但是diff算法过程中是使用insertBefore等方法来移动真实DOM节点的,所以不好加过渡动画,只会看到节点瞬间换位置,不符合我们的动画需求。
  到这里效果如下:
  接下来我们先把指针搞出来,我们创建一个处理函数对象,这个对象上会挂载一些方法,用于在diff算法过程中调用,在函数中更新相应的变量。consthandles{更新指针updatePointers(oldStartIdx,oldEndIdx,newStartIdx,newEndIdx){oldPointerList。value〔{name:oldStartIdx,value:oldStartIdx},{name:oldEndIdx,value:oldEndIdx}〕newPointerList。value〔{name:newStartIdx,value:newStartIdx},{name:newEndIdx,value:newEndIdx}〕},}
  然后我们就可以在diff函数中通过handles。updatePointers()更新指针了:constdiff(el,oldChildren,newChildren){指针。。。handles。updatePointers(oldStartIdx,oldEndIdx,newStartIdx,newEndIdx)。。。}
  这样指针就出来了:
  然后在while循环中会不断改变这四个指针,所以在循环中也需要更新:while(oldStartIdxoldEndIdxnewStartIdxnewEndIdx){。。。handles。updatePointers(oldStartIdx,oldEndIdx,newStartIdx,newEndIdx)}
  但是这样显然是不行的,为啥呢,因为循环也就一瞬间就结束了,而我们希望每次都能停留一段时间,很简单,我们写个等待函数:constwaitt{returnnewPromise(resolve{setTimeout((){resolve()},t3000)})}
  然后我们使用asyncawait语法,就可以轻松在循环中实现等待了:constdiffasync(el,oldChildren,newChildren){。。。while(oldStartIdxoldEndIdxnewStartIdxnewEndIdx){。。。handles。updatePointers(oldStartIdx,oldEndIdx,newStartIdx,newEndIdx)awaitwait()}}
  接下来我们新增两个变量,来突出表示当前正在比较的两个VNode:当前比较中的节点索引constcurrentCompareOldNodeIndexref(1)constcurrentCompareNewNodeIndexref(1)consthandles{更新当前比较节点updateCompareNodes(a,b){currentCompareOldNodeIndex。valueacurrentCompareNewNodeIndex。valueb}}{{item?item。children:空}}{{item。children}}
  给当前比较中的节点添加一个类名,用来突出显示,接下来还是一样,需要在diff函数中调用该函数,但是,该怎么加呢:while(oldStartIdxoldEndIdxnewStartIdxnewEndIdx){if。。。}elseif(isSameNode(oldStartVNode,newStartVNode)){。。。oldStartVNodeoldChildren〔oldStartIdx〕newStartVNodenewChildren〔newStartIdx〕}elseif(isSameNode(oldStartVNode,newEndVNode)){。。。oldStartVNodeoldChildren〔oldStartIdx〕newEndVNodenewChildren〔newEndIdx〕}elseif(isSameNode(oldEndVNode,newStartVNode)){。。。oldEndVNodeoldChildren〔oldEndIdx〕newStartVNodenewChildren〔newStartIdx〕}elseif(isSameNode(oldEndVNode,newEndVNode)){。。。oldEndVNodeoldChildren〔oldEndIdx〕newEndVNodenewChildren〔newEndIdx〕}else{。。。newStartVNodenewChildren〔newStartIdx〕}
  我们想表现出头尾比较的过程,其实就在这些if条件中,也就是要在每个if条件中停留一段时间,那么可以直接这样吗:constisSameNodeasync(){。。。handles。updateCompareNodes()awaitwait()}if(awaitisSameNode(oldStartVNode,newStartVNode))
  很遗憾,我尝试了不行,那么只能改写成其他形式了:while(oldStartIdxoldEndIdxnewStartIdxnewEndIdx){letstopfalseletisSameNodefalseif(oldStartVNodenull){callbacks。updateInfo()oldStartVNodeoldChildren〔oldStartIdx〕stoptrue}。。。if(!stop){callbacks。updateInfo(头头比较)callbacks。updateCompareNodes(oldStartIdx,newStartIdx)isSameNodeisSameNode(oldStartVNode,newStartVNode)if(isSameNode){callbacks。updateInfo(key值相同,可以复用,进行patch打补丁操作。新旧节点位置相同,不需要移动对应的真实DOM节点)}awaitwait()}if(!stopisSameNode){。。。oldStartVNodeoldChildren〔oldStartIdx〕newStartVNodenewChildren〔newStartIdx〕stoptrue}。。。}
  我们使用一个变量来表示是否进入到了某个分支,然后把检查节点是否能复用的结果也保存到一个变量上,这样就可以通过不断检查这两个变量的值来判断是否需要进入到后续的比较分支中,这样比较的逻辑就不在if条件中了,就可以使用await了,同时我们还使用updateInfo增加了提示语:consthandles{更新提示信息updateInfo(tip){info。valuetip}}
  接下来看一下节点的移动操作,当头(oldStartIdx对应的oldStartVNode节点)尾(newEndIdx对应的newEndVNode节点)比较发现可以复用时,在打完补丁后需要将oldStartVNode对应的真实DOM元素移动到oldEndVNode对应的真实DOM元素的位置,也就是插入到oldEndVNode对应的真实DOM的后面一个节点的前面:if(!stopisSameNode){头尾patchVNode(oldStartVNode,newEndVNode)把oldStartVNode节点移动到最后el。insertBefore(oldStartVNode。el,oldEndVNode。el。nextSibling)更新指针oldStartVNodeoldChildren〔oldStartIdx〕newEndVNodenewChildren〔newEndIdx〕stoptrue}
  那么我们可以在insertBefore方法移动完真实的DOM元素后紧接着调用一下我们模拟列表的移动节点的方法:if(!stopisSameNode){。。。el。insertBefore(oldStartVNode。el,oldEndVNode。el。nextSibling)callbacks。moveNode(oldStartIdx,oldEndIdx1)。。。}
  我们要操作的实际上是代表真实DOM节点的actNodeList列表,那么关键是要找到具体是哪个,首先头尾的四个节点指针它们表示的是在新旧VNode列表中的位置,所以我们可以根据oldStartIdx和oldEndIdx获取到oldVNodeList中对应位置的VNode,然后通过key值在actNodeList列表中找到对应的节点,进行移动、删除、插入等操作:consthandles{移动节点moveNode(oldIndex,newIndex){letoldVNodeoldVNodeList。value〔oldIndex〕letnewVNodeoldVNodeList。value〔newIndex〕letfromIndexfindIndex(oldVNode)lettoIndexfindIndex(newVNode)actNodeList。value〔fromIndex〕actNodeList。value。splice(toIndex,0,oldVNode)actNodeList。valueactNodeList。value。filter(item{returnitem!})}}constfindIndex(vnode){return!vnode?1:actNodeList。value。findIndex(item{returnitemitem。data。keyvnode。data。key})}
  其他的插入节点和删除节点也是类似的:
  插入节点:consthandles{插入节点insertNode(newVNode,index,inNewVNode){letnode{data:newVNode。data,children:newVNode。text}lettargetIndex0if(index1){actNodeList。value。push(node)}else{if(inNewVNode){letvNodenewVNodeList。value〔index〕targetIndexfindIndex(vNode)}else{letvNodeoldVNodeList。value〔index〕targetIndexfindIndex(vNode)}actNodeList。value。splice(targetIndex,0,node)}}}
  删除节点:consthandles{删除节点removeChild(index){letvNodeoldVNodeList。value〔index〕lettargetIndexfindIndex(vNode)actNodeList。value。splice(targetIndex,1)}}
  这些方法在diff函数中的执行位置其实就是执行insertBefore、removeChild方法的地方,具体可以本文源码,这里就不在具体介绍了。
  另外还可以凸显一下已经结束比较的元素、即将被添加的元素、即将被删除的元素等等,最终效果:
  时间原因,目前只实现了双端diff算法的效果,后续会增加上快速diff算法的动画过程,有兴趣的可以点个关注哟
  仓库:https:github。comwanglin2VNodevisualization。
投诉 评论

周王朝第一辅助,孔子都是他的忠实粉丝为什么孔圣人会成为他的小迷弟?中国的历史发展悠久,封建王朝也一直绵延了数千年。诞生的真正的伟人,其实并不多,而孔子,却被称为文圣。孔子创立了儒家学派,儒家思想在封建……35张怪异的图片展示着神奇的世界胖子与瘦子的X光片对比。每年九月,荷兰布雷达市都会举办世界上最大的红发聚会这一活动被称为红发日这是1938年的Colt38左轮手枪,每次扣动扳机时都会自动拍照……上汽新能源车今年销量将突破百万辆三年内销量占比将翻一番采访对象供图新民晚报讯(记者叶薇)今年11月份,上汽新能源车创下单月销售13万辆的历史新高;111月份,上汽新能源车销量达93万辆,在集团整体销量中的占比接近20。上汽将……湖南省首届非遗博览会现场,手艺人各显其招11月18日,三湘非遗风惊艳全世界湖南省首届非遗博览会在张家界大庸古城开幕,全省各地共有139个非遗项目参展参演。现场有哪些非遗项目呢?11月19日,记者前往探寻了一番。……这8大减寿习惯和长寿习惯,人人都要知道很多疾病与环境及生活方式相关树立健康的生活方式改变不良的生活习惯很多疾病如糖尿病、心脑血管疾病、腰腿病癌症等都是可以预防的!除了暴饮暴食、吸……最高法发布反垄断和反不正当竞争典型案例各10件本报记者张晨为充分发挥典型案例的示范和引领作用,加强反垄断和反不正当竞争司法,最高人民法院在去年9月27日发布10件人民法院反垄断和反不正当竞争典型案例的基础上,今天发布……美国要求日本等国对中国实施半导体出口限制,外交部回应中国青年报客户端北京11月2日电(中青报中青网记者胡文利)据报道,美国政府已经要求日本等国采取措施,对中国实施半导体出口限制。日本政府方面表示,接到美方提议之后,政府内部已经开……十一宅家追剧,这4种零食少不了,解压还解馋,便宜好吃,真不错十一宅家追剧,这4种零食少不了,解压还解馋,便宜好吃,真不错十一假期已经开始了几天,那在这段时间,大家是做了什么呢,和朋友相约出去游玩,还是回家看望父母,亦或者是自己来一……新华社长篇纪实通讯让党旗在新征程上高高飘扬中国共产党章程(修央广网北京10月27日消息据中央广播电视总台中国之声《新闻和报纸摘要》报道,新华社10月26日播发长篇纪实通讯《让党旗在新征程上高高飘扬〈中国共产党章程(修正案)〉诞生记》,全……黄圣依被爆离婚后更新动态,笑容灿烂,心情丝毫没有受影响9月23号,黄圣依和杨子被传婚变,一时间话题上了热搜。原因是两人一个多月前,在七夕节发布的文案,是一首藏头诗,末尾的一句:新开始,让人遐想连篇。当大家期待两人给出一……我让虚拟DOM的diff算法过程动起来了去年写了一篇文章手写一个虚拟DOM库,彻底让你理解diff算法介绍虚拟DOM的patch过程和diff算法过程,当时使用的是双端diff算法,今年看到了Vue3使用的已经是快速……深中14人入选数学奥林匹克省队,29人获得省一等奖深圳中学学子在2022年全国高中数学联赛(广东赛区)中再创佳绩!记者20日从深圳中学获悉,在上述赛事中,全省前十,深中有六人。14人入选2022年中国数学奥林匹克广东省代……
国家药监局专题解读医保药品个人倒卖最高可判无期唐山又爆雷了!女子隔离却被要求吃药拍视频?不吃就曝光?有些人退休金比当地平均工资高很多,有的人却低很多,为什么?关注小鹏汽车全国交付中心超过90家,今年已交付9万多辆车为什么小米手机发热严重,而其它品牌的手机不怎么烫手?腾讯的FIFA足球世界和网易的实况足球哪个好?又一明星嫖娼塌房,为何我国要大力禁娼呢?看看德国就知道了关键时刻,金砖峰会发出北京声音70亩小麦发霉,却不许村民使用收割机,专家用手割更环保有些老人自己有养老金,为什么还要儿女出生活费呢?农村老两口去世了,房子可以过户到儿子吗?山东高速男篮的两位顶薪大侠对球队的贡献有多少?匹配其待遇吗?自己好累好憔悴的说说刺的是别人的眼痛的却是自己的新视野新世界初三作文600字王者荣耀王者生涯奖励称号怎么获得CBA辽宁再胜广厦,大比分30,广厦三少如果没有伤病能否一战国有土地使用权能否转让?再游浮峰次韵为什么不能抠肚脐眼?怎么培养孩子的个性知足吟和崔十八未贫作。切蛋糕用什么样的锯齿刀好一场火灾让大家见识到嫁入百亿豪门的林青霞,过得究竟有多富贵

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找七猫云易事利