$scopedSlots(作用域插槽)
定义:子组件提供数据,父组件决定如何渲染,数据作用域属于子组件。
本质:子组件不直接渲染内容,而是接收一个函数,这个函数在子组件作用域内执行,从而让父组件的模板可以访问子组件的数据。
js
复制代码
// 作用域插槽的本质:一个函数,子组件调用时传入数据
this.$scopedSlots.default = function(data) {
// 这个函数在父组件的作用域编译
// 但参数 data 来自子组件
return VNode // 返回渲染好的节点
}
$slots(普通插槽)
定义:父组件提供内容,子组件决定在哪里渲染,数据作用域属于父组件。
本质:父组件在编译时就已经确定了插槽内容的所有数据和逻辑,子组件只是作为一个容器来摆放这些内容。
js
复制代码
// 普通插槽的本质:父组件编译好的 VNode 数组
// 子组件只是被动接收
this.$slots.default = [VNode, VNode, ...] // 已经是渲染好的节点
数据作用域的指向
html
复制代码
<!-- 父组件 Parent.vue -->
<template>
<div>
<!-- 普通插槽 -->
<ChildComponent>
<div>{{ parentMessage }}</div> <!-- ✅ 可以访问父组件数据 -->
<!-- <div>{{ childMessage }}</div> ❌ 不能访问子组件数据 -->
</ChildComponent>
<!-- 作用域插槽 -->
<ChildComponent>
<template v-slot:default="slotProps">
<div>{{ parentMessage }}</div> <!-- ✅ 可以访问父组件数据 -->
<div>{{ slotProps.childMessage }}</div> <!-- ✅ 可以访问子组件数据 -->
</template>
</ChildComponent>
</div>
</template>
<script>
export default {
data() {
return {
parentMessage: '父组件的数据' // 父组件作用域
}
}
}
</script>
html
复制代码
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<!-- 普通插槽:直接渲染父组件传来的内容 -->
<slot></slot>
<!-- 作用域插槽:将子组件数据传递给父组件 -->
<slot :childMessage="childMessage"></slot>
</div>
</template>
<script>
export default {
data() {
return {
childMessage: '子组件的数据' // 子组件作用域
}
}
}
</script>
编译时 vs 运行时
普通插槽:编译时确定
js
复制代码
// 父组件模板
<template>
<child>
<span>{{ message }}</span> <!-- message 在编译时就绑定到父组件 -->
</child>
</template>
// 编译后的渲染函数(简化)
render() {
// 在父组件作用域中创建 VNode
const children = [createVNode('span', null, this.message)]
// 传递给子组件
return h(Child, null, { default: () => children })
}
作用域插槽:运行时确定
js
复制代码
// 父组件模板
<template>
<child>
<template v-slot="props">
<span>{{ props.message }}</span> <!-- message 来自子组件 -->
</template>
</child>
</template>
// 编译后的渲染函数(简化)
render() {
// 父组件不直接创建 VNode,而是创建一个函数
const scopedSlotFn = (props) => {
return createVNode('span', null, props.message)
}
// 把这个函数传递给子组件
return h(Child, null, { default: scopedSlotFn })
}
// 子组件中
render() {
// 子组件调用这个函数,传入自己的数据
const vnode = this.$scopedSlots.default({ message: this.childMessage })
return vnode
}
总结对比表
| 维度 |
普通插槽 |
作用域插槽 |
| 数据来源 |
父组件 |
子组件 |
| 存储形式 |
VNode数组 |
函数 |
| 编译时机 |
父组件编译时 |
子组件运行时调用 |
| 使用场景 |
布局、内容填充 |
自定义渲染,列表渲染 |
| 灵活性 |
低(内容固定) |
高(可动态渲染) |
| 数据流向 |
父 → 子(仅传递内容) |
子 → 父 (仅传递数据) |
vue3变化, <math xmlns="http://www.w3.org/1998/Math/MathML"> s c o p e d S l o t s 被移除,统一使用 scopedSlots被移除,统一使用 </math>scopedSlots被移除,统一使用slots,所有插槽都是函数。
html
复制代码
<!-- 子组件 Child.vue -->
<template>
<div>
<!-- 普通插槽 -->
<slot></slot>
<!-- 作用域插槽 -->
<slot name="item" :data="itemData"></slot>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
const itemData = { name: 'Vue 3', version: 3 }
onMounted(() => {
// Vue 3 中,所有插槽都是函数
console.log(typeof $slots.default) // 'function'
console.log(typeof $slots.item) // 'function'
// 调用函数获取 VNode
const defaultVNode = $slots.default()
const itemVNode = $slots.item({ data: itemData })
// 注意:Vue 3 中 $slots 返回的是 VNode 数组
console.log(Array.isArray(defaultVNode)) // true
})
</script>
| 特性 |
Vue 2 |
Vue 3 |
| 普通插槽存储 |
$slots (VNode 数组) |
$slots (函数) |
| 作用域插槽存储 |
$scopedSlots (函数) |
$slots (函数) |
| 模板语法 |
slot + slot-scope |
统一 v-slot 或 # |
| 访问方式 |
this.$slots / this.$scopedSlots |
useSlots() 或 $slots |
| 类型判断 |
Array.isArray($slots.default) |
typeof $slots.default === 'function' |
| 调用方式 |
普通插槽直接使用,作用域插槽需调用 |
所有插槽都需调用 |