详解Vue3中对VDOM的改进
前言
vue-next对virtualdom的patch更新做了一系列的优化,从编译时加入了block以减少vdom之间的对比次数,另外还有hoisted的操作减少了内存的开销。本文写给自己看,做个知识点记录,如有错误,还请不吝赐教。
VDOM
VDOM的概念简单来说就是用js对象来模拟真实DOM树。由于MV**的架构,真实DOM树应该随着数据(Vue2.x中的data)的改变而发生改变,这些改变可能是以下几个方面:
- v-if
- v-for
- 动态的props(如:class,@click)
- 子节点的改变
- 等等
Vue框架要做的其实很单一:在用户改变数据时,正确更新DOM树,做法就是其核心的VDOM的patch和diff算法。
Vue2.x中的做法
在Vue2.x中,当数据改变后就要对所有的节点进行patch和diff操作。如以下DOM结构:
I'mheader
- 第一个静态li
{{item.desc}}
在第一次mount节点的时候会去生成真实的DOM,此后如果
mutableItems.push({ key:'asdf', desc:'anewliitem' })
预期的结果是页面出现新的一个li元素,内容就是anewliitem,Vue2.x中是通过patch时对ul元素对应的vnode的children来进行diff操作,具体操作在此不深究,但是该操作是需要比较所有的li对应的vnode的。
不足
正是由于2.x版本中的diff操作需要遍历所有元素,本例中包括了span和第一个li元素,但是这两个元素是静态的,不需要被比较的,不论数据怎么变,静态元素都不会再更改了。vue-next在编译时对这种操作做了优化,即Block。
Block
入上述模板,在vue-next中生成的渲染函数为:
const_Vue=Vue const{createVNode:_createVNode}=_Vue const_hoisted_1=_createVNode("span",{class:"header"},"I'mheader",-1/*HOISTED*/) const_hoisted_2=_createVNode("li",null,"第一个静态li",-1/*HOISTED*/) returnfunctionrender(_ctx,_cache){ with(_ctx){ const{createVNode:_createVNode,renderList:_renderList,Fragment:_Fragment,openBlock:_openBlock,createBlock:_createBlock,toDisplayString:_toDisplayString}=_Vue return(_openBlock(),_createBlock(_Fragment,null,[ _hoisted_1, _createVNode("ul",null,[ _hoisted_2, (_openBlock(true),_createBlock(_Fragment,null,_renderList(state.mutableItems,(item)=>{ return(_openBlock(),_createBlock("li",{key:item.key},_toDisplayString(item.desc),1/*TEXT*/)) }),128/*KEYED_FRAGMENT*/)) ]) ],64/*STABLE_FRAGMENT*/)) } }
我们可以看到调用了openBlock和createBlock方法,这两个方法的代码实现也很简单:
constblockStack:(VNode[]|null)[]=[] letcurrentBlock:VNode[]|null=null letshouldTrack=1 //openBlock exportfunctionopenBlock(disableTracking=false){ blockStack.push((currentBlock=disableTracking?null:[])) } exportfunctioncreateBlock( type:VNodeTypes|ClassComponent, props?:{[key:string]:any}|null, children?:any, patchFlag?:number, dynamicProps?:string[] ):VNode{ //avoidablockwithpatchFlagtrackingitself shouldTrack-- constvnode=createVNode(type,props,children,patchFlag,dynamicProps) shouldTrack++ //savecurrentblockchildrenontheblockvnode vnode.dynamicChildren=currentBlock||EMPTY_ARR //closeblock blockStack.pop() currentBlock=blockStack[blockStack.length-1]||null //ablockisalwaysgoingtobepatched,sotrackitasachildofits //parentblock if(currentBlock){ currentBlock.push(vnode) } returnvnode }
更加详细的注释还请看源代码中的注释,写的十分详尽,便于理解。这里面openBlock就是初始化一个块,createBlock就是对当前编译的内容生成一个块,这里面的这一行代码:vnode.dynamicChildren=currentBlock||EMPTY_ARR就是在收集动态的子节点,我们可以再看一下编译时运行的函数:
//createVNode function_createVNode( type:VNodeTypes|ClassComponent, props:(Data&VNodeProps)|null=null, children:unknown=null, patchFlag:number=0, dynamicProps:string[]|null=null ){ /** *一系列代码 **/ //presenceofapatchflagindicatesthisnodeneedspatchingonupdates. //componentnodesalsoshouldalwaysbepatched,becauseevenifthe //componentdoesn'tneedtoupdate,itneedstopersisttheinstanceonto //thenextvnodesothatitcanbeproperlyunmountedlater. if( shouldTrack>0&& currentBlock&& //theEVENTSflagisonlyforhydrationandifitistheonlyflag,the //vnodeshouldnotbeconsidereddynamicduetohandlercaching. patchFlag!==PatchFlags.HYDRATE_EVENTS&& (patchFlag>0|| shapeFlag&ShapeFlags.SUSPENSE|| shapeFlag&ShapeFlags.STATEFUL_COMPONENT|| shapeFlag&ShapeFlags.FUNCTIONAL_COMPONENT) ){ currentBlock.push(vnode) } }
上述函数是在模板编译成ast之后调用的生成VNode的函数,所以有patchFlag这个标志,如果是动态的节点,并且此时是开启了Block的话,就会将节点塞入Block中,这样createBlock返回的VNode中就会有dynamicChildren了。
到此为止,通过本文中案例经过模板编译和render函数运行后并经过了优化以后生成了如下结构的vnode:
constresult={ type:Symbol(Fragment), patchFlag:64, children:[ {type:'span',patchFlag:-1,...}, { type:'ul', patchFlag:0, children:[ {type:'li',patchFlag:-1,...}, { type:Symbol(Fragment), children:[ {type:'li',patchFlag:1...}, {type:'li',patchFlag:1...} ] } ] } ], dynamicChildren:[ { type:Symbol(Fragment), patchFlag:128, children:[ {type:'li',patchFlag:1...}, {type:'li',patchFlag:1...} ] } ] }
以上的result不完整,但是我们暂时只关心这些属性。可以看见result.children的第一个元素是span,patchFlag=-1,且result有一个dynamicChildren数组,里面只包含了两个动态的li,后续如果变动了数据,那么新的vnode.dynamicChildren会有第三个li元素。
patch
patch部分其实也没差多少,就是根据vnode的type执行不同的patch操作:
functionpatchElement(n1,n2){ let{dynamicChildren}=n2 //一系列操作 if(dynamicChildren){ patchBlockChildren( n1.dynamicChildren!, dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG ) }elseif(!optimized){ //fulldiff patchChildren( n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG ) } }
可以看见,如果有了dynamicChildren那么vue2.x版本中的diff操作就被替换成了patchBlockChildren()且参数只有dynamicChildren,就是静态的不做diff操作了,而如果vue-next的patch中没有dynamicChildren,则进行完整的diff操作,入注释写的fulldiff的后续代码。
结尾
本文没有深入讲解代码的实现层面,一是因为自己实力不济还在阅读源码当中,二是我个人认为阅读源码不可钻牛角尖,从大局入眼,再徐徐图之,先明白了各个部分的作用后带着思考去阅读源码能收获到的应该更多一些。
到此这篇关于详解Vue3中对VDOM的改进的文章就介绍到这了,更多相关Vue3VDOM内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!