前言:
除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。
Vue2
注册全局自定义指令
main.js
import Vue from 'vue'
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
注册局部指令
index.vue
<template>
...
</template>
<script>
export default {
data() {
return {};
},
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus();
},
},
},
};
</script>
使用自定义指令
<template>
<input v-model="value" v-focus>
</template>
钩子函数
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
钩子函数参数
指令钩子函数会被传入以下参数:
- el:指令所绑定的元素,可以用来直接操作 DOM。
- binding:一个对象,包含以下 property:
name:指令名,不包括 v- 前缀。
value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。
无论值是否改变都可用。
expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符
对象为 { foo: true, bar: true }。
- vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
- oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
例:
<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>
Vue.directive('demo', {
bind: function (el, binding, vnode) {
var s = JSON.stringify
el.innerHTML =
'name: ' + s(binding.name) + '<br>' +
'value: ' + s(binding.value) + '<br>' +
'expression: ' + s(binding.expression) + '<br>' +
'argument: ' + s(binding.arg) + '<br>' +
'modifiers: ' + s(binding.modifiers) + '<br>' +
'vnode keys: ' + Object.keys(vnode).join(', ')
}
})
new Vue({
el: '#hook-arguments-example',
data: {
message: 'hello!'
}
})
效果:
Vue3
注册全局自定义指令
简化形式
对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:
<div v-color="color"></div>
main.js
const app = createApp({})
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})
非简化形式
main.js
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('color', {
mounted(el, binding, vnode) {
el.style.color = binding.value
},
})
注册局部指令
index.vue
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
vue2、vue3的生命周期对照
钩子函数 | Vue.js 2 | Vue.js 3 |
bind | 绑定时执行的逻辑 | 移除,使用beforeMount替代 |
inserted | 元素插入到父节点时执行的逻辑 | 移除,可以使用mounted来代替 |
update | 元素所在组件的VNode更新时执行的逻辑 | 使用beforeUpdate和updated替代 |
componentUpdated | 元素所在组件的VNode及其子VNode更新时执行的逻辑 | 使用beforeUpdate和updated替代 |
unbind | 解绑时执行的逻辑 | 使用unmounted替代 |
vue3自定义指令七个钩子
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
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。
- prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。
实用的指令(以vue3为例)
waterMarker指令(水印)
main.js
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
function addWaterMarker(str, parentNode, font, textColor) {
// 水印文字,父元素,字体,文字颜色
var can = document.createElement("canvas");
can.width = 200;
can.height = 150;
var cans = can.getContext("2d");
cans.rotate((-20 * Math.PI) / 180);
cans.font = font || "16px Microsoft JhengHei";
cans.fillStyle = textColor || "rgba(180, 180, 180, 0.3)";
cans.textAlign = "left";
cans.textBaseline = "Middle";
cans.fillText(str, can.width / 10, can.height / 2);
parentNode.style.backgroundImage = "url(" + can.toDataURL("image/png") + ")";
}
app.directive("waterMarker", {
beforeMount(el, binding) {
addWaterMarker(
binding.value.text,
el,
binding.value.font,
binding.value.textColor
);
},
});
app.mount("#app");
App.vue
<template>
<div class="container" v-waterMarker="{text:'零凌林',textColor:'rgba(0, 0, 0, 0.4)'}">新页面</div>
</template>
<script setup>
import { ref, reactive } from "vue";
</script>
<style scoped>
.container{
width: 100%;
height: 98vh;
border: 1px solid skyblue;
}
</style>
效果:
copy指令(点击复制)
main.js
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.directive("copy", {
beforeMount(el, { value }) {
el.$value = value;
el.handler = () => {
if (!el.$value) {
console.log("复制内容为空");
return;
}
const textarea = document.createElement("textarea");
textarea.readOnly = "readonly";
textarea.style.position = "absolute";
textarea.style.left = "-9999px";
textarea.value = el.$value;
document.body.appendChild(textarea);
textarea.select();
if (navigator.clipboard) {
navigator.clipboard.writeText(el.$value).then(() => {
console.log("复制成功", el.$value);
});
} else {
// execCommand即将被废弃
const result = document.execCommand("Copy");
if (result) {
console.log("复制成功", el.$value);
}
}
document.body.removeChild(textarea);
};
el.addEventListener("click", el.handler);
},
updated(el, { value }) {
el.$value = value;
},
unmounted(el) {
el.removeEventListener("click", el.handler);
},
});
app.mount("#app");
App.vue
<template>
<div class="container">
<input type="text" v-model="name" />
<div>{{ name }}</div>
<button v-copy="name">点击复制</button>
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
const name = ref("");
</script>
<style scoped></style>
效果:
clickOutside指令(点击元素外部)
tips:可用于点击弹窗以外范围关闭弹窗
main.js
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.directive("click-outside", {
mounted(el, binding) {
el.clickOutsideEvent = function (event) {
if (!(el === event.target || el.contains(event.target))) {
binding.value(event);
}
};
document.addEventListener("click", el.clickOutsideEvent);
},
unmounted(el) {
document.removeEventListener("click", el.clickOutsideEvent);
},
});
app.mount("#app");
App.vue
<template>
<div class="container">
<div class="box" v-click-outside="handleClickOutside"></div>
</div>
</template>
<script setup>
const handleClickOutside = (event) => {
console.log('点击了box的外部');
};
</script>
<style scoped>
.box {
width: 100px;
height: 100px;
border: 1px solid red;
}
</style>
效果:
longPress指令(长按)
main.js
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.directive("longPress", {
beforeMount: function (el, binding, vNode) {
if (typeof binding.value !== "function") {
throw "callback must be a function";
}
// 定义变量
let pressTimer = null;
// 创建计时器( 1秒后执行函数 )
let start = (e) => {
if (e.type === "click" && e.button !== 0) {
return;
}
if (pressTimer === null) {
pressTimer = setTimeout(() => {
handler();
}, 1000);
}
};
// 取消计时器
let cancel = (e) => {
if (pressTimer !== null) {
clearTimeout(pressTimer);
pressTimer = null;
}
};
// 运行函数
const handler = (e) => {
binding.value(e);
};
// 添加事件监听器
el.addEventListener("mousedown", start);
el.addEventListener("touchstart", start, { passive: true });
// 取消计时器
el.addEventListener("click", cancel);
el.addEventListener("mouseout", cancel);
el.addEventListener("touchend", cancel);
el.addEventListener("touchcancel", cancel);
},
// 当传进来的值更新的时候触发
beforeUpdate(el, { value }) {
el.$value = value;
},
// 指令与元素解绑的时候,移除事件绑定
unmounted(el) {
el.removeEventListener("click", el.handler);
},
});
app.mount("#app");
App.vue
<template>
<div class="container">
<div class="box" v-longPress="handleLongPress"></div>
</div>
</template>
<script setup>
const handleLongPress = () => {
console.log('触发了红box的长按')
}
</script>
<style scoped>
.box {
width: 100px;
height: 100px;
border: 1px solid red;
}
</style>
效果:
banRightClick指令(禁止右键点击)
main.js
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.directive("banRightClick", {
beforeMount: function (el, binding, vnode) {
// 绑定事件处理程序
el.addEventListener("contextmenu", function (event) {
event.preventDefault(); // 阻止默认的右键菜单显示
binding.value(event); // 调用指令绑定的回调函数,并传入鼠标事件对象
});
},
});
app.mount("#app");
App.vue
<template>
<div class="container">
<div class="box" v-banRightClick="handleClickRight"></div>
</div>
</template>
<script setup>
const handleClickRight = (e) => {
console.log('右键被禁止',e)
}
</script>
<style scoped>
.box {
width: 100px;
height: 100px;
border: 1px solid red;
}
</style>
效果:
rightClickMenu指令(右键点击菜单)
main.js
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.directive("rightClickMenu", {
beforeMount: function (el, binding, vnode) {
el.addEventListener("contextmenu", function (event) {
event.preventDefault();
if (binding.instance.menuVisible) {
return false; // 如果菜单已经显示,则不再重复弹出
}
binding.instance.menuVisible = true; // 设置菜单为显示状态
const menuItems = binding.value || [];
let menu = document.createElement("ul");
menu.style.position = "fixed";
menu.style.zIndex = 999;
menu.style.top = event.clientY + "px";
menu.style.left = event.clientX + "px";
menuItems.forEach((item) => {
const menuItem = document.createElement("li");
menuItem.innerText = item.text;
menuItem.addEventListener("click", function () {
binding.instance.menuVisible = false; // 点击菜单项后隐藏菜单
item.handler();
});
menu.appendChild(menuItem);
});
document.body.appendChild(menu);
function deleteMenu(e) {
if (!el.contains(e.target)) {
binding.instance.menuVisible = false;
if (menu) document.body.removeChild(menu);
menu = null;
}
}
// 点击其他地方时隐藏菜单
el.__vueDeleteMenu__ = deleteMenu;
document.addEventListener("click", deleteMenu);
});
},
unmounted(el) {
document.removeEventListener("click", el.__vueDeleteMenu__);
delete el.__vueDeleteMenu__;
},
});
app.mount("#app");
App.vue
<template>
<div class="container">
<div class="box" v-rightClickMenu="menuItems"></div>
</div>
</template>
<script setup>
const menuItems = [
{ id: 1, text: '菜单项1', handler: () => console.log('点击了菜单项1') },
{ id: 2, text: '菜单项2', handler: () => console.log('点击了菜单项2') },
{ id: 3, text: '菜单项3', handler: () => console.log('点击了菜单项3') }
]
</script>
<style scoped>
.box {
width: 100px;
height: 100px;
border: 1px solid red;
}
</style>
效果:
highlightText指令(文本高亮)
main.js
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
function setHighlight(el, binding) {
const { value } = binding;
let _keywords = value;
let _color = "red";
if (typeof value === "object") {
const { keywords = "", color = _color } = value;
_keywords = keywords;
_color = color;
}
if (_keywords) {
_keywords = _keywords.split("|");
const regex = new RegExp(`(${_keywords.join("|")})`, "gi");
const highlightedText = el.innerText.replace(
regex,
`<span style="color: ${_color};">$1</span>`
);
el.innerHTML = highlightedText;
} else {
el.innerHTML = el.innerText;
}
}
app.directive("highlightText", {
beforeMount: function (el, binding) {
setHighlight(el, binding);
},
updated: function (el, binding) {
setHighlight(el, binding);
},
});
app.mount("#app");
App.vue
<template>
<div class="container">
<input type="text" v-model="content">
<div v-highlightText="content">内容:123456789abc</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const content = ref('')
</script>
效果:
resize指令(监听元素尺寸变化)
main.js
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
/* 元素大小发生变化的回调,监听窗口大小 */
app.directive("resize", {
mounted(el, binding) {
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
binding.value(entry);
}
});
resizeObserver.observe(el);
el._resizeObserver = resizeObserver;
},
unmounted(el) {
el._resizeObserver.disconnect();
},
});
app.mount("#app");
App.vue
<template>
<div class="container">
<div class="box" v-resize="handleSizeChange"></div>
</div>
</template>
<script setup>
const handleSizeChange = () => {
console.log('红box大小发生变化了')
}
</script>
<style scoped>
.box {
width: 100%;
height: 100px;
border: 1px solid red;
}
</style>
效果:
拓展:
边界情况:访问组件实例
通常来说,建议在组件实例中保持所使用的指令的独立性。从自定义指令中访问组件实例,通常意味着该指令本身应该是一个组件。然而,在某些情况下这种用法是有意义的。
在 Vue 2 中,必须通过 vnode 参数访问组件实例:
bind(el, binding, vnode) {
const vm = vnode.context
}
在 Vue 3 中,实例现在是 binding 参数的一部分:
mounted(el, binding, vnode) {
const vm = binding.instance
}
WARNING
有了片段的支持,组件可能会有多个根节点。当被应用于多根组件时,自定义指令将被忽略,并将抛出警告。