一、概述
文档
自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。其实主要就是封装作用于DOM节点的可复用逻辑
二、生命周期钩子以及钩子参数
1、生命周期
- 与vue生命周期一样,从初始化到更新最后卸载
<script setup>
// 使用驼峰命名法
const vMyDirect = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
</script>
<template>
<div v-myDirect>我是一个div</div>
</template>
- 简写,大多数情况下,上述生命周期钩子仅可用到mounted以及updated,故此可以继续简化为
<script setup>
// 使用驼峰命名法
const vMyDirect = (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
console.log(el, binding)
}
</script>
<template>
<div v-myDirect>我是一个div</div>
</template>
2、钩子参数
- 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。
- prevNode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。
三、应用
1、局部注册
- 即组件内部注册,内部使用
- 直接在组件内部,v开头驼峰命名,即可直接使用
<script setup>
import { onMounted, reactive, ref, computed, watch, nextTick } from 'vue';
// 局部自定义指令
// https://cn.vuejs.org/guide/reusability/custom-directives.html
defineOptions({name: ''});
const props = defineProps({});
const emits = defineEmits(['on-ok']);
onMounted(() => {});
const spanValue = ref('');
const copyValue = ref('');
// 使用驼峰命名法
const vCopy = {
created() {},
beforeMount() {},
// 加载
mounted(el, binding) {
copyValue.value = binding.value;
const suffix = el.getAttribute('copy-value-suffix');
el.addEventListener('click', function() {
copyToClipboard(copyValue.value, suffix);
});
},
beforeUpdate() {},
// 更新
updated(el, binding) {
copyValue.value = binding.value;
},
// 卸载前
beforeUnmount(el) {
el.removeEventListener('click', function() {
copyToClipboard();
});
},
unmounted() {}
}
function copyToClipboard(text, suffix) {
if(!text) {
ElNotification.warning({
message: '文本为空',
})
return;
}
try {
navigator.clipboard.writeText(text + suffix)
.then(function() {
console.log(text.split('.'));
ElNotification.success({
title: '复制成功',
message: '后缀名为' + suffix,
})
})
.catch(function(err) {
ElNotification.error({
message: '复制失败',
})
});
} catch (error) {
ElNotification.warning({
message: '当前浏览器不支持复制功能',
})
}
}
// 子组件暴露
defineExpose({});
</script>
<template>
<div>
<el-input v-focus v-model="spanValue" style="width: 240px" placeholder="输入要复制的文本" />
<a href="javascript:" copy-value-suffix=".exe" v-copy="spanValue">点击我进行复制</a>
</div>
</template>
<style lang="less" scoped>
div {
cursor: pointer;
}
</style>
2、全局注册
(1)、单独注册
- 直接在main.js内,通过app.directive注册后即可在全局内任何模块内使用
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
// 全局注册指令,模式一:单个注册
// 自动聚焦(v-focus)
app.directive('focus',{
mounted: (el) => {
setTimeout(() => {
// el.focus();
el.querySelector('input').focus();
}, 200);
}
})
// 防抖(v-debounce)
app.directive('debounce', {
mounted(el, binding) {
// 没有绑定函数抛出错误
if (!(binding.value instanceof Function)) {
throw '未绑定回调函数'
}
let timer
el.addEventListener('click', () => {
if (timer) clearTimeout(timer)
timer = setTimeout(_ => {
binding.value()
}, 1000)
})
},
beforeUnmount(el, binding) {
// 一次性将元素上的所有事件监听器移除
for (const eventType of Object.keys(el)) {
if (element[eventType] instanceof Array) {
element[eventType].length = 0;
}
}
},
})
app.use(createPinia()).use(ElementPlus)
app.use(router)
app.mount('#app')
(2)、 批量注册
- 一般情况下都是在src目录下创建directive目录,创建index.js文件
- 而后就可以批量编写指令,之后全部放在一个对象中,export default导出,最后再在main.js中引入后遍历对象,批量app.directive注册指令
- 需要注意的是与局部注册不一样的是,通过directive注册的指令,无需以v开头的驼峰命名
// src/directive/index.js
const debounce = {
mounted(el, binding) {
// 没有绑定函数抛出错误
if (!(binding.value instanceof Function)) {
throw '未绑定回调函数'
}
let timer
el.addEventListener('click', () => {
if (timer) clearTimeout(timer)
timer = setTimeout(_ => {
binding.value()
}, 1000)
})
},
beforeUnmount(el, binding) {
// 一次性将元素上的所有事件监听器移除
for (const eventType of Object.keys(el)) {
if (element[eventType] instanceof Array) {
element[eventType].length = 0;
}
}
},
}
const directive = {
mounted: (el) => {
setTimeout(() => {
// el.focus();
el.querySelector('input').focus();
}, 200);
}
}
export default { debounce, directive }
// main.js
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import project from '@/directive/index.js'
console.log('project', project);
const app = createApp(App)
// 全局注册指令,模式二:批量注册,直接引入js文件,通过object.keys()遍历,后directive注册
Object.keys(project).forEach((key) => {
app.directive(key, project[key])
})
app.use(createPinia())
app.use(router)
app.mount('#app')
(3)、批量注册,配合插件使用
- 配合插件可实现批量注册全局指令
- 插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码
- 一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use() 的额外选项作为参数
- 同上创建src/directive/index.js目录,而后在directive目录内创建project/index.js目录
- project目录内创建index.js,用于封装批量编写的指令,最后导出到directive/index.js
- directive/index.js引入后,批量注册指令,最后导出至mian.js使用app.use安装即可
// src/directive/project/index.js
const debounce = {
mounted(el, binding) {
// 没有绑定函数抛出错误
if (!(binding.value instanceof Function)) {
throw '未绑定回调函数'
}
let timer
el.addEventListener('click', () => {
if (timer) clearTimeout(timer)
timer = setTimeout(_ => {
binding.value()
}, 1000)
})
},
beforeUnmount(el, binding) {
// 一次性将元素上的所有事件监听器移除
for (const eventType of Object.keys(el)) {
if (element[eventType] instanceof Array) {
element[eventType].length = 0;
}
}
},
}
const directive = {
mounted: (el) => {
setTimeout(() => {
// el.focus();
el.querySelector('input').focus();
}, 200);
}
}
export default { debounce, directive }
// src/directive/index.js
import directive from "./project/index.js"; // 引入需要的指令
const directives = {
// 一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。
// 他有两个参数,安装它的应用实例(app)和 app.use 安装时传递的额外选项(options)
install: function (app, options) {
// 常用 app.component() 、 app.directive() 、app.provide() 、app.config.globalProperties
Object.keys(directive).forEach((key) => {
app.directive(key, directive[key]); // 注册
});
}
};
// 抛出,到main.js中引入,通过use注册为插件
export default directives;
// main.js
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import directive from '@/directive/index.js'
import App from './App.vue'
import router from './router'
// 全局注册指令,模式三:配合插件安装实现批量注册
app.use(directive, {a: '121'});
app.use(createPinia())
app.use(router)
app.mount('#app')
四、其他
- 除了一些偏向于逻辑的指令,可否实现偏向于组件的指令(例如气泡提示,对话窗口之类的),答案是肯定的
- 首先要先了解渲染函数 & JSX
- 在绝大多数情况下,Vue 推荐使用模板语法来创建应用。然而在某些使用场景下,我们真的需要用到 JavaScript 完全的编程能力。这时渲染函数就派上用场了。
- 在了解了渲染函数后,我们便可以通过js在mounted中直接将该组件创建为一个虚拟dom,然后再创建一个真实dom节点作为容器,最后再将该虚拟dom挂载到容器内,最后将该容器渲染到body中
- src/directive/project目录下创建.vue文件,例如气泡提示组件myPopover.vue
// src/directive/project/myPopover.vue
<script setup>
import { onMounted, reactive, ref, computed, watch, nextTick, useSlots, inject } from 'vue';
defineOptions({ name: '' });
const props = defineProps({
id: {required: true, type: String, default: ''},
virtualRef: {required: true},
trigger: {type: String, default: 'hover'},
placement: {type: String, default: 'top'},
width: {type: Number, default: 300},
hideAfter: {type: Number, default: 300},
showAfter: {type: Number, default: 100},
})
const emits = defineEmits(['on-ok']);
onMounted(() => {
console.log('id', props.id);
// console.log('myPopover', useSlots().mySlot ? true : false);
});
const a = inject('a')
console.log('a', a);
// 子组件暴露
defineExpose({});
const show = (event) => {
console.log(event);
}
</script>
<template>
<div>
<el-popover virtual-triggering v-bind="props" @show="show">
<div class="projectBox" ref="projectRef">
我是气泡弹窗
</div>
</el-popover>
</div>
</template>
<style lang="less" scoped></style>
- 导出到src/directive/project/indx.js内
- 引入createVNode、 render
- 将组件创建一个虚拟dom节点以及一个容器节点,最后将该容器渲染到body中
// src/directive/project/indx.js
import {createVNode, render, resolveComponent} from "vue";
import myPopoverEl from './myPopover.vue'
let container;
const myPopover = {
mounted(el, binding) {
// 创建一个myPopoverEl虚拟节点
const vnode = createVNode(myPopoverEl, {virtualRef: el, ...binding.value});
// 而后创建一个容器用于渲染虚拟节点
container = document.createElement('div');
render(vnode, container);
// 最后将该容器渲染到body中
document.body.appendChild(container);
},
beforeUnmount() {
// 卸载时记得将容器从body中移除
document.body.removeChild(container);
}
}
export default { myPopover }
- 使用
<el-button type="primary">
<span v-myPopover="{id: '124892749', placement: 'bottom'}">popover</span>
</el-button>