前言
又是这幅图:
graph LR O[baseCompile] -->|"①"| B[parse\n生成抽象语法树] A[template 模板]-->|args|B-->|return|C[ast] O-->|"②"|D[optimize\n标记静态节点] C-->|args|D C-->|args|E O-->|"③"|E["generate\n生成 render 函数"]-->|return|F[render]
众所周知,插槽分为两部分,亲组件中的<slot>和插进去的子元素(下称插槽内容),下面分为两部分来写:
slot
slot标签可以定义一个插槽让子元素插进去,在对应的地方渲染子组件的 UI,也可以提供亲组件上的变量给子组件访问。slot涉及到的是parse和generate两个阶段:
parse:解析 slot 标签
相关逻辑如下所示:
graph LR C[parse] C-->|call|D B[end]-->|args|D D[parseHTML] D-->|标签结束|B B-->|call|G["closeElement"]-->|call|H[processElement]-->|"el.tag === 'slot'"|E[processSlotOutlet]
processSlotOutlet很简单,把插槽名记录到slot标签的节点上,没了:
ts- // handle <slot/> outlets
- function processSlotOutlet(el) {
- if (el.tag === 'slot') {
- el.slotName = getBindingAttr(el, 'name')
- }
- }
generate:生成 slot 标签代码
相关逻辑如下所示
graph LR A[generate]-->|call|B[genElement]-->|"el.tag === 'slot'"|C[genSlot]-->|call|D[genChildren]
来看genSlot:
ts- function genSlot(el: ASTElement, state: CodegenState): string {
- const slotName = el.slotName || '"default"'
- const children = genChildren(el, state)
- let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
- // attrs、bind 就是作用域插槽传递给插槽内容的属性
- const attrs =
- el.attrs || el.dynamicAttrs
- ? genProps(
- (el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
- // slot props are camelized
- name: camelize(attr.name),
- value: attr.value,
- dynamic: attr.dynamic
- }))
- )
- : null
- const bind = el.attrsMap['v-bind']
- if ((attrs || bind) && !children) {
- res += `,null`
- }
- if (attrs) {
- res += `,${attrs}`
- }
- if (bind) {
- res += `${attrs ? '' : ',null'},${bind}`
- }
- return res + ')'
- }
attrs、bind就是作用域插槽传递给插槽内容的属性,然后就是把子元素和属性拼起来生成代码,举个例子:
html- <div>
- <slot name="test" data1="test1" v-bind:data2="test2" :data3="test3" v-bind="{ data4: test4 }">
- <span>default content</span>
- </slot>
- </div>
会生成:
ts- `_t("test",function(){return [_c('span',[_v("default content")])]},{"data1":"test1","data2":test2,"data3":test3},{ data4: test4 })`
插槽内容
如果你在 Vue 中写过JSX,你就会发现在虚拟 DOMVNode中有一个属性$slot,它是一个对象,每个键值都是一个生成插槽中子元素的函数,例如说$slot.default()就可以生成默认插槽的子元素的VNode,$slot.name1(),就是名为name1的具名插槽。然后,往这个函数里面传值,就可以被作用域插槽的插槽内容接收。模板编译就涉及到相关逻辑的处理。插槽内容的模板编译也涉及到parse和generate两个阶段。
parse:解析插槽内容
graph LR C[parse] C-->|call|D B[end]-->|args|D D[parseHTML] D-->|标签结束|B B-->|call|G["closeElement"]-->|call|H[processElement]-->|"有v-slot或者者旧版的插槽语法"|E[processSlotContent]
processSlotContent这个函数逻辑很多,主要是它兼容了旧版本的slot="xxx"和slot-scope="xxx"甚至更远古的scope="xxx"语法,先来看旧版本的语法:
ts- function processSlotContent(el) {
- let slotScope
- if (el.tag === 'template') {
- slotScope = getAndRemoveAttr(el, 'scope')
- el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
- } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
- el.slotScope = slotScope
- }
- // slot="xxx"
- const slotTarget = getBindingAttr(el, 'slot')
- if (slotTarget) {
- el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
- el.slotTargetDynamic = !!(
- el.attrsMap[':slot'] || el.attrsMap['v-bind:slot']
- )
- // preserve slot as an attribute for native shadow DOM compat
- // only for non-scoped slots.
- if (el.tag !== 'template' && !el.slotScope) {
- addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
- }
- }
- // ...
- }
el.slotScope记录了作用域插槽接受的变量,el.slotTarget记录了插槽名称,el.slotTargetDynamic记录动态插槽名。
下面来看新的语法:
ts- function processSlotContent(el) {
- let slotScope
- // ...
- // 2.6 v-slot syntax
- if (process.env.NEW_SLOT_SYNTAX) {
- if (el.tag === 'template') {
- // v-slot on <template>
- const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
- if (slotBinding) {
- const { name, dynamic } = getSlotName(slotBinding)
- el.slotTarget = name
- el.slotTargetDynamic = dynamic
- el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
- }
- } else {
- // 非 template ...
- }
- }
- }
还记得前面说过,parse阶段会把标签的属性解析为这样子的数组:
ts- {
- name: string, // 属性的键,xxx="yyy" 的 xxx
- value: any, // 属性的值,xxx="yyy" 的 yyy
- dynamic: boolean // 属性名是否动态,例如 v-slot:name 是静态的,v-slot:[name] 是动态的
- }[]
getAndRemoveAttrByRegex顾名思义,就是根据正则把属性的对象取下来,const slotRE = /^v-slot(:|$)|^#/匹配的是v-slot:name、slot或者#name的前半段slot:、slot或者#。el.slotScope、el.slotTarget、el.slotTargetDynamic和上面一样。
来看getSlotName
ts- function getSlotName(binding) {
- let name = binding.name.replace(slotRE, '')
- if (!name) {
- if (binding.name[0] !== '#') {
- name = 'default'
- }
- }
- // /^\[.*\]$/
- return dynamicArgRE.test(name)
- ? // dynamic [name]
- { name: name.slice(1, -1), dynamic: true }
- : // static name
- { name: `"${name}"`, dynamic: false }
- }
很显然是获取插槽语法的插槽名及其是否动态的。
下面是非template的情况
ts- function processSlotContent(el) {
- let slotScope
- // ...
- // 2.6 v-slot syntax
- if (process.env.NEW_SLOT_SYNTAX) {
- if (el.tag === 'template') {
- // ...
- } else {
- // v-slot on component, denotes default slot
- const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
- if (slotBinding) {
- // add the component's children to its default slot
- const slots = el.scopedSlots || (el.scopedSlots = {})
- const { name, dynamic } = getSlotName(slotBinding)
- const slotContainer = (slots[name] = createASTElement(
- 'template',
- [],
- el
- ))
- slotContainer.slotTarget = name
- slotContainer.slotTargetDynamic = dynamic
- slotContainer.children = el.children.filter((c: any) => {
- if (!c.slotScope) {
- c.parent = slotContainer
- return true
- }
- })
- slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
- // remove children as they are returned from scopedSlots now
- el.children = []
- // mark el non-plain so data gets generated
- el.plain = false
- }
- }
- }
- }
和上面大差不差,只是会把有v-slot或者#的插槽内容的子元素用template包裹起来,原来插槽内容的标签相当于把这个插槽传递了一遍。
generate:插槽内容
涉及的逻辑:
graph LR A[generate]-->|call|B[genElement]-->|call|C[genData]-->|作用域插槽|D[genScopedSlots]-->|call|E[containsSlotChild]-->|callback|B E-->|call|F["genChildren、genIf、genFor"]-->|"... callback"|B
genData这个函数非常复杂,就不展开说了,功能是把各种标签上的属性转化为字符串。
ts- export function genData(el: ASTElement, state: CodegenState): string {
- let data = '{'
- // ...
- // slot target
- // only for non-scoped slots
- if (el.slotTarget && !el.slotScope) {
- data += `slot:${el.slotTarget},`
- }
- // scoped slots
- if (el.scopedSlots) {
- data += `${genScopedSlots(el, el.scopedSlots, state)},`
- }
- // ...
- return data
- }
slot:${el.slotTarget}记录了插槽名,如果是作用域插槽,进入genScopedSlots:
ts- function genScopedSlots(
- el: ASTElement,
- slots: { [key: string]: ASTElement },
- state: CodegenState
- ): string {
- // 先是一些优化,标记插槽内容是否会随亲组件更新而更新,注释写得挺清楚的:
-
- // by default scoped slots are considered "stable", this allows child
- // components with only scoped slots to skip forced updates from parent.
- // but in some cases we have to bail-out of this optimization
- // for example if the slot contains dynamic names, has v-if or v-for on them...
- let needsForceUpdate =
- el.for ||
- Object.keys(slots).some(key => {
- const slot = slots[key]
- return (
- slot.slotTargetDynamic || slot.if || slot.for || containsSlotChild(slot) // is passing down slot from parent which may be dynamic
- )
- })
- // #9534: if a component with scoped slots is inside a conditional branch,
- // it's possible for the same component to be reused but with different
- // compiled slot content. To avoid that, we generate a unique key based on
- // the generated code of all the slot contents.
- let needsKey = !!el.if
- // OR when it is inside another scoped slot or v-for (the reactivity may be
- // disconnected due to the intermediate scope variable)
- // #9438, #9506
- // TODO: this can be further optimized by properly analyzing in-scope bindings
- // and skip force updating ones that do not actually use scope variables.
- if (!needsForceUpdate) {
- let parent = el.parent
- while (parent) {
- if (
- (parent.slotScope && parent.slotScope !== emptySlotScopeToken) ||
- parent.for
- ) {
- needsForceUpdate = true
- break
- }
- if (parent.if) {
- needsKey = true
- }
- parent = parent.parent
- }
- }
- // ...
- }
这一堆代码主要就是讲插槽是否需要随着亲元素更新而更新,以及是否需要key用于防止复用(位于不同 if 分支上插槽内容可能会因为更新时的 DOM 复用而污染状态)。需要更新的情况包括:
- 插槽内容及其各级亲元素有 for;
- 插槽内容及其各级亲元素是作用域插槽的插槽内容;
- 插槽内容的子元素使用了动态插槽名、if、for;
- 插槽内容的各级子元素存在
slot标签(见containsSlotChild)。
ts- function containsSlotChild(el: ASTNode): boolean {
- if (el.type === 1) {
- if (el.tag === 'slot') {
- return true
- }
- return el.children.some(containsSlotChild)
- }
- return false
- }
继续往下看:
ts- function genScopedSlots(
- el: ASTElement,
- slots: { [key: string]: ASTElement },
- state: CodegenState
- ): string {
- // ...
- // 递归子元素
- const generatedSlots = Object.keys(slots)
- .map(key => genScopedSlot(slots[key], state))
- .join(',')
- return `scopedSlots:_u([${generatedSlots}]${
- needsForceUpdate ? `,null,true` : ``
- }${
- !needsForceUpdate && needsKey ? `,null,false,${hash(generatedSlots)}` : ``
- })`
- }
genScopedSlot生成了一个键值对字符串,key为插槽名,fn为对应的生成子元素的函数。_u中,会使用它生成VNode的$slot的内容
needsKey时,_u传入 hash 值作为入参,旧版本 if 分支中的插槽内容中需要key属性防止复用,详见 Vue2 的 issue,现在可以自动为其加上一个 hash 值作为key。
genScopedSlot生成了插槽内容工厂函数:
ts- function genScopedSlot(el: ASTElement, state: CodegenState): string {
- const isLegacySyntax = el.attrsMap['slot-scope']
- if (el.if && !el.ifProcessed && !isLegacySyntax) {
- return genIf(el, state, genScopedSlot, `null`)
- }
- if (el.for && !el.forProcessed) {
- return genFor(el, state, genScopedSlot)
- }
- const slotScope =
- el.slotScope === emptySlotScopeToken ? `` : String(el.slotScope)
- const fn =
- `function(${slotScope}){` +
- `return ${
- el.tag === 'template'
- ? el.if && isLegacySyntax
- ? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`
- : genChildren(el, state) || 'undefined'
- : genElement(el, state)
- }}`
- // reverse proxy v-slot without scope on this.$slots
- const reverseProxy = slotScope ? `` : `,proxy:true`
- return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}`
- }
还记得genFor、genIf上面有一个altGen的入参吗,这里就用上了,用genScopedSlot代替了原有的genElement:
ts- function genIf(
- el: any,
- state: CodegenState,
- altGen?: Function | undefined,
- altEmpty?: string | undefined
- ): string
- function genFor(
- el: any,
- state: CodegenState,
- altGen?: Function | undefined,
- altHelper?: string | undefined
- ): string
在genElement或者genChildren外面包了一层函数,并且提供slotScope的入参,这也就是给$slot.xxx()传参能被传入插槽中的原因了。
总结
本文简介了模板编译阶段插槽及其子元素的生成规则。
