Vue2 自定义指令

自定义指令概述

内置指令与自定义指令

在Vue框架中,系统内置了一系列指令(如 v-htmlv-modelv-for 等),这些内置指令由Vue内部提供,各自拥有独立的功能。除了使用内置指令外,开发者还可以根据业务需求自行封装指令,这类指令被称为自定义指令。常见的自定义指令包括用于自动聚焦的 v-focus、控制加载效果的 v-loading 以及实现图片懒加载的 v-lazy 等。

自定义指令的核心作用

自定义指令的主要作用是封装底层的DOM操作,从而扩展Vue的额外功能。通过将重复的DOM操作逻辑封装到指令内部,可以有效简化业务代码,提高代码的复用性和可维护性。

自定义指令的基础语法

需求场景:页面加载自动获取焦点

假设需求为:当页面加载完成后,使特定的输入框元素自动获取焦点。

传统实现方案及其局限性

  1. HTML属性方案:使用 autofocus 属性。该方案在部分浏览器(如Safari)中存在兼容性问题,不推荐作为唯一方案。
  2. DOM操作方案:在Vue组件的 mounted 生命周期钩子中,通过 this.$refs 获取DOM元素并调用其 focus() 方法。
    1
    2
    3
    mounted() {
    this.$refs.input.focus();
    }
    局限性:如果多个页面或组件都有相同的需求,这段DOM操作代码需要在每个组件中重复编写,导致代码冗余。

自定义指令的注册方式

自定义指令分为全局注册局部注册两种方式。

全局注册

全局注册的指令可以在应用中的任何组件中使用。通过 Vue.directive 方法进行注册。

1
2
3
4
5
Vue.directive('focus', {
inserted: function (el) {
el.focus();
}
});
局部注册

局部注册的指令仅在当前组件范围内有效。在组件的 directives 选项中进行定义。

1
2
3
4
5
6
7
8
9
export default {
directives: {
focus: {
inserted: function (el) {
el.focus();
}
}
}
}

核心钩子函数:inserted

在上述代码中,inserted 是指令的一个生命周期钩子函数

  • 触发时机:当指令所绑定的元素被插入到页面(DOM树)中时自动调用。其触发时机类似于组件的 mounted 钩子。
  • 参数 el:钩子函数的第一个参数 el 代表指令所绑定的DOM元素。通过该参数可以直接对DOM进行操作。

在模板中使用指令

无论是全局注册还是局部注册,定义完成后即可在模板中使用。使用方式为 v- 加上指令名称。

1
<input type="text" v-focus>

当该 input 元素被插入页面时,会自动执行 inserted 钩子中的逻辑,实现自动聚焦。

自定义指令的传值与响应式更新

需求场景:动态设置文本颜色

假设需求为:封装一个 v-color 指令,允许在绑定指令时传入不同的颜色值,从而动态设置元素的文本颜色,并且当颜色值发生变化时,视图能够自动更新。

指令传值语法与参数获取

在绑定指令时,可以通过等号(=)为指令传递具体的参数值。

1
2
<p v-color="color1">文本一</p>
<p v-color="color2">文本二</p>

在指令的钩子函数中,可以通过第二个参数 binding 对象来获取传递的值。

  • binding.value:获取指令绑定的具体值(如上述代码中的 color1color2 变量的值)。

3.3 核心钩子函数:update

Vue具有响应式特性。当指令绑定的数据值发生变化时,inserted 钩子不会再次触发。为了在数据变化时更新DOM,需要使用 update 钩子函数。

  • 触发时机:当指令所在组件的VNode更新(即指令绑定的值发生变化)时调用。
  • 作用:处理数据变化后的DOM更新逻辑。

完整代码实现

以下为实现 v-color 指令的局部注册完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default {
data() {
return {
color1: 'red',
color2: 'green'
};
},
directives: {
color: {
// 元素插入页面时触发(初始化)
inserted: function (el, binding) {
el.style.color = binding.value;
},
// 指令绑定的值变化时触发(响应式更新)
update: function (el, binding) {
el.style.color = binding.value;
}
}
}
}

通过上述实现,当 color1color2 的数据在运行时被修改(例如从 red 改为 blue),update 钩子会被触发,DOM元素的样式将自动更新。

自定义指令核心知识点总结

  1. 自定义指令的定义:用于封装DOM操作,扩展Vue功能,简化业务代码。
  2. 注册方式
    • 全局注册:使用 Vue.directive('指令名', { 配置项 })
    • 局部注册:在组件内使用 directives: { 指令名: { 配置项 } }
  3. 传值语法:通过 v-指令名="变量" 传值,在钩子函数中通过 binding.value 获取。
  4. 核心生命周期钩子
    • inserted:元素被插入页面时触发,用于执行初始化的DOM操作。
    • update:指令绑定的值发生变化时触发,用于执行数据变化后的DOM更新操作。

Vue2 自定义指令进阶:封装 v-loading 指令

需求分析与场景说明

在实际前端开发中,发送网络请求获取数据需要一定的时间。在数据尚未返回且页面未完成渲染的初始阶段,页面通常会处于空白状态。如果网络环境较差,空白状态会持续较长时间,严重影响用户体验。

需求目标
封装一个名为 v-loading 的自定义指令。当页面或组件处于数据加载状态时,在目标容器上显示 Loading 遮罩层;当数据请求完成并渲染完毕后,自动移除遮罩层。通过指令封装,实现该加载效果的高度复用。

核心实现思路

Loading 效果的本质是一个覆盖在目标容器上方的遮罩层。实现该效果的核心思路分为以下三步:

  1. 准备 CSS 遮罩层样式:定义一个 loading 类,利用 CSS 伪元素结合绝对定位生成遮罩层,并设置背景图片(如 loading.gif)。
  2. 控制遮罩层显隐:通过动态添加或移除目标容器的 loading 类名,实现遮罩层的显示与隐藏。相比直接操作 DOM 创建和删除节点,控制类名的方式更加简洁高效。
  3. 结合自定义指令封装:将 DOM 类名操作的逻辑封装到 Vue 自定义指令的钩子函数中,通过指令绑定的值(布尔值)来自动控制遮罩层的状态。

基础代码与环境准备

在开始封装指令前,需准备基础的项目代码并模拟慢网络请求场景。

  1. 安装依赖:确保项目中已安装 axios 用于发送网络请求。
  2. 模拟请求与渲染:在 Vue 组件的 created 生命周期钩子中发送请求,并通过 setTimeout 模拟 2 秒的网络延迟。数据返回后更新列表数据,触发视图渲染。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 基础数据结构与请求模拟
data() {
return {
list: [],
isLoading: true // 控制 loading 状态的变量
};
},
created() {
// 模拟慢网络请求
setTimeout(() => {
axios.get('/api/data').then(res => {
this.list = res.data;
this.isLoading = false; // 数据加载完成,关闭 loading
});
}, 2000);
}

CSS 遮罩层样式实现

利用 CSS 伪元素和定位机制实现遮罩层。需要严格遵循子绝父相(子元素绝对定位,父元素相对定位) 原则,即使用绝对定位的伪元素时,其父容器必须设置为相对定位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 目标容器必须设置相对定位 */
.box {
position: relative;
/* 其他基础样式 */
}

/* Loading 遮罩层样式 */
.loading::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8); /* 半透明白色背景 */
background-image: url('./assets/loading.gif'); /* 加载动画图片 */
background-repeat: no-repeat;
background-position: center;
z-index: 999; /* 确保遮罩层在最上层 */
}

重点说明:使用伪元素(::before::after)生成遮罩层,可以避免在 HTML 结构中插入多余的 DOM 节点,仅需通过控制 .loading 类名的添加与移除即可实现遮罩层的显隐。

自定义指令逻辑封装

在 Vue2 中,通过 directives 选项注册局部自定义指令。需要用到 insertedupdate 两个钩子函数来处理初始状态和更新状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
directives: {
loading: {
// inserted 钩子:元素插入 DOM 时调用,处理初始状态
inserted(el, binding) {
// binding.value 为指令绑定的值(如 isLoading)
if (binding.value) {
el.classList.add('loading');
} else {
el.classList.remove('loading');
}
},
// update 钩子:绑定值更新时调用,处理状态变更
update(el, binding) {
if (binding.value) {
el.classList.add('loading');
} else {
el.classList.remove('loading');
}
}
}
}

钩子函数解析

  • inserted:在绑定元素插入父节点时调用。用于根据 binding.value 的初始值决定是否在页面初次渲染时添加 loading 类名。
  • update:在绑定值发生变化时调用。当数据请求完成,状态变量变为 false 时,触发此钩子,移除 loading 类名,从而隐藏遮罩层。

模板使用

1
2
3
4
5
6
<!-- 将 v-loading 指令绑定到目标容器,并传入 isLoading 变量 -->
<div class="box" v-loading="isLoading">
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
</div>

指令复用与测试

自定义指令的核心优势在于高复用性。封装完成后,可在任意需要加载效果的容器上直接使用该指令,且各容器的状态相互独立。

1
2
3
4
5
6
7
8
9
<!-- 容器 1 -->
<div class="box" v-loading="isLoading1">
<!-- 列表内容 1 -->
</div>

<!-- 容器 2 -->
<div class="box2" v-loading="isLoading2">
<!-- 列表内容 2 -->
</div>

测试要点

  1. 确保每个使用 v-loading 指令的容器都设置了 position: relative,以保证遮罩层定位准确。
  2. 为不同的容器绑定不同的状态变量(如 isLoading1isLoading2),验证指令在不同组件或容器间的独立控制能力。

封装 v-loading 指令核心知识点总结

  1. 场景痛点:网络请求导致的页面空白期会降低用户体验,需通过 Loading 遮罩层进行过渡。
  2. CSS 实现技巧:利用伪元素绝对定位生成遮罩层,遵循子绝父相原则,通过控制类名显隐遮罩层,避免直接操作 DOM 节点。
  3. Vue2 自定义指令
    • 使用 directives 选项注册局部指令。
    • 利用 inserted 钩子处理元素初始化时的类名绑定。
    • 利用 update 钩子监听绑定值(binding.value)的变化,动态更新类名状态。
  4. 工程化思维:将通用的 DOM

Vue组件插槽(Slot)

插槽的概念与作用

在Vue组件开发中,插槽(Slot) 主要用于实现组件内部结构的自定义

在封装组件时,若组件内部的某些结构不希望被固定写死,可以通过插槽机制允许使用者在调用组件时传入自定义内容。简而言之,插槽的核心作用是让组件内部的结构支持自定义

插槽的应用场景

对话框(Dialog)组件为例:

假设系统中存在多个对话框,其整体结构(如头部标题、底部操作按钮)和样式逻辑完全一致,但中间的内容区域不同(例如:一个提示“确认要删除吗”,另一个提示“确认要退出本系统吗”)。

此时,可以将对话框封装为一个公共组件以实现代码复用。对于头部和底部等固定结构,直接在组件内部编写;而对于中间不希望写死的内容部分,则使用插槽进行占位,由组件的使用者在调用时动态传入。

插槽的基本使用步骤

插槽的基本使用分为以下两个核心步骤:

  1. 组件内占位:在封装组件时,找到需要定制的结构位置,将原有内容替换为 <slot> 标签进行占位。
  2. 使用时传入内容:在使用该组件时,在组件标签的内部编写需要传入的HTML结构或文本。这些内容最终会被插入并渲染到组件内部 <slot> 标签所在的位置。

代码实现与演示

以下基于Vue2语法演示插槽的具体实现过程。

1. 封装对话框组件(MyDialog.vue)

创建一个对话框组件,包含头部、内容区和底部。将内容区使用 <slot> 进行占位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<div class="dialog">
<!-- 头部:固定结构 -->
<div class="dialog-header">
<h3>友情提示</h3>
</div>

<!-- 内容区:使用 slot 占位,支持自定义 -->
<div class="dialog-body">
<slot></slot>
</div>

<!-- 底部:固定结构 -->
<div class="dialog-footer">
<button>确认</button>
<button>取消</button>
</div>
</div>
</template>

<script>
export default {
name: 'MyDialog'
}
</script>

<style scoped>
.dialog {
border: 1px solid #ccc;
width: 300px;
margin: 20px;
}
.dialog-header, .dialog-body, .dialog-footer {
padding: 10px;
}
</style>

2. 在父组件中使用插槽(App.vue)

在父组件中导入并注册 MyDialog 组件,通过在组件标签内部传入不同的内容,实现结构的自定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div id="app">
<!-- 第一次使用:传入纯文本内容 -->
<my-dialog>
你确认要删除吗?
</my-dialog>

<!-- 第二次使用:传入包含HTML标签的复杂结构 -->
<my-dialog>
<p>你确认要<strong>退出本系统</strong>吗?</p>
</my-dialog>
</div>
</template>

<script>
import MyDialog from './components/MyDialog.vue'

export default {
name: 'App',
components: {
MyDialog
}
}
</script>

代码解析

  • MyDialog.vue 中,<slot></slot> 充当了内容区的占位符。
  • App.vue 中,<my-dialog> 标签内部包裹的内容,都会被自动填充到 MyDialog 组件内部的 <slot> 位置进行渲染。
  • 插槽不仅支持传入纯文本,还支持传入多个标签复杂的HTML结构,具备高度的灵活性。

插槽(Slot)知识点总结

  1. 核心作用:当组件内的某部分结构不确定或希望由使用者自定义时,应使用插槽(Slot) 机制。
  2. 使用步骤
    • 第一步:在组件内部需要定制的位置,使用 <slot> 标签进行占位。
    • 第二步:在使用组件时,在组件标签内部传入具体的标签或文本内容。
  3. 渲染规则:所有在组件标签内部传入的内容,均会精确填充到组件内部 <slot> 标签所在的位置并完成渲染。

Vue2 插槽的后备内容

在组件封装过程中,通过插槽(Slot) 可以实现组件内容的自定义与定制。开发者可以在组件内部预留空间,由父组件在调用时传入具体的结构或文本。然而,在实际应用中存在一种边界情况:若父组件在调用组件时未传入任何内容,插槽所在位置将呈现空白状态。这种空白状态会导致页面布局异常,进而降低用户体验。为解决此问题,Vue 提供了插槽的后备内容机制。

插槽后备内容的概念

插槽的后备内容(Fallback Content),即插槽的默认值。在封装组件时,开发者可以为预留的 <slot> 插槽提供一段默认展示的内容。当父组件未向该插槽传入任何内容时,组件将自动渲染这段后备内容,从而避免页面出现空白区域。

语法与实现

配置插槽后备内容的语法非常直观。只需在组件模板的 <slot> 标签内部编写内容,该内部内容即会被作为默认展示的后备内容。

基本语法结构如下:

1
2
3
4
5
6
7
8
<template>
<div class="component-wrapper">
<!-- 插槽标签内部的内容即为后备内容 -->
<slot>
这里是后备内容(默认值)
</slot>
</div>
</template>

渲染规则:

  1. 当外部使用组件且未传入内容时,<slot> 标签内部的后备内容将被渲染显示。
  2. 当外部使用组件且传入了内容时,传入的内容将完全替换整个 <slot> 标签及其内部的后备内容,此时后备内容不会显示。

代码演示与逻辑分析

以下通过一个对话框组件 my-dialog 的封装与调用,详细演示插槽后备内容的实际应用。

1. 基础组件封装与正常传值

首先,封装一个包含插槽的 my-dialog 组件,并在父组件中传入自定义内容。

子组件 my-dialog 模板:

1
2
3
<div class="dialog">
<slot></slot>
</div>

父组件调用:

1
2
3
<my-dialog>
你确认要删除吗?
</my-dialog>

运行结果: 页面正常渲染,插槽位置显示传入的文本“你确认要删除吗?”。

2. 未传入内容时的空白问题

若将父组件调用时的传入内容删除,代码如下:

1
<my-dialog></my-dialog>

运行结果: 由于 <slot> 标签内部为空,且父组件未传入内容,页面插槽位置呈现空白,影响用户体验。

3. 配置后备内容

为解决空白问题,在子组件的 <slot> 标签内部添加后备内容。

修改后的子组件 my-dialog 模板:

1
2
3
4
5
<div class="dialog">
<slot>
我是默认的文本内容
</slot>
</div>

父组件调用(不传值):

1
<my-dialog></my-dialog>

运行结果: 页面插槽位置自动显示后备内容“我是默认的文本内容”,避免了空白现象。

4. 传入内容时的替换逻辑

在配置了后备内容的基础上,父组件再次传入自定义内容。

父组件调用(传入新内容):

1
2
3
<my-dialog>
你确认要退出吗?
</my-dialog>

运行结果: 页面插槽位置显示“你确认要退出吗?”。传入的结构完全替换<slot> 标签,后备内容“我是默认的文本内容”不再显示

核心知识点总结

  1. 如何给插槽设置默认的显示内容?
    在组件模板中,直接在 <slot> 标签内部填写文本或 HTML 结构。标签内部填写的内容即作为后备内容(默认值)

  2. 插槽的后备内容在何时显示?
    仅当父组件调用该组件时,未向该插槽传入具体的标签或内容时,后备内容才会被渲染显示。一旦父组件传入内容,后备内容将被传入内容完全替换。

通过掌握插槽的后备内容机制,开发者可以显著提升组件的健壮性与默认状态下的用户体验。建议结合上述代码示例进行上机实践,以加深对插槽替换逻辑的理解。

Vue 组件进阶:具名插槽

场景引入与默认插槽的局限性

在实际前端开发中,经常会遇到组件内有多处结构需要由外部传入标签进行定制的场景。以封装一个对话框组件为例,对话框通常包含头部标题、中间内容和底部按钮三个部分。

在前置课程中,通过默认插槽语法可以完成中间内容的定制。然而,默认插槽的局限性在于只能提供一个定制位置。当需求升级为不仅需要定制中间内容,还需要定制头部标题和底部按钮时,默认插槽便无法满足需求。针对这种组件内有多处结构需要外部定制的场景,必须引入具名插槽

具名插槽的核心概念

具名插槽,即具有名字的插槽。其核心思想是为组件内的多个 <slot> 标签分别命名,通过 name 属性进行区分,使每一个插槽都拥有唯一的标识。

在父组件提供定制结构时,通过特定的指令将内容精确分发到对应名称的插槽中。这种机制确保了多处结构定制的精确性和独立性。

具名插槽的使用步骤

实现具名插槽的定向分发,需要严格遵循以下两个步骤:

1. 子组件:定义具名插槽

在子组件中需要定制的部分提供多个 <slot> 标签,并为每一个 <slot> 添加 name 属性以指定名称。

注意:一旦插槽被赋予了 name 属性,即成为具名插槽。具名插槽仅支持定向分发,未明确指定分发给该插槽的内容将不会在此处渲染。

2. 父组件:分发定制内容

在父组件中使用 <template> 标签将需要定制的内容包裹成一个整体,并配合 v-slot:插槽名 指令进行定向分发。

  • 必须使用 <template> 标签进行包裹,否则 Vue 无法准确区分哪一部分结构属于同一个整体。
  • v-slot:插槽名 指令用于明确指定该 <template> 内的结构应分发至哪一个具名插槽。

代码实战演示

以下通过具体的代码示例,展示具名插槽在 Vue 2 中的标准用法。

子组件代码 (Dialog.vue)

在子组件中定义三个具名插槽,分别命名为 headcontentfoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="dialog-container">
<!-- 头部具名插槽 -->
<div class="dialog-header">
<slot name="head"></slot>
</div>

<!-- 内容具名插槽 -->
<div class="dialog-content">
<slot name="content"></slot>
</div>

<!-- 底部具名插槽 -->
<div class="dialog-footer">
<slot name="foot"></slot>
</div>
</div>
</template>

父组件代码

在父组件中引入子组件,并使用 <template> 结合 v-slot 进行内容分发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div>
<Dialog>
<!-- 分发至 head 插槽 -->
<template v-slot:head>
<h3>自定义大标题</h3>
</template>

<!-- 分发至 content 插槽 -->
<template v-slot:content>
<p>这是自定义的中间内容部分。</p>
</template>

<!-- 分发至 foot 插槽,使用简写语法 -->
<template #foot>
<button>确认</button>
<button>取消</button>
</template>
</Dialog>
</div>
</template>

<script>
import Dialog from './Dialog.vue';

export default {
components: {
Dialog
}
}
</script>

语法简写说明

在 Vue 2.6.0 及以上版本中,官方为 v-slot 指令提供了简写形式。

  • 标准语法:v-slot:插槽名
  • 简写语法#插槽名

在上述父组件代码的底部按钮部分,v-slot:foot 被简写为 #foot。在实际开发中,推荐使用简写语法以提升代码的简洁度与可读性。

具名插槽知识点总结

  1. 应用场景:当组件内存在多处不确定的结构需要外部定制时,应使用具名插槽。
  2. 定义方式:在子组件中使用多个 <slot> 标签,并通过 name 属性为其命名。未添加 name 属性的插槽称为默认插槽。
  3. 分发方式:在父组件中必须使用 <template> 标签包裹分发结构,并通过 v-slot:插槽名 指令实现定向分发。
  4. 指令简写v-slot:插槽名 可简写为 #插槽名

Vue2 作用域插槽

插槽的分类与作用域插槽的概念

插槽的分类

在 Vue 组件化开发中,插槽(Slot)主要用于内容分发。就插槽的分类而言,严格意义上仅包含以下两类:

  • 默认插槽:用于在组件内定制单一结构。当组件只需要一处内容定制时,通常使用默认插槽。
  • 具名插槽:用于在组件内定制多处不同的结构。当组件需要多个独立的内容定制区域时,通过名称进行区分和分发。

作用域插槽的本质

作用域插槽并不属于插槽的分类,而是插槽的一种传参语法

  • 定义:在定义 <slot> 标签(无论是默认插槽还是具名插槽)的同时,通过绑定属性的方式向父组件传递数据。
  • 作用:使父组件在使用插槽定制结构时,能够访问到子组件内部的数据。

作用域插槽的应用场景

在实际工程中,作用域插槽常用于封装通用组件(如表格组件、列表组件)。以表格组件为例:

  1. 数据渲染:表格的整体样式和结构相似,但数据不同。通常通过父传子(Props)将数据数组传递给子组件,子组件内部使用 v-for 动态渲染表格行。
  2. 结构定制:表格的“操作列”(如删除、查看按钮)在不同业务场景下结构不同,需要使用插槽进行结构定制。
  3. 数据依赖:操作列的按钮在注册点击事件时(如删除某一行),必须依赖当前行的唯一标识(如 id)。然而,由于组件间数据相互独立,父组件无法直接获取子组件 v-for 循环内部的局部变量(如 item)。
  4. 解决方案:此时必须使用作用域插槽,将子组件内部的行数据传递给父组件,供父组件的插槽内容使用。

作用域插槽的核心语法与步骤

作用域插槽的数据传递与接收分为三个标准步骤:

1. 子组件传值

在子组件的 <slot> 标签上,以添加属性的方式绑定需要传递的数据。

1
2
<!-- 子组件内部 -->
<slot :row="item" message="测试文本"></slot>

2. 数据收集机制

子组件在 <slot> 标签上绑定的所有属性(无论是动态绑定的变量还是静态写死的字符串),最终都会被 Vue 自动收集并封装到一个单一的对象中
以上述代码为例,收集到的对象结构如下:

1
2
3
4
{
row: { id: 2, name: '孙大明', age: 19 }, // 动态绑定的行数据
message: '测试文本' // 静态传递的字符串
}

3. 父组件接收数据

在父组件提供插槽结构时,使用 <template> 标签配合 v-slot 指令(Vue 2.6.0+ 支持 # 简写)来接收该对象。

  • 默认插槽:使用 #default="变量名" 接收。
  • 具名插槽:使用 #插槽名="变量名" 接收。
1
2
3
4
5
<!-- 父组件内部 -->
<template #default="obj">
<!-- obj 即为子组件传递过来的完整对象 -->
<button @click="deleteRow(obj.row.id)">删除</button>
</template>

注:obj 为局部变量,可自定义命名。若只需使用对象中的部分属性,支持直接使用 ES6 解构语法,如 #default="{ row }"

综合案例:封装通用表格组件

以下通过封装一个学生信息表格组件,完整演示作用域插槽的应用流程。

基础结构与父传子

首先定义子组件 MyTable.vue,通过 props 接收父组件传递的数组数据,并使用 v-for 渲染表格基础内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- 子组件:MyTable.vue -->
<template>
<table border="1">
<thead>
<tr>
<th>序号</th>
<th>姓名</th>
<th>年龄</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in data" :key="item.id">
<td>{{ index + 1 }}</td>
<td>{{ item.name }}</td>
<td>{{ item.age }}</td>
<td>
<!-- 操作列留空,准备使用插槽定制 -->
<slot></slot>
</td>
</tr>
</tbody>
</table>
</template>

<script>
export default {
props: {
data: {
type: Array,
default: () => []
}
}
}
</script>

父组件引入 MyTable 并传递数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- 父组件 -->
<template>
<div>
<MyTable :data="list1" />
<MyTable :data="list2" />
</div>
</template>

<script>
import MyTable from './MyTable.vue';

export default {
components: { MyTable },
data() {
return {
list1: [
{ id: 1, name: '张三', age: 18 },
{ id: 2, name: '李四', age: 19 }
],
list2: [
{ id: 3, name: '王五', age: 20 },
{ id: 4, name: '赵六', age: 21 }
]
};
}
}
</script>

使用默认插槽定制操作列

在父组件中,通过默认插槽为两个表格分别定制“删除”和“查看”按钮。

1
2
3
4
5
6
7
8
9
10
11
<MyTable :data="list1">
<template #default>
<button>删除</button>
</template>
</MyTable>

<MyTable :data="list2">
<template #default>
<button>查看</button>
</template>
</MyTable>

注:由于插槽位于 v-for 循环内部,父组件只需提供一次插槽结构,子组件会自动将其复用到每一行中。

跨组件数据访问问题

若直接为“删除”按钮绑定点击事件并尝试传入 item.id,会导致报错。

1
2
<!-- 错误示范 -->
<button @click="del(item.id)">删除</button>

原因分析item 是子组件 v-for 循环产生的局部变量,父组件的作用域无法直接访问子组件内部的变量。控制台会抛出 item is not defined 的错误。

使用作用域插槽传递内部数据

为解决上述问题,需在子组件的 <slot> 标签上绑定 item 数据,并在父组件中接收。

修改子组件 MyTable.vue

1
2
<!-- 将 item 重命名为 row 传递,并附加一个静态测试属性 -->
<slot :row="item" message="测试文本"></slot>

修改父组件接收数据:

1
2
3
4
5
6
7
<MyTable :data="list1">
<!-- 使用 obj 接收整个对象 -->
<template #default="obj">
<button @click="del(obj.row.id)">删除</button>
<!-- 验证对象内容:{{ obj }} -->
</template>
</MyTable>

实现删除与查看功能(含解构语法)

在父组件中完善 delshow 方法,并演示对象解构的便捷写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<template>
<div>
<!-- 表格1:使用完整对象接收 -->
<MyTable :data="list1">
<template #default="obj">
<button @click="del(obj.row.id)">删除</button>
</template>
</MyTable>

<!-- 表格2:使用 ES6 解构语法直接提取 row -->
<MyTable :data="list2">
<template #default="{ row }">
<button @click="show(row)">查看</button>
</template>
</MyTable>
</div>
</template>

<script>
import MyTable from './MyTable.vue';

export default {
components: { MyTable },
data() {
return {
list1: [
{ id: 1, name: '张三', age: 18 },
{ id: 2, name: '李四', age: 19 }
],
list2: [
{ id: 3, name: '王五', age: 20 },
{ id: 4, name: '赵六', age: 21 }
]
};
},
methods: {
// 删除功能:通过 filter 过滤掉对应 id 的数据
del(id) {
this.list1 = this.list1.filter(item => item.id !== id);
},
// 查看功能:弹窗显示当前行信息
show(row) {
alert(`姓名:${row.name},年龄:${row.age}`);
}
}
}
</script>

作用域插槽总结

  1. 核心认知:作用域插槽不是插槽的分类,而是插槽的传参语法,主要用于解决父组件定制插槽结构时,需要依赖子组件内部数据的问题。
  2. 传值机制:在子组件 <slot> 标签上以添加属性的方式传值,所有属性会被自动收集到一个单一对象中。
  3. 接收语法:在父组件使用 <template> 标签,通过 #插槽名="变量名"(Vue 2.6.0+ 语法)接收该对象。
  4. 进阶技巧:接收对象时,支持使用 ES6 解构语法(如 #default="{ row }"),以简化模板中的数据访问路径。
  5. 应用场景:在封装表格、列表等通用型组件时,作用域插槽是实现高内聚、低耦合结构定制的标准解决方案。