Vue 实现了一套内容分发的 API,将 <slot>
元素作为承载分发内容的出口。<slot>
在子组件中可以有多个,使用 name 属性实现具名插槽。
从插槽内容能否使用子组件数据的角度可将插槽分为两类:普通插槽
、作用域插槽
。
普通插槽
不能使用子组件的数据,父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。作用域插槽
是指在子组件<slot>
上绑定 插槽prop,父组件可以访问子组件插槽上的全部prop。
插槽的使用
在Vue2.x版本中,父组件向插槽提供内容的方式并不是一成不变的,普通插槽与作用域插槽的使用均有所改变。
slot
在2.6.0以前,父组件中使用 slot 属性实现普通插槽。借用官网示例,能够很清晰的阐述其用法。假设组件 <base-layout>
代码如下:
1 | <div class="container"> |
使用 slot 属性实现具名插槽时,没有 slot 属性的按照放在默认插槽中,如果子组件中没有实现默认插槽则丢弃该部分内容。
1 | <base-layout> |
slot 属性不仅可以放在 <template>
元素中,也可以放在普通元素中。
1 | <base-layout> |
上述两种写法渲染的结果一样,如下所示:
1 | <div class="container"> |
scope 和 slot-scope
最初Vue2.0版本使用 scope
属性来实现作用域插槽,在2.5.0以后被 slot-scope
取代。二者唯一的区别在于 scope
属性只能用在 <template>
元素上,slot-scope
可以在普通元素上使用。
依旧使用官方示例来介绍 slot-scope
,假设有组件 <slot-example>
代码如下:
1 | <span> |
在父组件中,可以使用如下方式获取到子组件的数据 user:
1 | <slot-example> |
前面提到过,slot-scope
与 scope
的唯一区别在于能够在普通元素上使用,因此可以省略掉 <template>
元素。
1 | <slot-example> |
v-slot
从 Vue2.6.0 版本开始,原有的 slot 与 slot-scope 皆被废弃,新版本使用 v-slot
指令来实现普通插槽与作用域插槽。
v-slot 指令向前面示例中 <base-layout>
组件提供普通插槽内容的方式如下:
1 | <base-layout> |
v-slot
指令可以被缩写成字符 #
1 | <base-layout> |
使用v-slot
指令实现作用域插槽的方式如下所示:
1 | <current-user> |
v-slot 指令一般只能添加在 <template>
元素上,除非当被提供的内容只有默认插槽时,才能在组件标签上使用。
1 | <current-user v-slot="slotProps"> |
新增v-slot的原因
作用域插槽原本是通过 scope
实现的,scope 只能在 <template>
元素上使用。为了提升使用的灵活性,在2.5.0版本之后,使用 slot-scope 取代 scope。
slot-scope 与 scope 用法基本一致,唯一有区别的是不仅可以在 <template>
元素上使用,还可以在普通标签上使用。可以在普通标签上使用,也就意味着可以在组件标签上使用。这在一定程度上提高了灵活性,但是也带来了一些问题。
1 | <foo> |
在这种深度嵌套的情况下,并不能清晰的知道哪个组件在此模板中提供哪个变量。后来 Vue 作者说允许在没有模板的情况下使用 slot-scope 是一个错误。
Vue2.6.0之后废弃 slot、slot-scope,将关于插槽的功能统一到 v-slot 指令中。这样首先能够使语法更为简洁,更重要的是能够解决组件提供变量不清的问题。
v-slot 指令规定只能使用在 元素上,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样就能很清晰的确定变量是哪个组件提供的。1
2
3
4
5
6
7<foo v-slot="foo">
<bar v-slot="bar">
<baz v-slot="baz">
{{ foo }} {{ bar }} {{ baz }}
</baz>
</bar>
</foo>
采用新指令 v-slot
而不是修复 slot-scope
功能漏洞的原因主要有以下三点:
- Vue2.x版本在2.6.0时已到了后期,Vue3.x马上发布了,类似这种突破性的改变没有合适的时机发布。
- 一旦改变 slot-scope 的功能,会使得之前关于 slot-scope 的资料全部过时,容易给新学习者带来迷惑。
- 在3.x中,插槽类型将会被统一起来,使用 v-slot 指令统一语法是很好的选择。
原理解析:
虽然 slot、slot-scope 被废弃,不建议开发者使用,但是在 Vue2.6.0 版本里依然保留其实现代码,这两个属性依然可以使用。因此探究二者的实现代码还是挺有必要的。
以下面示例代码的编译渲染过程来阐述 slot、slot-scope 属性的实现原理。
1 | <!--父组件使用插槽--> |
父组件slot、slot-scope属性的编译
在模板编译的过程中,解析结束标签时会调用 processElement
函数对 AST 中的当前节点进行处理:
1 | function processElement(element,options) { |
对插槽的解析,就从 processSlotContent
函数开始。processSlotContent
函数对普通插槽属性 slot、slot-scope 的处理代码如下:
1 | function processSlotContent (el) { |
processSlotContent
函数主要有以下几点功能:
- 将作用域插槽信息取出,然后赋值给节点 slotScope 属性。
- 取出插槽名称赋值给节点 slotTarget 属性,如果没有则置为 default。
- 根据slot值是否使用了v-bind指令绑定来赋予节点slotTargetDynamic属性不同的布尔值。
- 如果 slot 是普通标签(非template)的属性且不是作用域插槽,则在节点上添加 attrs 对象数组,用于存储 slot 的信息。
属性 slot、slot-scope 在优化 AST 的部分不进行处理,在根据 AST 生成渲染函数时会调用 genData
函数处理节点属性。
1 | function genData (el, state){ |
其中 genScopedSlots
函数代码如下所示:
1 | function genScopedSlots (el,slots,state){ |
可以看到只带有 slot 属性的节点被正常解析成子组件的子节点,而带有 slot-scope 属性的节点被 _u()
方法包裹,作为子组件 scopedSlots
属性的一部分。
另外一点需要注意的是,上面提到过不在 template
上使用普通插槽时,会将 slot 信息存储到 attrs 中,因此 genData 函数中对 attrs 的处理也可能与普通插槽有关:
1 | if (el.attrs) { |
上述实例最终生成的渲染函数为:
1 | _c( |
v-slot 指令的编译
processSlotContent
函数对指令 v-slot
的处理代码如下:
1 | if (el.tag === 'template') { |
可以看到在 标签上使用指令时,经过 processSlotContent 函数处理后,跟使用slot、slot-scope属性生成的结果一样,因此后续的处理与上一小节描述的相同。
子组件标签的编译
在编译过程中,会使用 processSlotOutlet
函数对 <slot>
标签处理,提取出 <slot>
标签上 name
属性的值。
1 | function processSlotOutlet (el) { |
在 codegen
阶段会调用 genSlot
函数,根据子组件的内容生成被 _t()
包裹的字符串。
1 | function genSlot (el, state) { |
最终示例中子组件的渲染函数为:
1 | _c( |
渲染
上述示例中父组件经过 _render 方法处理后生成的 VNode
如下所示:
1 | { |
由上可知,仅含有 slot 属性的标签被处理成父组件的 子VNode
节点,含有 slot-scope
属性的标签被存放在 父VNode
的 data.scopedSlots
属性对象上。
子组件 <slot>
标签生成的渲染函数中 _t()
为 renderSlot
函数:
1 | function renderSlot (name,fallback,props,bindObject) { |
renderSlot
函数返回值为父组件的子VNode数组,根据不同情况来完成父组件替换子组件插槽内容的过程。renderSlot
函数的逻辑分为两部分:
- 如果是普通插槽,则取已经生成的以父实例为上下文的子VNode作为返回值。
- 如果是作用域插槽,则根据
$scopedSlots
属性中的值生成新的VNode作为返回值。
由此可知:普通插槽只能使用父组件数据的原因在于其VNode是以父实例为上下文生成的,而作用域插槽则是在编译渲染子组件的时候才生成的。
总结:
在2.x版本中,插槽的使用有过改变。最初使用scope属性实现作用域插槽,2.5.0版本以后被slot-scope 取代,普通插槽的使用在2.6.0版本之前使用slot属性完成。在2.6.0之后使用v-slot指令来实现普通插槽与作用域插槽。
slot-scope属性可以在普通标签上使用,在深度嵌套的情况下,不能很清晰的知道组件和变量的关系。另外,2.x版本已经到了后期,做出这种突破性的改变并不合适,因此2.6.0版本新增v-slot指令来规避这种错误。
v-slot指令在 标签上使用时,内部实现上与slot、slot-scope本质上相同。普通插槽的子组件生成的上下文是父组件实例,因此不能使用自身的数据,只能使用父组件的数据。作用域插槽在父组件生成VNode时并没有一起生成,而是在渲染子组件时才生成的,因此可以使用子组件自身的数据。