当前位置: 首页 > Web前端 > JavaScript

【vue3源码】十三、认识Block

时间:2023-03-27 17:38:17 JavaScript

什么是Block?Block是一个特殊的vnode。与普通的vnode相比,它多了一个dynamicChildren属性来存储动态节点。什么是动态节点?观察下面的vnode,children中第一个vnode的children是dynamic的,第二个vnode的class是dynamic的,两个vnode都是动态节点。动态节点会有一个patchFlag属性,用来表示节点的哪些属性是动态的。constvnode={type:'div',children:[{type:'span',children:ctx.foo,patchFlag:PatchFlags.TEXT},{type:'span',children:'foo',props:{类:normalizeClass(cls)},patchFlag:PatchFlags.CLASS},{type:'span',children:'foo'}]}作为一个Block,它会将它所有的子动态节点收集到dynamicChildren中(childrenofchildren动态元素是也收集到dynamicChildren中)。constvnode={type:'div',children:[{type:'span',children:ctx.foo,patchFlag:PatchFlags.TEXT},{type:'span',children:'foo',props:{类:normalizeClass(cls)},patchFlag:PatchFlags.CLASS},{type:'span',children:'foo'}],dynamicChildren:[{type:'span',children:ctx.foo,patchFlag:PatchFlags.TEXT},{type:'span',children:'foo',props:{class:normalizeClass(cls)},patchFlag:PatchFlags.CLASS}]}哪些节点会被用作Block?模板中的根节点,带有v-for、v-if/v-else-if/v-else的节点将作为一个块。下面的例子:SFCPlaygrounddynamicChildren的集合观察tempalte编译后的代码,你会发现在创建Block之前会执行一个openBlock函数。//一个块栈用来存储exportconstblockStack:(VNode[]|null)[]=[]//一个数组用来存储动态节点,最终会赋值给dynamicChildrenexportletcurrentBlock:VNode[]|null=nullexportfunctionopenBlock(disableTracking=false){blockStack.push((currentBlock=disableTracking?null:[]))}在openBlock中,如果disableTracking为true,currentBlock将被设置为null;否则,创建一个新数组并将其分配给currentBlock,并推送到blockStack。再看createBlock,createBlock调用了一个setupBlock方法。导出函数createBlock(type:VNodeTypes|ClassComponent,props?:Record|null,children?:any,patchFlag?:number,dynamicProps?:string[]):VNode{returnsetupBlock(createVNode(type,props,children,patchFlag,dynamicProps,true/*isBlock:防止块跟踪自身*/))}setupBlock接收一个vnode参数。functionsetupBlock(vnode:VNode){//当isBlockTreeEnabled>0时,将currentBlock分配给vnode.dynamicChildren//否则设置为nullvnode.dynamicChildren=isBlockTreeEnabled>0?当前块||(EMPTY_ARRasany):null//关闭块closeBlock()//父块收集子块//如果isBlockTreeEnabled>0,并且currentBlock不为??空,将vnode放入currentBlockif(isBlockTreeEnabled>0&¤tBlock){currentBlock.push(vnode)}//returnvnodereturnvnode}closeBlock:exportfunctioncloseBlock(){//弹出栈顶块blockStack.pop()//设置currentBlock为父块currentBlock=blockStack[blockStack.length-1]||null}在了解dynamicChildren的收集过程之前,我们首先要了解嵌套vnode的创建顺序是从内向外执行的。如:exportdefaultdefineComponent({render(){returncreateVNode('div',null,[createVNode('ul',null,[createVNode('li',null,[createVNode('span',null,'foo')])])])}})vnode的创建过程是:span->li->ul->div。每次创建Block之前,都需要调用openBlock创建一个新数组,赋值给currentBlock,放在blockStack栈顶。然后调用createBlock。在createBlock中,首先会创建vnode,并将vnode作为参数传递给setupBlock。创建vnode时,如果满足一定的条件,vnode会被收集到currentBlock中。//将当前动态节点收集到currentBlockif(isBlockTreeEnabled>0&&//避免自己收集!isBlockNode&&//有父块currentBlock&&//vnode.patchFlag需要大于0或者ShapeFlags中存在ShapeFlag。COMPONENT//patchFlag的存在表明该节点需要被打补丁和更新。//组件节点也应该一直被打补丁,因为即使组件不需要更新,它也需要将实例持久化到下一个vnode以便以后可以正确卸载(vnode.patchFlag>0||shapeFlag&ShapeFlags.COMPONENT)&&vnode.patchFlag!==PatchFlags.HYDRATE_EVENTS){currentBlock.push(vnode)}然后在setupBlock中,将currentBlock赋值给vnode.dynamicChildren属性,然后调用closeBlock关闭块(弹出blockStack栈顶元素,currentBlock执行blockStack的最后一个元素,也就是刚刚弹出的块的父块),then将vnode收集到父块中。例子为了阐明dynamicChildren的收集过程,我们通过一个例子继续分析。在上面的例子中,编译器编译后生成的代码是如下。SFCPlaygroundimport{renderListas_renderList,Fragmentas_Fragment,openBlockas_openBlock,createElementBlockas_createElementBlock,toDisplayStringas_toDisplayString,resolveComponentas_resolveComponent,createVNodeas_createVNode}from"vue"import{ref,reactive}from'vue'const__sfc__={__name:'App',setup(__props){constdata=reactive([1,2,3])constcount=ref(0)return(_ctx,_cache)=>{const_component_ComA=_resolveComponent("ComA")return(_openBlock(),_createElementBlock("div",null,[(_openBlock(true),_createElementBlock(_Fragment,null,_renderList(data,(item)=>{return(_openBlock(),_createElementBlock("span",null,_toDisplayString(item),1/*TEXT*/))}),256/*UNKEYED_FRAGMENT*/)),_createVNode(_component_ComA,{count:count.value},null,8/*道具*/,["count"])]))}}}__sfc__.__file="App.vue"exportdefault__sfc__当渲染函数(这里的渲染函数是setup的返回值)执行时,其执行流程如下:执行_openBlock()创建一个新数组(称为div-block),并压入blockStack栈顶执行_openBlock(true)。由于参数为true,所以不会创建新数组,而是将null赋值给currentBlock,并压入blockStack栈顶执行_renderList,_renderList会遍历数据,执行第二个renderItem参数,即(item)=>{...}第一项为1,执行renderItem,执行_openBlock()创建一个新数组(称为span1-block),并压入blockStack栈顶。此时blockStack和currentBlock的状态如下:然后执行_createElementBlock("span",null,_toDisplayString(item),1/*TEXT*/),在_createElementBlock中,首先调用createBaseVNode创建一个vnode,创建的时候avnode因为这是一个blockvnode(isBlockNode参数为true),所以不会被收集到currentBlock中。创建vnode后,执行setupBlock,将currentBlock赋值给vnode.dynamicChildren。执行closeBlock(),弹出blcokStack的栈顶元素,将currentBlock指向blcokStack的最后一个元素。如下图所示:由于此时currentBlock为null,所以跳过了currentBlock.push(vnode)。当item=2和item=3时,过程同步骤4-7。当item=3时,block被创建后的状态如下:此时,列表被渲染,然后调用_createElementBlock(_Fragment)。_createElementBlock执行过程中,由于isBlockNode参数为true,currentBlock为null,不会被currentBlock收集到执行setupBlock,将EMPTY_ARR(空数组)赋值给vnode.dynamicChildren,调用closeBlock()弹出最顶层元素堆栈,以便currentBlcok指向最新的堆栈顶部元素。由于此时currentBlock不为null,执行currentBlock.push(vnode),执行_createVNode(_component_ComA)。在创建vnode的过程中,由于vnode.patchFlag===PatchFlag.PROPS,vnode会被添加到currentBlock中。执行_createElementBlock('div')。先创建vnode,因为isBlockNode为true,所以不会收集到currentBlock中。执行setupBlock()并将currentBlock分配给vnode.dynamicChildren。然后执行closeBlock()弹出栈顶元素。此时blockStack的长度为0,所以currentBlock会指向null,最终生成的vnode:{type:"div",children:[{type:Fragment,children:[{type:"span",children:“1”,patchFlag:PatchFlag.TEXT,dynamicChildren:[],},{type:“span”,children:“2”,patchFlag:PatchFlag.TEXT,dynamicChildren:[],},{type:“span”,children:"3",patchFlag:PatchFlag.TEXT,dynamicChildren:[],}],patchFlag:PatchFlag.UNKEYED_FRAGMENT,dynamicChildren:[]},{type:ComA,children:null,patchFlag:PatchFlag.PROPS,dynamicChildren:null}],patchFlag:0,dynamicChildren:[{type:Fragment,children:[{type:"span",孩子们:“1”,patchFlag:PatchFlag.TEXT,dynamicChildren:[],},{type:“span”,children:“2”,patchFlag:PatchFlag.TEXT,dynamicChildren:[],},{type:“span”",children:"3",patchFlag:PatchFlag.TEXT,dynamicChildren:[],}],patchFlag:PatchFlag.UNKEYED_FRAGMENT,dynamicChildren:[]},{type:ComA,children:null,patchFlag:PatchFlag.PROPS,dynamicChildren:null}]}Block的作用如果了解Diff过程,应该知道在Diff过程中,即使vnode没有变化,也会进行一次比较,Block的出现减少了这种不必要的比较,因为在Block中所有的动态节点都会被收集到dynamicChildren中,所以Block之间的patch可以直接比较dynamicChildren中的节点,减少非动态节点之间的比较。在块之间打补丁时,会调用patchBlockChildren方法来打补丁dynamicChildren。constpatchElement=(n1:VNode,n2:VNode,parentComponent:ComponentInternalInstance|null,parentSuspense:SuspenseBoundary|null,isSVG:boolean,slotScopeIds:string[]|null,optimized:boolean)=>{//...让{patchFlag,dynamicChildren,dirs}=n2if(dynamicChildren){patchBlockChildren(n1.dynamicChildren!,dynamicChildren,el,parentComponent,parentSuspense,areChildrenSVG,slotScopeIds)if(__DEV__&&parentComponent&&parentComponent.type,__hmrse)}}elseif(!optimized){patchChildren(n1,n2,el,null,parentComponent,parentSuspense,areChildrenSVG,slotScopeIds,false)}//...}如果patchElement中的新节点有dynamicChildren,说明此时的新节点是一个Block,然后调用patchBlockChildren方法来patchdynamicChildren;否则,如果optimized为false,将调用patchChildren,并且可以在patchChildren中调用patchKeyedChildren/patchUnkeyedChildrenDiffconstpatchBlockChildren:PatchBlockChildrenFn=(oldChildren,newChildren,fallbackContainer,parentComponent,parentSuspense,isSVG,slotScopeIds)=>{for(leti=0;i