除了Vue内置的一系列指令(如v-model或者v-show等等)之外,Vue还允许注册自定义的指令(Custom Directives)。自定义指令主要是为了重用涉及普通元素的底层DOM访问的逻辑。

一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。下面是一个自定义指令的例子,当Vue将元素插入到DOM中后,该指令会将一个class添加到元素中:

<script setup>
// 在模板中启用v-highlight
const vHighlight = {
mounted: (el) => {
el.classList.add('is-highlight')
}
}
</script>

<template>
<p v-highlight>This sentence is important!</p>
</template>

<script setup>中,任何以v开头的驼峰式命名的变量都可以当做自定义指令使用。在上述例子中,vHighlight可以在模板中以v-highlight的形式使用。

在不使用<script setup>的情况下,自定义指令需要通过directives选项注册:

export default {
setup() {
/*...*/
},
directives: {
// 在模板中启用 v-highlight
highlight: {
/*...*/
}
}
}

将一个自定义指令全局注册到应用层级也是一种常见的做法:

const app = createApp({})

//使用 v-highlight 在所在组件中都可用
app.directive('highlight', {
/* ... */
})

自定义指令使用时机

只有当所需功能只能通过直接的DOM操作来实现时,才应该使用自定义指令。

一个常见的例子是使元素获取焦点的v-focus指令。

<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>

<template>
<input v-focus />
</template>

该指令比autofocus属性更有用,因为它不仅在页面加载时有效,而且在Vue动态插入元素时也有效。

指令钩子

一个指令的定义对象可以提供几种钩子函数(都是可选的):

const myDirective = {
// 在绑定元素的attribute前或事件监听器应用前调用
create(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:之前的值,仅在beofreUpdateupdated中可用。无论值是否更改,它都可用
    • arg:传递给指令的参数(如果有的话)。例如在v-my-directive:foo中,参数就是foo
    • modifiers:一个包含修饰符的对象(如果有的话)。例如在v-my-directive.foo.bar中,修饰符对象是{foo: true, bar: true}
    • instance:使用该指令的组件实例
    • dir:指令的定义对象
  • vnode:代表绑定元素的底层VNode
  • prevVnode:代表之前的渲染中指令所绑定元素的VNode。仅在beforeUpdateupdated钩子中可用。

举个例子,像下面的指令:

<div v-example:foo.bar="baz">

binding参数会是一个这样的对象:

{
"arg": "foo",
"modifiers": { "bar": true },
"value": /* baz 的值*/,
"oldValue": /* 上一次更新时 baz 的值*/
}

和内置指令类似,自定义指令的参数也可以是动态的,举例来说:

<div v-example:[arg]="value"></div>

这里指令的参数会基于组件的arg数据属性响应式地更新。

注意⚠️:除了el之外,其他参数都是只读的,不要更改它们。

简化形式

对于自定义指令来说,一个很常见的情况是仅仅需要在mountedupdated上实现相同的行为,除此之外并不需要其他钩子。这种情况下可以直接使用一个函数来定义指令:

<div v-color="color"></div>
app.directive('color', (el, binding) => {
//这会在`mounted`和`updated` 时调用
el.style.color = binding.value
})

对象字面量

如果指令需要多个值,可以向它传递一个Javascript对象字面量。指令也可以接收任何合法的Javascript表达式。

<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive('demo', (el, binding) => {
console.log(binding.value.color) // => 'white'
console.log(binding.value.text) // => 'hello!'
})

不推荐在组件上使用

不推荐在组件上使用自定义指令。当组件具有多个根节点时可能会出现预期外的行为。

当在组件上使用自定义指令时,它会始终应用于组件的根节点,和透传attributes类似。

<MyComponent v-demo="test" />
<!-- MyComponent 的模板 -->
<div> <!-- v-demo的指令会被应用到此处 -->
<span>My component content</span>
</div>

需要注意的是组件可能含有多个根节点。当应用到一个多根组件时,指令将会被忽略且抛出一个警告。和attribute不同,指令不能通过v-bind="$attrs"来传递给一个不同的元素。