坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.1.4

前言

part2我们来分析插件和gencode的部分

另外如果没看过parse以及part1部分的可以去看下

坏蛋Dan:vue/compiler-core源码分析学习--day2: parse部分

坏蛋Dan:vue/compiler-core源码分析学习--day3: compile部分part1


nodeTransforms

有些东西指令的加工或者二次加工我们在compiler-dom/compiler-sfc上面说过了,所以这里就不说跟着options传进来的了。

接下来的分析是跟着执行顺序以及getBaseTransformPreset这个方法来的。

另外也会放一些通用函数代码

transformOnce

看名字就是用来处理.once修饰符的场景

  • findDir:这个就不看代码了,就是用来找到该节点某个指令用的。
  • inVOnce:这个参考inVPre。
  • SET_BLOCK_TRACKING:setBlockTracking。

这个方法有些没头没脑的,没办法需要和runtime搭配才行。如果发现.once这个修饰符,就将setBlockTracking这个辅助函数放入helper中。注意这里把inVOnce置为true。

然后返回一个回调,这个回调会在traverseNode方法的最后执行,不用担心isVOnce标志位被置为false,在执行的这个回调的时候子节点已经递归处理完毕了。

  • cache方法来细嗦下

返回一个节点,类型是JS_CACHE_EXPRESSION

这个节点会作为当前节点的codegenNode, 注意这里的curr指的是当前的节点,也不用担心被切换到其它子节点去了,前面强调了很多次回调执行的时间,这个时候context.currentNode已经确保指回当前的节点了。


transformIf

transformIf是一个高阶函数,通过传入一个函数给一个函数并最终返回一个函数。

  • createStructuralDirectiveTransform这个方法下面单独开了个小标题分析了,作为一个高阶函数,返回一个封装了的函数。

在这里这个返回的函数就是transformIf。

我们传入的正则是/^(if|else|else-if)$/,表示如果prop匹配到if分支就命中这个传入的回调。

  • processIf

代码过长。。。现在有些后悔不拆分为几个主题分几篇文章写了。。。。

跑题了,回到代码中。

这货也是接受一个函数作为参数,不过它并没有返回一个函数,不过很不巧的是它的这个函数参数返回了一个函数,所以这货本质也是一个高阶函数。

而这个函数参数返回的函数最终是作为createStructuralDirectiveTransform这个方法收集到的方法并返回给traverseNode方法中。

这中间有好几层套娃,在开始分析这个函数和它的参数之前,我们来缕清这之间的套娃关系。

直接画图吧。。。。

那么在理解这些函数之间的关系之后我们再回来分析代码就会简单很多。

回到processIf代码中

  • dir.name:为啥这里会是else而不是v-else呢?上篇文章中讲过,在parse的过程中把v-xxx、:xxx以及@都处理掉了,v-xxx会变成xxx,:会变成bind 而@会变成on等。
  • dir.arg:这个之前分析compiler-dom的时候也有说过,比如v-bind:xxx="aaa",这个xxx就是这个dir.arg,而这个aaa则是dir.exp。
  • createIfBranch:来看下代码

这个时候的tagType == TEMPLATE就派上用场了,之前parse过滤TEMPLATE的时候是不会过滤IF/FOR/SLOT等场景的。

创建了一个IF_BRANCH类型的节点

而findProp就不看了,就是用来找它的某个静态属性的,在这里用来找key

回到processIf中

如果不是v-else的if指令但是没有表达式,直接报错。老规矩报错的代码咱就不看了。

接着如果context.prefixIdentifiers是true并且这个if指令有表达式也就是exp的话就提前处理这个表达式,为什么是提前呢?因为if指令的处理是在表达式的处理之前

接着如果是v-if指令则创建一个IF_BRANCH类型的节点,而这个branch节点则作为这个IF节点的子节点。

  • context.replaceNode我们来嗦下,代码就不贴出来了,就是将context.currentNode以及parent.childrenindex也就是节点当前的位置替换成另一个节点,这也就是为什么之后traverseNode之后需要把context.currentNode重置回去的原因,这中间有可能存在被删除/替换等问题。
  • processCodegen:就是processIf接收的回调,这里先不看那块代码,先继续往下分析。

如果不是v-if指令,那就有可能是v-else以及v-else-if。

在处理v-else/else-if之前,还需要处理这个v-else/else-if节点之前的注释节点,都给它们移除先,为什么要这么做呢?因为if branch需要搭配注释节点,如果if条件是false,那么它就会被一个注释节点替代,当它变成true的时候它又会替换回来,当然你的注释都给你留着,不会做删掉你代码的行为的。另外没有用的节点也都去掉,比如空文本节点。

接着是判断这个else-if/else的节点之前是否存在v-if或者v-else-if节点,为什么要这么做呢?因为v-if和v-else-if/else之间是没有联系的,在ast中它们就是独立的节点。所以这个时候发现这是个v-else-if/v-else的节点之后,它得先回去看下是否存在v-if/v-else-if分支才行。

注意这里判断的时候是判断前一个节点里的branches的最后一个节点,为什么呢?因为如果这是个符合要求的if branch,那么它就会被移动到node.branches里面去,这样就能建立联系了。

这里还有一个fix,当transition组件里的第一个节点使用if指令的话,就得忽略掉全部注释,具体原因可以看注释里的issue。除transition之外的就把注释节点放到它的children里面,也就是说原本和注释同级的关系,现在变成了父子关系,这样也保留了注释的位置。

这里还有一个对key属性的处理,你有可能把for block的标志位key放到了一个if branch节点上,如果每个节点都一样的话是有问题的,因为vue会尽可能的复用每一个dom,如果你这俩key一样,当条件变了之后,这个节点和它的子节点们会被会复用,那么你这个if条件就有问题了,当然,如果tag不同应该是可以避免这个问题的。

最后再执行传入的回调,当然同时还得traverseNode一遍这个节点的子节点,为什么呢?因为这个if节点已经被移动到IF类型的节点的 branches里面去了,所以它的子节点不手动调用traverseNode的话是不会被遍历处理的。

注意最后这里把原来应该返回出去的回调给执行了,这个点和v-if分支的不同。

接下来我们来看下这个回调做了什么。

这个函数的调用位置是在节点被移除之后。

  • createCodegenNodeForBranch:

看上面的图,这个函数中返回的回调是在traverseNode函数的最后调用的,这就意味着这个时候的ast节点们都已经有了codegenNode了,也就是经过nodeTransforms里注册的插件们的摧残了。

开始先是找到这个IF_BRANCH节点的父节点也就是IF类型的节点的位置,然后把它之前的IF节点的IF_BRANCH分支的节点的个数都加起来作为这个IF类型的节点包括它branch分支里的IF_BRANCH节点的key值,为什么要这么做呢?我们先往下看。

  • condition:是经过加工处理后的exp,还记得我们强调了好几次的回调执行位置吗?它是在节点被nodeTransforms里的插件们都折磨之后才执行的,所以这个时候自然也执行过了transformExpression这个插件,所以dir.exp在if指令的场景下变成了condition这个字段。
  • CREATE_COMMENT:辅助函数createCommentVNode,看到没,果然和注释节点勾搭上了。
  • createCallExpression:创建一个函数调用节点,返回一个JS_CALL_EXPRESSION类型的节点,注意这个节点是在codegen之后才会有的,callee是上面的createCommentVNode辅助函数。
  • createObjectProperty:这个就不看代码了,返回一个对象属性表达式,也就是{ xx: aa }的xx: aa节点
  • createSimpleExpression:这个也不看代码了,就是创建一个单一表达式节点作为上面对象属性节点的value,它的content就是之前遍历keyIndex也就是当前if分支的第几个。
  • createChildrenCodegenNode这个方法很明显是用来给branch自身创建codegenNode的。
  • injectProp:这个方法一看就是用来把prop注入到某个子节点里的。这个方法里做了什么我们先不看了。。又是一个上百行的函数。

注意这个createChildrenCodegenNode处理的是这个branch的children也就是子节点而不是branches。

还记得之前说过的v-if/v-for等都是会创建一个block的,这里显然就是创建一个block,如果这个branch的子节点(注意是子节点而不是所有子孙节点)不止一个,那就得创建一个fragement来包裹它们,当然,如果第一个节点不是ELEMENT的话那也得用fragement包裹它们,因为它可能是FOR等会创建一个block的类型。

如果节点数只有一个并且第一个子节点是FOR类型的,那直接复用同一个fragement就行了。但是如果节点数不止一个,也就是说在FOR节点下面还跟着一些同级节点,这个时候自然就不能复用同一个fragement了,自然需要重新创建一个fragement。

现在我们再来看下这个injectProp。

这个方法一看就是用来将其中一个节点的属性注入到另一个节点里面

  • getUnnormalizedProps:这个方法的代码我们就不看了,简单地说就是如果你这个props是一个JS_CALL_EXPRESSION,那么就会递归找这个callee的arguments0,看是否是规范的并且存储这个prop的递归路径最后返回这个prop和callPath。
  • createObjectExpression:这个方法的代码我们也不看了,就是创建一个js对象节点,prop是上面创建的对象属性节点,key是key,value是上面获取到的keyIndex,现在合并到这个对象节点中,注意这个对象节点是一个字面量节点,比如prop="{ a: 123 }",这个{ a: 123 }就是一个字面量对象。
  • createCallExpression:不多说。

由于这个方法中的场景我们并不清楚,比如这个props出现的条件,我这里思考的几种场景比如for的目标是一个scope variable等都是undefined,所以我这里也只能简单的猜测下发生了什么事。

  1. 首先是获取对应需要合并的属性
  2. 注入属性:
  • 如果props是一个函数调用节点,那么就注入到这个节点的第一个参数中,前提是这个参数是一个对象字面量。
  • 如果props是一个函数调用节点,它的callee是一个名为toHandlers的辅助函数,这个时候就需要创建一个新的节点并加入辅助函数mergeProps,这样做可以避免覆盖该注入节点原有的key属性。
  • 如果这个节点的props自身就是一个对象字面量,那么这个就判断是否存在key这个属性,没有就加上,有就算逑。
  • 剩余的场景也是直接同上第二点,如果遇到了嵌套辅助函数调用,比如normalizeProps(guardReactiveProps(props)),会被重写为normalizeProps(mergeProps({ key: 0 }, props))。

那么这个指令的处理我们终于分析完了,我们来简单的总结下

  1. 将所有的IF_BRANCH节点关联起来(指同一if逻辑块的,并非全部,同层节点可能存在多个if逻辑块),move到一个IF类型的节点的branch中。注意这里是move而不是copy。
  2. 给这个IF_BRANCH节点生成codegenNode节点。
  3. 给这个IF节点生成codegenNode节点。

最后来看下render function

补充: 1. 貌似这个keyIndex是用来干啥的还是不清楚,其实这个key是每个节点都会有的,到时候runtime的diff操作就需要这个key来判断是否需要re-render。而这个key代表着这个节点的位置。


transformMemo

在看代码之前,我们先来看下/熟悉下v-memo的用处 Built-in Directives | Vue.js (vuejs.org)

这个方法的逻辑没啥好说的,就是创建一个函数调用表达式节点,辅助函数是withMemo。直接来看render function舒服一点

没啥好说的,就是做了cache缓存处理


transformFor

transformFor的执行流程和v-if类似的,所以图我就不放了,参考transformIf里的即可。

既然是类似的,我这里就不放代码了,到时候接触到再放出来。

我们先来看下processFor的代码

  • parseForExpression:想了下,还是不要看代码了,后面的其他函数的也一样,由于没有拆成几篇文章来写,现在有些太大了,控制不住。。。。简单的说就是处理v-for="xx"的xx。我们直接来看下数据

不过有几个地方需要注意。

  1. 正则,这个正则可能有公司笔试题会出,貌似阿里就出过手写解析v-for的笔试题。

/(\s\S?)\s+(?:in|of)\s+(\s\S)/

  1. 获取(xx, index) in/of aaaa里的()里的内容的表达式:/^(|)$/g。

  1. 匹配这个()里的数据的index/key正则:/,(^,}])(?:,(^,}]))?$/。

当迭代的是一个object,那么你可以传入三个参数,比如

回到processFor里面

  • addIdentifiers:这个方法这里就不贴代码了,代码在createTransformContext方法里。简单的说就是将这个变量也就是indetifier存放到context里面,如果已经有了就自加。
  • removeIdentifiers:同上,把这个identifier从这个context中移除。

这俩方法是用来处理scope variable的,当进入这个block的scope的时候就add,当出了就remove。

  • replaceNode:这个之前说过了,这里再简单说下,将context.currentNode替换成当前的node。
  • scopes:之前有说过,就是会生成scope的几个指令的个数记录

简单的说下这个processFor方法,重点是parseForExpression,解析出for表达式的数据,然后将它们add到context里的,因为从这里开始已经进入了for的scope里。然后执行回调参数,返回一个函数,这个函数也是和processIf的一样,返回的这个回调会在这个节点被处理完之后再执行,所以这个时候已经退出scope了,自然就得将它们remove。

那么现在来看下传入processFor的回调

在这之前,需要注意的一点是这个函数参数执行的位置并不在processFor的返回函数中,所以它的执行是在transformFor这个插件的处理过程中就执行的。

  • isStableFragement:v-for="xxx"中的这个xxx就是source,如果不是http://ctx.xxx,那就是stable。
  • fragementFlag:也就是即将创建的fragement的nodeflag,前面说过这里的v-for也是会创建fragement的。

这里有个地方需要注意:前面说了这个函数参数是在插件执行过程中执行的,但是这个函数参数的返回函数却是在所有插件执行之后才执行的,也就是过了codegen阶段。

  • isSlotOut:这个方法是确定这个节点是否用了slot,比如或者这两种情况都是。
  • injectProp:这个方法上面有分析了,这里再简单说下就是将父节点的属性注入到子节点里。在这里则是场景中将template的属性注入到slot里。
  • createForLoopParams:代码就不看了,简单地说就是创建一个params数组,囊括for的key、index、value以及memo自己就有的_cache,而这些params将作为renderList的参数。
  • renderExp:这个就是renderList的函数调用表达式。

简单的总结下:

这个函数参数中实际上是在创建v-for的codegenNode。

有几个特殊场景需要说下:

  1. 搭配的场景需要注入给
  2. 如果for包裹的子节点存在多个,那就需要创建fragement
  3. 如果非上面的场景,那就是常规场景:
  • 如果这个v-for="xxx"的xxx是稳定的(等级高于NOT_CONSTANT)单一表达式节点,那么就创建VNode
  • 而如果等级是NOT_CONSTANT,那么就是创建一个block。

另外这里还有兼容v-memo的场景。最后生成的render function其实前面分析v-memo的时候你已经看过了,这里再贴一次。

补充:vForNode搭配的辅助函数是renderList


transformFilter

因为篇幅和兼容性问题(filter在3.x中废弃),这里咱就不分析了,感兴趣的大佬可以自行去了解


trackVForSlotScopes

这个插件处理的场景比较特殊

先来看下/复习下这个v-slot属性 Built-in Directives | Vue.js (vuejs.org)

然后我们来复现下场景

这个场景需要一个for和v-slot同时存在一个template节点上

  • parseForExpression:这个我们前面说过了是用来解析v-for的表达式的。

这个插件很简单,就是这个特殊场景给它加上scope variable。

在出scope的时候再移除这些scope variable。具体为啥要这么做呢?我们后面分析vSlot的时候会分析到,这里先mark下。


transformExpression

这个方法也是相当的简单(不包括processExpression,它里面的东西很多很杂)

就是将这个节点的所有涉及到表达式的内容转换成节点,比如指令的表达式,null语法中的表达式,其中v-on & v-for的场景需要额外插件处理,比较特殊。

这里需要注意的一点是slot的会被转换成一个函数参数节点。


transformSlotOutlet

  • processSlotOutlet:我们先不看这个方法代码,这里看名字可以指导师在处理v-slot的表达式。
  • RENDER_SLOT:renderSlot,slot tag搭配的辅助函数

注意这个slotoutlet处理的是而不是v-slot。

然后我们来看下processSlotOutlet的代码

这个方法也没啥好说的,收集prop,找到name字段,然后返回。


transformElement

这个插件是重点插件

  • resolveComponentType:代码分析放下面,这里简单地说就是确定这个组件的具体类型,比如内置组件,自定义组件等。
  • isDynamicComponent:是否是动态组件。
  • shouldUseBlock:是否需要开启block包裹,如果是动态组件/TELEPORT/SUSPENSE以及svg/foreignObject这几种情况都是需要开启的。
  • buildProps这个方法代码过长,所以这里不贴代码,分析也放到下面去,不然篇幅又不够了,这里简单地说就是处理节点的prop,给prop分门别类,比如directivesprops等。这里的props就是之前我们埋的一个坑的来源。
  • buildDirectiveArgs:这个方法也放到下面分析,看名字就知道是给directivearg创建节点的。
  • buildSlots:同buildProps
  • stringifyDynamicPropNames:这个方法也不看代码了,就是将动态的属性收集然后字符串化,比如'[xxxx, xxx]'
  • createVNodeCall:这回我们倒是要看一下了,因为现在是codegenNode的核心

代码没什么内容,但是还是要贴出来,有个意识,知道它里面有哪些东西,这些东西代表着什么

这插件只处理元素和组件节点。

先是处理props,收集directives/dynamicPropNames/props(attribute)

如果存在子节点,那么就有下面几种场景:

  1. 如果是组件并且组件存在子节点,那么这个组件就应该被视作一个v-slot的节点。
  2. 不是上面的情况那就是元素节点,如果是只有一个子节点并且不是teleport这个内置组件,那么就判断这个子节点是否是一个动态的文本节点({{}}或者复合表达式比如多个表达式之类的),如果确实是并且它的flagNOT_CONSTANT,那么当前我们要处理的这个元素节点的flag就标记为PatchFlags.TEXT,这类型看注释是Indicates an element with dynamic textContent (children fast path)。也就是声明这个元素节点是一个带有动态文本内容的节点。
  3. 其他。

根据上面三种情况收集子节点。

然后收集这个节点的flag

最后创建codegenNode

补充:

这里有一点需要注意,那就是这个transformElement插件的执行的时间点,它也是作为回调执行的,所以也是在节点被nodeTransforms里注册的插件遍历完之后再处理的,不过这个不是重点。

还记得之前我们说到过的这些回调的调用顺序是逆向的,就像是调用栈一样,所以transformElement插件的回调实际上执行的时间是比transformIftransformFor早的,这也就是为什么那俩插件可以判断codegenNode了。

  • isComponentTag:不用看代码了,判断tag是否是component/Component
  • RESOLVE_DYNAMIC_COMPONENT:也就是辅助函数resolve_dynamic_component

组件有以下几种场景

  1. 动态组件,动态组件有两种,一种是大家熟悉的component tag搭配is属性;第二种则是3.x中支持的原生元素也可以用is属性,但是is属性的值的表达式要用vue:开头。
  2. 内置组件:Teleport、Transition、KeepAlive、Suspense等。
  3. 开发者们自身的组件(setup bindings应该是inline mode场景的,还有一个就是常规的)
  4. 自引用组件,也就是自己引用自己

注意这里的内置组件代码中有一段if (!ssr) context.helper(builtIn),这也就是为什么我们不需要手动导入这些内置组件就能直接使用的原因。

  • resolveSetupReference:这个方法就不看了,是inline mode场景的。

buildProps

我这里说一下里面做了什么

  1. 处理type == NodeTypes.ATTRIBUTE,也就是静态属性的场景:
  • 如果是ref并且是在v-forscope里面的,这个时候就需要创建一个keycontentref_for的对象属性节点,将它pushproperties数组里。
  • 如果这个属性是is搭配tagcomponent或者它的expvue:开头的,那么它就是一个动态组件,应该跳过,因为这里是处理静态的。这里你应该会奇怪为什么这个的type是一个ATTRIBUTE,因为这个属性没有1用:或者v-bind开头,所以在最开始分配的时候就把它分配为静态的了。所以我们写代码的时候动态的就尽量用:或者v-bind,这样在parse阶段就可以分辨出来了。
  • 其它的就都算是静态的了,即使你这个属性是不认识的
  1. 静态的结束了,就该轮到指令了
    • v-slot如果没有搭配component,那么直接报错v-slot can only be used on components or <template> tags.,然后跳过
    • 然后绕过v-once/v-memo,因为已经有插件来处理它们了。
    • 如果是动态的key以及vue:before-update这俩,那就需要开启block
    • 动态的refv-forblock里面也是得创建keycontentres_for的对象属性节点。
    • 如果是v-bind或者v-on,但是它没有arg节点,也就是v-on:xx="xxxx"xx。实际上这是一种特殊的写法,v-bindexp是一个对象,这个时候是可以的,比如:v-bind="{ key: 123 }"。在2.x的时候,这种方式是不会覆盖原来已有属性的,而3.x则相反,后面的会覆盖前面的。具体请看:v-bind Merge Behavior | Vue 3 Migration Guide (vuejs.org)。而v-on也是可以传入对象来绑定事件的,会创建一个JS_CALL_EXPRESSION的节点,辅助函数是toHandlers,这个方法是不是很眼熟?其实我们之前分析injectProp也就是注入属性的时候,就遇到了这个问题。结合当时分析的内容,我们可以确定这里的props以及toHandlers就是上边遇到的那俩。由于我们是动态传入一个对象,所以这个对象只能是在runtime的时候才能知道是什么东西,所以合并属性的过程自然就只能在runtime阶段。这也就是为什么需要再包裹一层mergeProps的辅助函数。
    • 然后调用context.directiveTransforms里注册的插件来处理这些指令,这里就暂时不分析插件里做了什么,我们继续先往下看。然后收集这些处理之后的指令。
    • 如果是自定义指令,那么直接放到runtimeDirectives中,因为编译器压根不知道是啥,得等到runtime阶段才知道,并且为了安全,直接开block,避免有涉及到before-update的调用相关的。

如果存在v-bind="object"/v-on="object"的就合并props,当然是在runtime阶段,辅助函数是mergeProps

接着是根据这些个属性/指令等来进一步确定这个节点的flag

  • 如果有使用v-bind/on="object",那么就当做FULL_PROPS,来看下这个类型的注释When keys change, a full diff is always needed to remove the old key.,也就是说patch阶段没办法绕过它的属性,diff需要全量。另外它和CLASS、PROPS、STYLE互斥。
  • 没有动态key:1. 不是组件但是有动态class绑定,这个时候flag类型是CLASS;2. 不是组件但是有动态样式绑定,flagSTYLE,这个类型有部分是可以hoist的,比如:style="{ color: 'red' }"就没有动态的值,这个时候可以staticHoist;3. 有部分属性是动态的,flag类型是PROPS,这个时候之前收集的动态prop就很有用了,到时候patch时不需要diff所有属性,只需要diff这些个动态的即可;4. 最后一种和事件有关,标记为HYDRATE_EVENTS,也就是混合事件。

如果不需要开启block && 没有flag或者flagHYDRATE_EVENTS && 有ref属性或者有生命周期监听的hook(比如@vue:updated)或者存在动态的指令需要在runtime确定的,这个时候标记为NEED_PATCH,来看下这个类型的注释:Indicates an element that only needs non-props patching, e.g. ref or directives (onVnodeXXX hooks). since every patched vnode checks for refs and onVnodeXXX hooks, it simply marks the vnode so that a parent block will track it.,也就是到时候patch只关注non-props比如事件什么的就行了,而不是跟踪所有的属性。

现在我们分门别类好了,但是还有些地方需要深入处理,比如:style="{ xx: xx }",这个动态style的表达式也就是exp里面的字段就有可能是一个动态的,如果有,那就说明得到runtime去处理了。CLASS需要创建一个函数调用表达式节点,辅助函数是NORMALIZE_CLASS。同理STYLE的需要NORMALIZE_STYLE。如果你这个元素/组件节点有动态key,不好意思,上面的判断style/class的就不需要了,反正跑不掉,直接NORMALIZE_PROPS乱棍打死。

最后返回

  • props: propsExpression,属性表达式节点,正常情况下的事件/属性都放到这里面
  • directivesruntimeDirectives,自定义指令数组
  • patchFlag:不多说
  • dynamicPropNames:动态key数组
  • shouldUseBlock:指的是你这个节点是否需要开启block,比如有用到@vue:before-update的监听子组件生命周期的情况。

这货代码咱也不看了,这里说下发生了什么。由于这货比较特殊,所以它不是一个directiveTreansform插件,在buildProps处理完之后才会处理。

v-slot这玩意儿只会用在component里面。

上来直接把withCtx这个辅助函数存到context里。

如果这个v-slot是在另一个v-slot或者v-forscope里面的话,并且只有在这个v-slot中使用了scope variable的情况下才会被定义为dynamic,除此之外则是static的(前提是prefixIdentifiers: true)。

v-slot仅能用于template或者component这两种标签上面:

  1. 如果是放到component上面,会创建一个对象属性节点,keyarg,没有就创建一个新的单一表达式,keycontentdefault,而value则是一个函数。如果这里的arg也就是v-slot:xx="aa"里的xx是一个动态的,比如v-slot[xx]="aa",那么这个v-slot就会被标记为dynamic
  2. 如果是template上面的,那就需要判断几种场景:
  • 搭配v-if的场景,如果搭配了v-if,那就肯定是dynamic了,因为runtime阶段可能切换slot。会创建一个条件表表达式节点。如果是v-elsei(-if),操作类似transformIf里的,找到IF的节点,让后将当前节点放到IF节点的branch里,建立if的联系。它也是创建一个条件表达式节点。
  • 如果搭配v-for的场景,它也是动态的。直接就是将forrenderList放到createSlot里面。

为什么会有上面两种兼容场景呢?因为我们在transformIftransformFor的时候绕过了v-slot的场景,所以这里自然就得处理这两种场景。 接着兼容处理,如果你这个component里存在子节点,但是没有一处地方有v-slot的指令,那也是可以的,实际上这也是大多数时候我们写的slot代码,默认是创建一个defaultSlotProperty节点。 然后就是插旗啦:

    • 如果是动态的,则是SlotFlags.DYNAMIC,注释:The parent will need to force the child to update because the slot does not fully capture its dependencies.为了安全考虑,还是把它也一起更新了。
    • 如果是盖中盖也就是类型为SlotFlags.FORWARD,比如<template></template>,把一个插槽传给了组件,这样就能盖中盖了。
    • 除此之外都是stable的了


trackSlotScopes

这个插件看名字就知道是用来跟踪slot的变量相关的。

代码也没啥好说的了,和trackVForSlotScopes这个做的活儿一样,进入到当前节点就将scopes.vSlot++并且将slot的数据放到context.identifiers上。然后返回回调,当回调被执行的时候就意味着所有子节点都已经处理完了,自然就可以退出当前vslotscope了。


transformText

这个其实也是没啥好说的..

  1. 将前后两个文本节点拼接成一个节点,这里的文本节点指的是NodeTypes.INTERPOLATION || NodeTypes.TEXT
  2. 满足以下条件的节点可以视作为文本节点(逻辑与):
  • 这个节点是元素节点或者是根节点
  • 这个节点没有任何的指令
  • 这个节点只有一个子节点
  • 这个子节点是一个文本节点
  1. 提前创建VNode,我们前面说了codegenNode是在transformElement插件中生成的,但是那里处理的时候只是处理元素节点和组件节点,其它的都不处理。所以这里并不冲突。辅助函数是createText


directiveTransforms

现在我们分析完了nodeTransforms里面注册的插件(不包括compiler-domcompiler-sfc传入的,感兴趣的可以去之前的文章里看下)

directiveTransforms里面的插件好像没看到地方调用?实际上有,在buildProps中调用了,用来处理元素/组件节点的props。但是buildProps方法由于篇幅的问题我们并没有贴出来代码,这里把调用的那部分叠出来

现在就让我们进入到具体的指令处理插件中分析下代码(有的就不分析了,比如v-on的,之前在compiler-dom里面分析完了)

transformOn

这个插件之前在compiler-dom里面分析过了。

vue/compiler-dom源码分析学习--day3: 转换指令 - 知乎 (zhihu.com)

这里就不说了,来看下render function


transformBind

  • exp:老熟人了,等号右边的表达式
  • modifiers:也是老熟人,修饰符
  • arg:也是,等号左边指令右边的内容,比如v-on:click.once="handle",这个click就是arg,而这个handle就是exp,而这个once自然就是修饰符了。

这些都是在parse阶段就处理好的。

先是对arg做空值保护,然后处理camel修饰符。

这个修饰符可以把你的属性名字驼峰化。如果这个arg是一个动态的,比如v-bind:[test]="xxx",那么这个test就是一个动态的。这个时候就只能借助辅助函数camelize来处理了,否则可以直接在编译阶段就camlize

接下来处理3.2引入的v-bind的两个修饰符.prop.attr

  • .prop可以强制这个绑定的属性变成property
  • .attr同上,变成attribute

那么问题来了,propattribute有啥不同呢?

the-front-end-knowledge-you-may-not-know/015-dom-attributes-and-properties.md at master · justjavac/the-front-end-knowledge-you-may-not-know (github.com)

可以看下这篇文章,这里就不说了。

  • injectPrefix:看名字就知道是用来注入前缀的,实际上确实如此,所以代码就不看了,不过这里还需要处理动态的arg

最后直接就创建一个对象属性节点并返回。


transformModel

在分析之前,先来看下/复习下vue3.xv-model的用法,做了一些breaking update

Built-in Directives | Vue.js (vuejs.org)

2.x -&gt; 3.x迁移文档:v-model | Vue 3 Migration Guide (vuejs.org)

另外里面涉及到的inline mode的代码我这里也不会分析

处理modifier的部分在compiler-dom的包里,这里就不再说了,感兴趣的大佬可以去看下之前的文章。vue/compiler-dom源码分析学习--day3: 转换指令 - 知乎 (zhihu.com)

前面都是在处理错误场景,就不多说了。。

不过这里有一个需要注意的,那就是你这个v-modelexp不能是来自scopevariable,否则会报错:v-model cannot be used on v-for or v-slot scope variables because they are not writable.scope variable不可写,只能读。而v-model确实需要改写原数据,这样才算是双向绑定。

其实没啥好说的了,实际上v-model算是一种语法糖写法,它在编译阶段会变成下面这样

其实没啥好说的了,实际上v-model算是一种语法糖写法,它在编译阶段会变成下面这样

而在组件中则多了一个prop,那就是modifiervue3.x中允许通过defineProps去自定义modifier的行为。

最后将这个节点返回出去


genNode

那么现在对于transform阶段 AST节点的处理以及codegenNode的生成我们已经分析完了,现在我们来到generate阶段,分析每种类型对应的生成render function的场景。

为了看着方便,我这里把代码再贴一遍。

isString

如果节点自身就是一个字符串形式的,那直接就能渲染了,都不用runtime的辅助函数。


isSymbol

如果这节点自身就只有一个辅助函数,那直接就放到helpers集合里


ELEMENT/IF/FOR

这三种情况是需要递归的,因为可能存在子节点


TEXT

文本节点,将节点内容字符串化


SIMPLE_EXPRESSION

单节点表达式,如果是静态的内容,直接stringify处理,如果不是就直接push,拼接到runtimecode里。


INTERPOLATION

  • pure:这个和webpacktree-shake相关的。
  • PURE_ANNOTATION/*#__PURE__*/, 声明这个方法可以是pure纯净的,可以被webpack的编译器 shake掉。
  • TO_DISPLAY_STRINGtoDisplayString辅助函数。

由于{{ xxx }}xxx可能是一个复合表达式,所以还需要递归处理这个节点的content

这个a()b()就是俩callExpression


TEXT_CALL

这个我们刚分析完的transformText里如果有两个相连的text节点,就会组成一个新的节点,这就是TEXT_CALL节点。这个新节点的codegenNode是一个callExpression,所以也需要递归处理。


COMPOUND_EXPRESSION

这个就是我们经常念叨的复合类型,比如一个表达式里面有两个语句。


COMMENT

这个就不多说了,注释节点


VNODE_CALL

这个节点就是最常见的codegenNode节点类型,我们刚分析完的transformElement给元素/组件节点生成的codegenNode就是这个类型。

这里的这个directives不是内置的,而是custom的指令,需要withDirectives辅助函数。

这个genNullableArgs就不看代码了,就是过滤掉你这节点的null数据。

getNodeList:这个方法没啥好说的,就是将子元素递归genNode处理


JS_CALL_EXPRESSION

这个也没啥好说的,一个函数调用表达式,辅助函数就是callee

比如v-on="object"会变成toHandlers(obj)的形式


JS_OBJECT_EXPRESSION

对象表达式节点,比如一个指令的expcodegenNode就有可能是一个JS_OBJECT_EXPRESSION:style="{ xx: xxx }"


JS_ARRAY_EXPRESSION

这个就不多说了,调用gentNodeListAsArray


JS_CONDITIONAL_EXPRESSION

条件表达式节点,比如v-if的场景,用的就是这个


JS_CACHE_EXPRESSION

缓存表达式,这个有些陌生但实际上我们是遇到过的,不过是在另一个包compiler-dom分析的时候遇到的。


JS_BLOCK_STATEMENT

这个就不看代码了,调用的是genNodeList,一个block的表达式,比如v-for搭配v-memo的场景。


剩下的ssr only类型的这里就不看了


总结

那么这个包中的插件也就都分析完了,这里没有把buildPropsbuildSlot的代码贴出来,可能读起来有些没头没脑。。

总之,如果觉得这篇文章对你有帮助的话,点个赞也是可以哒~

发布于 2023-01-23 09:41・IP 属地广东