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

petite-vue源码分析——v-if和v-for的工作原理

时间:2023-03-31 15:40:35 vue.js

深入v-if的工作原理

人肉单步调试:调用createApp根据入参生成全局作用域rootScope,创建根上下文rootCtx;调用mount为
根块构建根块对象,并以此为模板进行解析处理;解析时识别v-scope属性,根据全局作用域rootScope获取局部作用域scope。子节点的解析和渲染;获取$template属性值并生成HTML元素;子节点的深度优先遍历和解析(调用walkChildren);parsingOFFLINEParsingOFFLINE本书接上篇第一章,我们继续人肉单步调试:识别元素带上v-if属性,调用_if原始指令分析元素和兄弟元素;将带有v-if的元素和后面带有v-else-if和v-else的元素转换为逻辑分支记录;loop遍历分支,为逻辑运算结果为真的分支创建块对象并销毁原分支的块对象(没有原分支的块对象第一次渲染),将渲染任务提交给异步队列//文件./src/walk.ts//为了便于理解,我简化了代码exportconstwalk=(node:Node,ctx:Context):ChildNode|空|void{consttype=node.nodeTypeif(type==1){//节点对于元素类型constel=nodeasElementletexp:string|nullif((exp=checkAttr(el,'v-if'))){return_if(el,exp,ctx)//返回没有`v-else-if`或`v-else`兄弟节点的最新一个}}}//文件./src/directives/if.tsinterface分支{exp?:string|null//该分支的逻辑运算表达式el:Element//该分支对应的模板元素,每次渲染时都会以该元素为模板,通过cloneNode复制一个实例到DOM树中}exportconst_if=(el:元素,exp:字符串,ctx:上下文)=>{constparent=el.parentElement!/*锚元素,由于v-if、v-else-if和v-else标识的元素在某个状态下可能不在DOM树上,*所以插入点由锚元素位置信息标记,当状态改变时,目标元素可以被插入到正确的位置。*/constanchor=newComment('v-if')parent.insertBefore(anchor,el)//逻辑分支,将v-if标识的元素作为第一个分支constbranches:Branch[]=[{exp,el}]/*定位v-else-if和v-else元素并将它们压入逻辑分支*无法控制v-else-if和v-else出现的顺序,所以我们可以这样写**但是效果是变为,最后一个分支永远没有机会匹配。*/letelseEl:元素|nullletelseExp:字符串|nullwhile((elseEl=el.nextElementSibling)){elseExp=nullif(checkAttr(elseEl,'v-else')===''||(elseExp=checkAttr(elseEl,'v-else-if'))){//从在线模板中删除分支节点parent.removeChild(elseEl)branches.push({exp:elseExp,el:elseEl})}else{break}}//保存最新的节点,不带`v-else`和`v-else-if`作为下一轮遍历分析的模板节点constnextNode=el.nextSibling//从在线模板中移除带有`v-if`的节点parent.removeChild(el)letblock:Block|undefined//当前逻辑运算结构为真的分支对象对应的块对象letactiveBranchIndex:number=-1//当前逻辑运算结构为真的分支索引//如果状态发生变化如果分支的索引逻辑操作结构为真变化,需要销毁原分支对应的块对象(包括挂起下监听状态变化的副作用函数、执行指令的清理函数和递归触发子块对象的清理操作)constremoveActiveBlock=()=>{if(block){//重新插入锚元素定位插入点parent.insertBefore(anchor,block.el)block.remove()//解引用被销毁的块对象,让GC回收it对应的JavaScript对象和detached元素block=undefined}}//将渲染任务推送到异步任务对立面,在这一轮EventLoop微Queue执行阶段会执行一次ctx.effect(()=>{for(leti=0;i元素,所以我删减了代码所以我把代码删掉了当前块对象和它的子孙一起被移除this.template.parentNode!.removeChild(this.template)this.teardown()}teardown(){//先递归调用子块对象的清理方法this.ctx.blocks.forEach(child=>{child.teardown()})//包含中止副作用函数监控状态变化this.ctx.effects.forEach(stop)//执行指令的清理函数this.ctx.cleanups.forEach(fn=>fn())}}深入了解v-for的工作原理
人工单步调试:根据调用createApp输入参数生成全局作用域rootScope,创建根上下文rootCtx;调用mount为
构建根块对象rootBlock,并以此为模板进行解析;v-scope在解析Attributes时,根据全局作用域rootScope计算得到局部作用域scope,并根据根上下文rootCtx构建新的上下文ctx,用于子节点的解析和渲染;获取$template属性值并生成HTML元素;深度优先遍历解析子节点(调用walkChildren);parse我是选项之一parseI'mtheoneofoptions从上一本书开始,我们继续人工单步调试:识别带有v-for属性的元素,调用_for原始指令对元素是parsed;通过正则表达式提取v-for中的collection和collection元素的表达式字符串,key的表达式字符串;基于每个collection元素创建一个独立的scope,创建一个独立的block对象渲染元素//File./src/walk.ts//为了便于理解,我简化了代码exportconstwalk=(node:Node,ctx:Context):ChildNode|空|void{consttype=node.nodeTypeif(type==1){//节点是元素类型constel=nodeasElementletexp:string|nullif((exp=checkAttr(el,'v-for'))){return_for(el,exp,ctx)//返回最近的没有`v-else-if`或`v-else`的兄弟节点}}}//file./src/directives/for.ts/*[\s\S]*表示识别空格字符和非空格字符,默认为贪心模式,即`(item,index)invalue`将匹配整个字符串。*改为[\s\S]*?是惰性模式,即`(item,index)invalue`只会匹配`(item,index)`*/constforAliasRE=/([\s\S]*?)\s+(?:in)\s+([\s\S]*?)///用于移除`(item,index)`中的`(`和`)`conststripParentRE=/^\(|\)$/g//匹配`item,index`中的`,index`,然后可以提取value和index独立处理constforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/typeKeyToIndexMap=Map//为了便于理解,我们假设只接受`v-for="valinvalues"`形式,所有输入参数都是有效的,删除了入参有效性和解构等代码exportconst_for=(el:Element,exp:string,ctx:Context)=>{//通过正则表达式提取`in`两边的子表达式字符串在表达式字符串中constinMatch=exp.match(forAliasRE)//保存下一轮遍历分析的模板节点constnextNode=el.nextSibling//插入锚点,移除element与来自DOM树的`v-for`constparent=el.parentElement!constanchor=newText('')parent.insertBefore(anchor,el)parent.removeChild(el)constsourceExp=inMatch[2].trim()//获取`value`in`(item,index)invalue`letvalueExp=inMatch[1].trim().replace(stripParentRE,'').trim()//Get`(item,index)invalue`in`item,index`letindexExp:字符串|未定义letkeyAttr='key'letkeyExp=el.getAttribute(keyAttr)||el.getAttribute(keyAttr=':key')||el.getAttribute(keyAttr='v-bind:key')if(keyExp){el.removeAttribute(keyExp)//将表达式序列化,比如`value`变成`"value"`,这样就不会参与了随后的表达式计算()//获取`item,index`中的itemindexExp=match[1].trim()//获取`item,index`中的index}letmounted=false//false表示第一次渲染,true表示重新渲染-renderingletblocks:Block[]letchildCtxs:Context[]letkeyToIndexMap:KeyToIndexMap//用来记录key和index的关系,当重新渲染发生时,元素会被重用constcreateChildContexts=(source:unknown):[Context[],KeyToIndexMap]=>{constmap:KeyToIndexMap=newMap()constctxs:Context[]=[]if(isArray(source)){for(leti=0;i{constdata:any={}data[valueExp]=valueindexExp&&(data[indexExp]=index)//为每个子项创建一个单独的项elementScopeconstchildCtx=createScopedContext(ctx,data)//key表达式在对应的子元素范围内运行constkey=keyExp?evaluate(childCtx.scope,keyExp):索引map.set(key,index)childCtx.key=keyreturnchildCtx}//为每个子元素创建一个块对象constmountBlock=(ctx:Conext,ref:Node)=>{constblock=newBlock(el,ctx)block.key=ctx.keyblock.insert(parent,ref)returnblock}ctx.effect(()=>{constsource=evaluate(ctx.scope,sourceExp)//计算`(item,index)initems`中items的真实值constprevKeyToIndexMap=keyToIndexMap//生成一个新的作用域并计算`key`、`:key`或`v-bind:key`;[childCtxs,keyToIndexMap]=createChildContexts(source)if(!mounted){//为每个子元素创建块对象,解析子元素的后代插入DOM树blocks=childCtxs.map(s=>mountBlock(s,anchor))mounted=true}//由于我们的示例只研究静态视图,我们稍后会详细了解重新渲染代码})returnnextNode}Summary我们看到生成了v-if和v-for在解析Block对象的过程中,v-if的每个分支对应一个block对象,而v-for表示每个子元素对应一个block对象。事实上,块对象不仅是控制DOM操作的单位,还用来表示树结构中不稳定的部分。比如节点的增删改查会导致树结构不稳定。将这些不稳定的部分打包成独立的块对象,在构造和删除时封装资源回收等操作。这样不仅可以提高代码的可读性,还可以提高程序的运行效率。v-if的第一次渲染和重渲染使用的是同一套逻辑,但是v-for在重渲染的时候会使用key重用元素来提高效率,而且重渲染的时候的算法可以复制很多。在下一篇文章中,我们将详细了解v-for在重新渲染时的工作原理,敬请期待:),也详细分析了响应式系统中使用JS引擎的SMI优化依赖清理算法。绝对是Vue3源码入门前的绝佳敲门砖。喜欢的话记得转发和欣赏哦!