Ⅰ. 自定义指令
一、基本使用:directives
除了 Vue 内置的一系列指令 (比如 v-model 或 v-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)。
在不使用 <script setup> 的情况下,自定义指令需要通过 directives 选项注册:
-
注册:
// main.js文件 app.**directive** ('指令名', { **mounted** (el) { **// el: 指令所在的DOM元素** } }) -
使用:
<p v-指令名></p>
💥注意事项: 元素挂载后 (成为DOM树的一部分时) 自动执行 mounted钩子 。
代码示例:(当页面加载时,让元素获取焦点)
main.js文件:
app.**directive** ('focus', {
mounted(el) {
console.log(el) // 拿到input元素
el.focus()
}
})App.vue文件:
<script setup></script>
<template>
<div class="app">
<input type="text" **v-focus** />
</div>
</template>二、什么是指令钩子
上述 mounted 指的是 指令钩子函数 ,和组件的生命周期钩子同名但不是一回事。
-
组件生命周期钩子 :围绕 组件实例 。
-
指令钩子 :围绕 指令绑定的 DOM 元素 。
常见的指令钩子如下表所示:
| 阶段 | 钩子名 | 说明 |
|---|---|---|
| 绑定 | created |
指令第一次绑定到元素时调用(元素还没插入 DOM) |
| 挂载 | beforeMount |
元素即将插入 DOM 时调用 |
| 挂载完成 | mounted |
元素插入 DOM 后调用(常用,比如 el.focus()) |
| 更新前 | beforeUpdate |
元素所在组件更新前调用 |
| 更新后 | updated |
元素所在组件更新后调用 |
| 卸载前 | beforeUnmount |
元素所在组件卸载前调用 |
| 卸载后 | unmounted |
元素卸载后调用 |
三、指令钩子的参数
一个指令的定义对象可以提供几种钩子函数 (都是可选的):
const myDirective = {
// 指令第一次绑定到元素时调用(元素还没插入 DOM)
created(el, binding, vnode) {},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode) {}
}-
el:指令当前绑定到的元素。这可以用于直接操作 DOM。 -
binding:一个对象,包含以下属性:-
value:传递给指令的值。例如在v-my-directive="1 + 1"中,值是2。 -
oldValue:之前的值,仅在beforeUpdate和updated中可用。无论值是否更改,它都可用。 -
arg:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo中,参数是"foo"。 -
modifiers:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar中,修饰符对象是{ foo: true, bar: true }。 -
instance:使用该指令的组件实例。 -
dir:指令的定义对象。
-
-
vnode:代表当前绑定元素的底层 VNode。用于了解绑定的虚拟 DOM 信息,一般用得不多。 -
prevVnode:代表之前的渲染中指令所绑定元素的 VNode(只在beforeUpdate和updated中有用)。
💥注意事项: 除了 el外,其他参数都是只读的,不要更改它们 。
📌 举个例子:带参数和修饰符的自定义指令
-
背景色蓝色(
arg = "blue") -
加粗(
modifiers.bold = true) -
绑定值
msg也可以用来动态控制颜色。
app.directive('highlight', {
mounted(el, binding) {
console.log(binding)
// 默认颜色
let color = 'yellow'
// 如果传了参数(比如 :blue)
if (binding.arg) {
color = binding.arg
}
// 如果有修饰符,比如 .bold
if (binding.modifiers.bold) {
el.style.fontWeight = 'bold'
}
el.style.backgroundColor = color
}
})使用:
<p v-highlight:blue.bold="msg">Hello Vue!</p>四、绑定数据
1. 需求
实现一个 color 指令:传入不同的颜色,给标签设置文字颜色
2. 语法
-
在绑定指令时,可以通过 "等号" 的形式为指令绑定具体的参数值
<div **v-color="colorStr"** >Some Text</div> -
通过
binding.value可以拿到指令值,指令值修改会触发updated钩子app.directive('指令名', { // 挂载后自动触发一次 mounted(el, binding) { }, // 数据更新, 每次都会执行 **updated** (el, binding) { } })
3. 代码示例
main.js文件:
//
app.directive('color', {
mounted(el, binding) {
el.style.color = binding.value
},
updated(el, binding) {
el.style.color = binding.value
}
})App.vue文件:
<script setup>
import { ref } from 'vue'
const colorStr = ref('red') // 颜色
</script>
<template>
<p **v-color="colorStr"** ></p>
</template>4. 简化写法
对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为。这种情况下我们可以直接用一个箭头函数来定义指令,如下所示:
app.directive('color', (el, binding) => {
// 这会在 mounted 和 updated 时都调用
el.style.color = binding.value
})👉 这种写法其实就是 语法糖 。
Vue 规定: 如果你注册指令时传入的是一个函数,而不是对象,那么它会自动把这个函数同时当作 mounted 和 updated 两个钩子。
案例:图片懒加载
实际开发过程中,如果项目中图片过多,我们不会一次加载所有的图片,而是当图片出现在可视区的时候才去加载,比如京东、淘宝等都采用了这种方案。
解决方案:封装一个 v-lazyload 自定义指令,实现图片懒加载,从而节省资源、提高性能。
认识 IntersectionObserver
IntersectionObserver 接口(从属于Intersection Observer API)为开发者提供了一种可以异步监听目标元素与其祖先或视窗(viewport)交叉状态的手段。

完整代码
App.vue文件:
<script setup>
const imgList = [
'https://img1.baidu.com/it/u=14492133,1259363498&fm=253&fmt=auto&app=138&f=JPEG?w=667&h=500',
'https://img0.baidu.com/it/u=1764531212,1643995922&fm=253&fmt=auto&app=120&f=JPEG?w=750&h=500',
'https://img1.baidu.com/it/u=3461494820,2726880132&fm=253&fmt=auto&app=138&f=JPEG?w=773&h=500',
'https://img1.baidu.com/it/u=2991964469,2851730176&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500',
'https://img2.baidu.com/it/u=1519104236,3241953583&fm=253&fmt=auto&app=138&f=JPEG?w=781&h=500',
'https://img0.baidu.com/it/u=3431675376,3243768390&fm=253&fmt=auto&app=138&f=JPEG?w=712&h=447',
'https://img1.baidu.com/it/u=2111075854,406597938&fm=253&fmt=auto&app=138&f=JPEG?w=888&h=500',
'https://img0.baidu.com/it/u=1615464091,2840643412&fm=253&fmt=auto?w=945&h=605',
'https://img0.baidu.com/it/u=3926979850,631936366&fm=253&fmt=auto&app=120&f=JPEG?w=785&h=500',
'https://img1.baidu.com/it/u=469866567,781924764&fm=253&fmt=auto&app=120&f=JPEG?w=750&h=500'
]
</script>
<template>
<div class="container">
<img v-for="item in imgList" **v-lazyload="item"**
width="600" height="320" />
</div>
</template>
<style lang="scss">
* {
margin: 0;
}
.container {
width: 600px;
display: flex;
flex-direction: column;
margin: 0 auto;
}
</style>main.js文件中:
app.directive('lazyload', (el, binding) => {
const io = new IntersectionObserver(([entry]) => {
// entry:交叉状态对象
if(entry.isIntersecting) {
// 到这说明图片与可视区发送交叉,说明要渲染出来
el.src = binding.value
// 监听图片加载错误事件
el.addEventListener('error', (error) => {
console.log('图片加载失败', error);
})
// 停止监听,关闭监听
io.unobserve(el)
io.disconnect()
}
})
// 开启监视
**io.observe(el)**
})Ⅱ. 插槽
插槽分类:
-
默认插槽
-
具名插槽
-
作用域插槽
一、默认插槽
1. 需求
让组件内部的一些结构支持自定义,比如下面提示框中只有提示内容不同,而标题跟按钮都是不变的,要提高复用性 的话,就可以使用插槽!

2. 默认插槽的语法
-
组件内需要定制的结构部分,改用
<slot></slot>占位 -
使用组件时, 将
<MyDialog></MyDialog>写成双标签,里面包裹要替换的结构
此外,在封装组件时,可以为 <slot></slot> 提供默认内容 。
-
使用组件时,不传,则会显示
slot的默认内容 -
使用组件时,传了,则
slot整体会被换掉,从而显示传入的
3. 代码示例
MyDialog.vue文件:
<script setup>
</script>
<template>
<div class="dialog">
<div class="dialog-header">
<h3>友情提示</h3>
<span class="close">✖</span>
</div>
<div class="dialog-content">
**<slot>我是默认内容!</slot>**
</div>
<div class="dialog-footer">
<button>取消</button>
<button>确认</button>
</div>
</div>
</template>
<style scoped>
* {
margin: 0;
padding: 0;
}
.dialog {
width: 470px;
height: 230px;
padding: 0 25px;
background-color: #ffffff;
margin: 40px auto;
border-radius: 5px;
}
.dialog-header {
height: 70px;
line-height: 70px;
font-size: 20px;
border-bottom: 1px solid #ccc;
position: relative;
}
.dialog-header .close {
position: absolute;
right: 0px;
top: 0px;
cursor: pointer;
}
.dialog-content {
height: 80px;
font-size: 18px;
padding: 15px 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.dialog-footer button {
width: 65px;
height: 35px;
background-color: #ffffff;
border: 1px solid #e1e3e9;
cursor: pointer;
outline: none;
margin-left: 10px;
border-radius: 3px;
}
.dialog-footer button:last-child {
background-color: #007acc;
color: #fff;
}
</style>App.vue文件:
<template>
**<my-dialog></my-dialog>**
**<my-dialog>你确认要进行删除操作么?</my-dialog>**
</template>
<script setup>
import MyDialog from './components3/MyDialog.vue';
</script>二、具名插槽
1. 需求
一个组件内有多处结构,需要外部传入标签,进行定制

比如上面的弹框中有三处不同之处,但是默认插槽只能定制一处内容,此时就需要用到具名插槽!
2. 具名插槽的语法
-
多个
slot使用name属性区分 -
使用
<template>配合v-slot:名字来匹配对应插槽- 简写: 由于
v-slot写起来太长,vue 给我们提供一个简单写法,将v-slot:名字直接简写为#名字
- 简写: 由于
3. 代码示例
MyDialog.vue文件:
<script setup>
</script>
<template>
<div class="dialog">
<div class="dialog-header">
**<slot name="header"><h3>默认标题</h3></slot>**
<span class="close">✖</span>
</div>
<div class="dialog-content">
**<slot name="content">我是默认内容!</slot>**
</div>
<div class="dialog-footer">
<button>取消</button>
<button>确认</button>
</div>
</div>
</template>
<style scoped>
* {
margin: 0;
padding: 0;
}
.dialog {
width: 470px;
height: 230px;
padding: 0 25px;
background-color: #ffffff;
margin: 40px auto;
border-radius: 5px;
}
.dialog-header {
height: 70px;
line-height: 70px;
font-size: 20px;
border-bottom: 1px solid #ccc;
position: relative;
}
.dialog-header .close {
position: absolute;
right: 0px;
top: 0px;
cursor: pointer;
}
.dialog-content {
height: 80px;
font-size: 18px;
padding: 15px 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.dialog-footer button {
width: 65px;
height: 35px;
background-color: #ffffff;
border: 1px solid #e1e3e9;
cursor: pointer;
outline: none;
margin-left: 10px;
border-radius: 3px;
}
.dialog-footer button:last-child {
background-color: #007acc;
color: #fff;
}
</style>App.vue文件:
<template>
<my-dialog></my-dialog>
<my-dialog>
**<template v-slot:header>**
<h3>友情提示</h3>
**</template>**
**<template #content>**
<p>请输入正确的手机号</p>
**</template>**
</my-dialog>
</template>
<script setup>
import MyDialog from './components4/MyDialog.vue';
</script>
<style>
body {
background-color: #b3b3b3;
}
</style>
三、作用域插槽(scoped slot)
1. 作用
所谓 "作用域",指的是 子组件的数据可以暴露出来,让父组件在插槽里用 。
带数据的插槽,可以让组件功能更强大、更灵活、复用性更高;用 slot 占位的同时,还可以给 slot 绑定数据,将来使用组件时,不仅可以传内容,还能使用 slot 带来的数据。
2. 场景
以 "表格 + 作用域插槽" 这个经典应用为例:
-
如果 没有作用域插槽 ,则子组件在循环代码的时候,相当于写死了,如果有的表格需要在 "操作" 中显示删除功能,而有的需要显示查看功能,则该情况是做不到的!
-
但是 **有作用域插槽 ** 的话,则 子组件只需要负责循环列表 ,而具体每个元素渲染什么工作,可以通过作用域插槽将数据传给父组件,让父组件是控制元素输出的内容 ,这样子父组件需要输出什么,就用输出什么,提高了组件的灵活性!

3. 使用方式
-
在 **子组件 ** 中,给
slot标签添加属性,用这种方式暴露数据给外部<slot a="hello" b="liren" :c=40></slot>- 所有上述添加的属性,都会被收集到一个对象中,该对象如下所示:
{ a: 'hello', b: 666 }
- 所有上述添加的属性,都会被收集到一个对象中,该对象如下所示:
-
然后 父组件 在
<template>中,通过#插槽名= "obj"接收(默认插槽名为default)**<!-- obj会收集 slot 上绑定的所有自定义属性 -->** <template #default="obj"> {{ obj }} </template>

4. 代码示例
MyTable.vue文件:
<script setup>
// 接收父组件的数据
const props = defineProps({
data: {
type: Array,
default: () => []
}
})
</script>
<template>
<table class="my-table">
<thead>
<tr>
<th>序号</th>
<th>姓名</th>
<th>年纪</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in props.data" :key="item.id">
<td>{{ index + 1 }}</td>
<td>{{ item.name }}</td>
<td>{{ item.age }}</td>
<td>
**<slot :index="index"></slot>**
</td>
</tr>
</tbody>
</table>
</template>
<style>
.my-table {
width: 450px;
text-align: center;
border: 1px solid #ccc;
font-size: 24px;
margin: 30px auto;
}
.my-table thead {
background-color: #1f74ff;
color: #fff;
}
.my-table thead th {
font-weight: normal;
}
.my-table thead tr {
line-height: 40px;
}
.my-table th,
.my-table td {
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
}
.my-table td:last-child {
border-right: none;
}
.my-table tr:last-child td {
border-bottom: none;
}
.my-table button {
width: 65px;
height: 35px;
font-size: 18px;
border: 1px solid #ccc;
outline: none;
border-radius: 3px;
cursor: pointer;
background-color: #ffffff;
margin-left: 5px;
}
</style>App.vue文件:
<script setup>
import { ref } from 'vue'
import MyTable from './components6/MyTable.vue'
const tableData1 = ref([
{ id: 11, name: '狗蛋', age: 18 },
{ id: 22, name: '大锤', age: 19 },
{ id: 33, name: '铁棍', age: 17 }
])
const tableData2 = ref([
{ id: 21, name: 'Jack', age: 18 },
{ id: 32, name: 'Rose', age: 19 },
{ id: 43, name: 'Henry', age: 17 }
])
const del = (index) => {
if(window.confirm("确认删除吗?")) {
tableData1.value.splice(index, 1)
}
}
const check = (index) => {
alert(JSON.stringify(tableData2.value[index]))
}
</script>
<template>
<MyTable **:data="tableData1"** >
**<template #default="obj">**
<button @click="del(obj.index)">删除</button>
**</template>**
</MyTable>
<MyTable **:data="tableData2"** >
<**template #default="obj">**
<button @click="check(obj.index)">查看</button>
**</template>**
</MyTable>
</template>
<style>
body {
background-color: #fff;
}
</style>Ⅲ. 综合案例

需求说明:
-
my-table表格组件封装
-
动态传递表格数据渲染
-
表头支持用户自定义
-
主体支持用户自定义
-
-
my-tag标签组件封装
-
双击显示输入框,输入框获取焦点
-
失去焦点,隐藏输入框
-
回显标签信息
-
内容修改,回车修改标签信息
-
App.vue文件:
<template>
<my-table **:data="goodsList"** >
<template **#header** >
<th>序号</th>
<th>封面</th>
<th>名称</th>
<th>操作</th>
</template>
<template **#default="{ item, index }"** >
<td>{{ index + 1 }}</td>
<td><img :src="item.picture" /></td>
<td>{{ item.name }}</td>
<td>
**<my-tag v-model="item.tag"></my-tag>**
</td>
</template>
</my-table>
</template>
<script setup>
import MyTable from './components7/MyTable.vue';
import MyTag from './components7/MyTag.vue';
import {ref} from 'vue'
// 商品列表
const goodsList = ref([
{
id: 101,
picture: 'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg',
name: '梨皮朱泥三绝清代小品壶经典款紫砂壶',
tag: '茶具'
},
{
id: 102,
picture: 'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg',
name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌',
tag: '男鞋'
},
{
id: 103,
picture: 'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png',
name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm',
tag: '儿童服饰'
},
{
id: 104,
picture: 'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg',
name: '基础百搭,儿童套头针织毛衣1-9岁',
tag: '儿童服饰'
}
])
</script>
<style lang="scss">
#app {
width: 1000px;
margin: 50px auto;
img {
width: 100px;
height: 100px;
object-fit: contain;
vertical-align: middle;
}
td:last-child {
width: 150px;
}
}
</style>MyTable文件:
<script setup>
// 接收父组件的商品数据
const props = defineProps({
data: {
type: Array,
default: () => []
}
})
</script>
<template>
<table class="my-table">
<thead>
<tr>
<!-- 表头使用具名插槽 -->
**<slot name="header"></slot>**
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in props.data" :key="item.id">
<!-- 表体使用默认插槽 -->
**<slot :item="item" :index="index"></slot>**
</tr>
</tbody>
</table>
</template>
<style lang="scss">
.my-table {
width: 100%;
border-spacing: 0;
img {
width: 100px;
height: 100px;
object-fit: contain;
vertical-align: middle;
}
th {
background: #f5f5f5;
border-bottom: 2px solid #069;
}
td {
border-bottom: 1px dashed #ccc;
}
td,
th {
text-align: center;
padding: 10px;
transition: all .5s;
&.red {
color: red;
}
}
.none {
height: 100px;
line-height: 100px;
color: #999;
}
}
</style>MyTag.vue文件:
<script setup>
import { nextTick, ref } from 'vue';
// 与父组件的双向绑定数据,可读可写
**const tag = defineModel()**
const isEdit = ref(false) // false表示显示模式,true表示编辑模式
const input_ref = ref(null) // 输入框的引用
const inputText = ref('') // 输入框的内容
// 双击标签后,修改输入框状态,生成输入框焦点
const changeStatus = () => {
isEdit.value = true
if(isEdit.value === true) {
nextTick(() => {
input_ref.value.focus()
})
}
}
// 输入框回车后逆转状态
const updateTag = () => {
if(inputText.value) {
tag.value = inputText.value
inputText.value = ''
}
isEdit.value = false
}
</script>
<template>
<div class="my-tag">
<input class="input" type="text" placeholder="输入标签"
ref="input_ref"
**v-if="isEdit"**
v-model.trim="inputText"
@keyup.enter="updateTag"
/>
<div class="text" @dblclick="changeStatus" **v-else** >
**{{ tag }}**
</div>
</div>
</template>
<style lang="scss" scoped>
.my-tag {
cursor: pointer;
.input {
appearance: none;
outline: none;
border: 1px solid #ccc;
width: 100px;
height: 40px;
box-sizing: border-box;
padding: 10px;
color: #666;
&::placeholder {
color: #666;
}
}
}
</style>