coverPiccoverPic

模板编译之插槽(v2)

前言

又是这幅图:

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涉及到的是parsegenerate两个阶段:

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
  1. // handle <slot/> outlets
  2. function processSlotOutlet(el) {
  3. if (el.tag === 'slot') {
  4. el.slotName = getBindingAttr(el, 'name')
  5. }
  6. }

generate:生成 slot 标签代码

相关逻辑如下所示

graph LR
A[generate]-->|call|B[genElement]-->|"el.tag === 'slot'"|C[genSlot]-->|call|D[genChildren]

来看genSlot:

ts
  1. function genSlot(el: ASTElement, state: CodegenState): string {
  2. const slotName = el.slotName || '"default"'
  3. const children = genChildren(el, state)
  4. let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
  5. // attrs、bind 就是作用域插槽传递给插槽内容的属性
  6. const attrs =
  7. el.attrs || el.dynamicAttrs
  8. ? genProps(
  9. (el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
  10. // slot props are camelized
  11. name: camelize(attr.name),
  12. value: attr.value,
  13. dynamic: attr.dynamic
  14. }))
  15. )
  16. : null
  17. const bind = el.attrsMap['v-bind']
  18. if ((attrs || bind) && !children) {
  19. res += `,null`
  20. }
  21. if (attrs) {
  22. res += `,${attrs}`
  23. }
  24. if (bind) {
  25. res += `${attrs ? '' : ',null'},${bind}`
  26. }
  27. return res + ')'
  28. }

attrsbind就是作用域插槽传递给插槽内容的属性,然后就是把子元素和属性拼起来生成代码,举个例子:

html
  1. <div>
  2. <slot name="test" data1="test1" v-bind:data2="test2" :data3="test3" v-bind="{ data4: test4 }">
  3. <span>default content</span>
  4. </slot>
  5. </div>

会生成:

ts
  1. `_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的具名插槽。然后,往这个函数里面传值,就可以被作用域插槽的插槽内容接收。模板编译就涉及到相关逻辑的处理。插槽内容的模板编译也涉及到parsegenerate两个阶段。

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
  1. function processSlotContent(el) {
  2. let slotScope
  3. if (el.tag === 'template') {
  4. slotScope = getAndRemoveAttr(el, 'scope')
  5. el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
  6. } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
  7. el.slotScope = slotScope
  8. }
  9. // slot="xxx"
  10. const slotTarget = getBindingAttr(el, 'slot')
  11. if (slotTarget) {
  12. el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
  13. el.slotTargetDynamic = !!(
  14. el.attrsMap[':slot'] || el.attrsMap['v-bind:slot']
  15. )
  16. // preserve slot as an attribute for native shadow DOM compat
  17. // only for non-scoped slots.
  18. if (el.tag !== 'template' && !el.slotScope) {
  19. addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
  20. }
  21. }
  22. // ...
  23. }

el.slotScope记录了作用域插槽接受的变量,el.slotTarget记录了插槽名称,el.slotTargetDynamic记录动态插槽名。

下面来看新的语法:

ts
  1. function processSlotContent(el) {
  2. let slotScope
  3. // ...
  4. // 2.6 v-slot syntax
  5. if (process.env.NEW_SLOT_SYNTAX) {
  6. if (el.tag === 'template') {
  7. // v-slot on <template>
  8. const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
  9. if (slotBinding) {
  10. const { name, dynamic } = getSlotName(slotBinding)
  11. el.slotTarget = name
  12. el.slotTargetDynamic = dynamic
  13. el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
  14. }
  15. } else {
  16. // 非 template ...
  17. }
  18. }
  19. }

还记得前面说过,parse阶段会把标签的属性解析为这样子的数组:

ts
  1. {
  2. name: string, // 属性的键,xxx="yyy" 的 xxx
  3. value: any, // 属性的值,xxx="yyy" 的 yyy
  4. dynamic: boolean // 属性名是否动态,例如 v-slot:name 是静态的,v-slot:[name] 是动态的
  5. }[]

getAndRemoveAttrByRegex顾名思义,就是根据正则把属性的对象取下来,const slotRE = /^v-slot(:|$)|^#/匹配的是v-slot:nameslot或者#name的前半段slot:slot或者#el.slotScopeel.slotTargetel.slotTargetDynamic和上面一样。

来看getSlotName

ts
  1. function getSlotName(binding) {
  2. let name = binding.name.replace(slotRE, '')
  3. if (!name) {
  4. if (binding.name[0] !== '#') {
  5. name = 'default'
  6. }
  7. }
  8. // /^\[.*\]$/
  9. return dynamicArgRE.test(name)
  10. ? // dynamic [name]
  11. { name: name.slice(1, -1), dynamic: true }
  12. : // static name
  13. { name: `"${name}"`, dynamic: false }
  14. }

很显然是获取插槽语法的插槽名及其是否动态的。

下面是非template的情况

ts
  1. function processSlotContent(el) {
  2. let slotScope
  3. // ...
  4. // 2.6 v-slot syntax
  5. if (process.env.NEW_SLOT_SYNTAX) {
  6. if (el.tag === 'template') {
  7. // ...
  8. } else {
  9. // v-slot on component, denotes default slot
  10. const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
  11. if (slotBinding) {
  12. // add the component's children to its default slot
  13. const slots = el.scopedSlots || (el.scopedSlots = {})
  14. const { name, dynamic } = getSlotName(slotBinding)
  15. const slotContainer = (slots[name] = createASTElement(
  16. 'template',
  17. [],
  18. el
  19. ))
  20. slotContainer.slotTarget = name
  21. slotContainer.slotTargetDynamic = dynamic
  22. slotContainer.children = el.children.filter((c: any) => {
  23. if (!c.slotScope) {
  24. c.parent = slotContainer
  25. return true
  26. }
  27. })
  28. slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
  29. // remove children as they are returned from scopedSlots now
  30. el.children = []
  31. // mark el non-plain so data gets generated
  32. el.plain = false
  33. }
  34. }
  35. }
  36. }

和上面大差不差,只是会把有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
  1. export function genData(el: ASTElement, state: CodegenState): string {
  2. let data = '{'
  3. // ...
  4. // slot target
  5. // only for non-scoped slots
  6. if (el.slotTarget && !el.slotScope) {
  7. data += `slot:${el.slotTarget},`
  8. }
  9. // scoped slots
  10. if (el.scopedSlots) {
  11. data += `${genScopedSlots(el, el.scopedSlots, state)},`
  12. }
  13. // ...
  14. return data
  15. }

slot:${el.slotTarget}记录了插槽名,如果是作用域插槽,进入genScopedSlots

ts
  1. function genScopedSlots(
  2. el: ASTElement,
  3. slots: { [key: string]: ASTElement },
  4. state: CodegenState
  5. ): string {
  6. // 先是一些优化,标记插槽内容是否会随亲组件更新而更新,注释写得挺清楚的:
  7. // by default scoped slots are considered "stable", this allows child
  8. // components with only scoped slots to skip forced updates from parent.
  9. // but in some cases we have to bail-out of this optimization
  10. // for example if the slot contains dynamic names, has v-if or v-for on them...
  11. let needsForceUpdate =
  12. el.for ||
  13. Object.keys(slots).some(key => {
  14. const slot = slots[key]
  15. return (
  16. slot.slotTargetDynamic || slot.if || slot.for || containsSlotChild(slot) // is passing down slot from parent which may be dynamic
  17. )
  18. })
  19. // #9534: if a component with scoped slots is inside a conditional branch,
  20. // it's possible for the same component to be reused but with different
  21. // compiled slot content. To avoid that, we generate a unique key based on
  22. // the generated code of all the slot contents.
  23. let needsKey = !!el.if
  24. // OR when it is inside another scoped slot or v-for (the reactivity may be
  25. // disconnected due to the intermediate scope variable)
  26. // #9438, #9506
  27. // TODO: this can be further optimized by properly analyzing in-scope bindings
  28. // and skip force updating ones that do not actually use scope variables.
  29. if (!needsForceUpdate) {
  30. let parent = el.parent
  31. while (parent) {
  32. if (
  33. (parent.slotScope && parent.slotScope !== emptySlotScopeToken) ||
  34. parent.for
  35. ) {
  36. needsForceUpdate = true
  37. break
  38. }
  39. if (parent.if) {
  40. needsKey = true
  41. }
  42. parent = parent.parent
  43. }
  44. }
  45. // ...
  46. }

这一堆代码主要就是讲插槽是否需要随着亲元素更新而更新,以及是否需要key用于防止复用(位于不同 if 分支上插槽内容可能会因为更新时的 DOM 复用而污染状态)。需要更新的情况包括:

  1. 插槽内容及其各级亲元素有 for;
  2. 插槽内容及其各级亲元素是作用域插槽的插槽内容;
  3. 插槽内容的子元素使用了动态插槽名、if、for;
  4. 插槽内容的各级子元素存在slot标签(见containsSlotChild)。
ts
  1. function containsSlotChild(el: ASTNode): boolean {
  2. if (el.type === 1) {
  3. if (el.tag === 'slot') {
  4. return true
  5. }
  6. return el.children.some(containsSlotChild)
  7. }
  8. return false
  9. }

继续往下看:

ts
  1. function genScopedSlots(
  2. el: ASTElement,
  3. slots: { [key: string]: ASTElement },
  4. state: CodegenState
  5. ): string {
  6. // ...
  7. // 递归子元素
  8. const generatedSlots = Object.keys(slots)
  9. .map(key => genScopedSlot(slots[key], state))
  10. .join(',')
  11. return `scopedSlots:_u([${generatedSlots}]${
  12. needsForceUpdate ? `,null,true` : ``
  13. }${
  14. !needsForceUpdate && needsKey ? `,null,false,${hash(generatedSlots)}` : ``
  15. })`
  16. }

genScopedSlot生成了一个键值对字符串,key为插槽名,fn为对应的生成子元素的函数。_u中,会使用它生成VNode$slot的内容

needsKey时,_u传入 hash 值作为入参,旧版本 if 分支中的插槽内容中需要key属性防止复用,详见 Vue2 的 issue,现在可以自动为其加上一个 hash 值作为key

genScopedSlot生成了插槽内容工厂函数:

ts
  1. function genScopedSlot(el: ASTElement, state: CodegenState): string {
  2. const isLegacySyntax = el.attrsMap['slot-scope']
  3. if (el.if && !el.ifProcessed && !isLegacySyntax) {
  4. return genIf(el, state, genScopedSlot, `null`)
  5. }
  6. if (el.for && !el.forProcessed) {
  7. return genFor(el, state, genScopedSlot)
  8. }
  9. const slotScope =
  10. el.slotScope === emptySlotScopeToken ? `` : String(el.slotScope)
  11. const fn =
  12. `function(${slotScope}){` +
  13. `return ${
  14. el.tag === 'template'
  15. ? el.if && isLegacySyntax
  16. ? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`
  17. : genChildren(el, state) || 'undefined'
  18. : genElement(el, state)
  19. }}`
  20. // reverse proxy v-slot without scope on this.$slots
  21. const reverseProxy = slotScope ? `` : `,proxy:true`
  22. return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}`
  23. }

还记得genForgenIf上面有一个altGen的入参吗,这里就用上了,用genScopedSlot代替了原有的genElement

ts
  1. function genIf(
  2. el: any,
  3. state: CodegenState,
  4. altGen?: Function | undefined,
  5. altEmpty?: string | undefined
  6. ): string
  7. function genFor(
  8. el: any,
  9. state: CodegenState,
  10. altGen?: Function | undefined,
  11. altHelper?: string | undefined
  12. ): string

genElement或者genChildren外面包了一层函数,并且提供slotScope的入参,这也就是给$slot.xxx()传参能被传入插槽中的原因了。

总结

本文简介了模板编译阶段插槽及其子元素的生成规则。

0 条评论未登录用户
Ctrl or + Enter 评论
© 2023-2025 LittleRangiferTarandus, All rights reserved.
🌸 Run