自定义指令

基础

除了核心提供的默认指令集之外,Vue.js 还允许你注册自定义指令。自定义指令提供了一种机制,可以将数据更改映射到任意 DOM 行为。

你可以使用 Vue.directive(id, definition) 方法注册全局自定义指令,传入一个 **指令 ID** 和一个 **定义对象**。你也可以通过将自定义指令包含在组件的 directives 选项中来注册本地自定义指令。

钩子函数

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

示例

Vue.directive('my-directive', {
bind: function () {
// do preparation work
// e.g. add event listeners or expensive stuff
// that needs to be run only once
},
update: function (newValue, oldValue) {
// do something based on the updated value
// this will also be called for the initial value
},
unbind: function () {
// do clean up work
// e.g. remove event listeners added in bind()
}
})

注册后,你可以在 Vue.js 模板中像这样使用它(记住要添加 v- 前缀)

<div v-my-directive="someValue"></div>

当你只需要 update 函数时,你可以传入一个单一函数而不是定义对象

Vue.directive('my-directive', function (value) {
// this function will be used as update()
})

指令实例属性

所有钩子函数都将被复制到实际的 **指令对象** 中,你可以在这些函数内部以它们的 this 上下文访问它们。指令对象公开了一些有用的属性

你应该将所有这些属性视为只读,并且永远不要修改它们。你也可以将自定义属性附加到指令对象,但要小心不要意外覆盖现有的内部属性。

一个使用其中一些属性的自定义指令示例

<div id="demo" v-demo:hello.a.b="msg"></div>
Vue.directive('demo', {
bind: function () {
console.log('demo bound!')
},
update: function (value) {
this.el.innerHTML =
'name - ' + this.name + '<br>' +
'expression - ' + this.expression + '<br>' +
'argument - ' + this.arg + '<br>' +
'modifiers - ' + JSON.stringify(this.modifiers) + '<br>' +
'value - ' + value
}
})
var demo = new Vue({
el: '#demo',
data: {
msg: 'hello!'
}
})

结果

对象字面量

如果你的指令需要多个值,你也可以传入一个 JavaScript 对象字面量。请记住,指令可以接受任何有效的 JavaScript 表达式

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

字面量修饰符

当指令与字面量修饰符一起使用时,它的属性值将被解释为一个普通字符串,并直接传递到 update 方法中。update 方法也将只调用一次,因为普通字符串不能是响应式的。

<div v-demo.literal="foo bar baz">
Vue.directive('demo', function (value) {
console.log(value) // "foo bar baz"
})

元素指令

在某些情况下,我们可能希望我们的指令以自定义元素的形式使用,而不是作为属性使用。这与 Angular 的“E”模式指令的概念非常相似。元素指令为完整的组件(在指南的前面部分解释过)提供了一种更轻量级的替代方案。你可以像这样注册一个自定义元素指令

Vue.elementDirective('my-directive', {
// same API as normal directives
bind: function () {
// manipulate this.el...
}
})

然后,代替

<div v-my-directive></div>

我们可以写

<my-directive></my-directive>

元素指令不能接受参数或表达式,但它可以读取元素的属性以确定其行为。

与普通指令的一个主要区别是,元素指令是 **终端** 的,这意味着一旦 Vue 遇到一个元素指令,它将完全跳过该元素 - 只有元素指令本身才能操作该元素及其子元素。

高级选项

params

自定义指令可以提供一个 params 数组,Vue 编译器将自动提取指令绑定的元素上的这些属性。示例

<div v-example a="hi"></div>
Vue.directive('example', {
params: ['a'],
bind: function () {
console.log(this.params.a) // -> "hi"
}
})

此 API 还支持动态属性。this.params[key] 值会自动保持最新。此外,你可以在值发生更改时指定一个回调函数

<div v-example v-bind:a="someValue"></div>
Vue.directive('example', {
params: ['a'],
paramWatchers: {
a: function (val, oldVal) {
console.log('a changed!')
}
}
})

请注意,与 props 类似,指令参数遵循 JavaScript 和 HTML 之间的相同驼峰式 <=> 短横线式映射。例如,对于在模板中用作 disable-effect 的参数,你需要在 JavaScript 中访问它为 disableEffect

deep

如果你的自定义指令预期用于一个对象,并且它需要在对象内部的嵌套属性发生更改时触发 update,你需要在你的指令定义中传入 deep: true

<div v-my-directive="obj"></div>
Vue.directive('my-directive', {
deep: true,
update: function (obj) {
// will be called when nested properties in `obj`
// changes.
}
})

twoWay

如果你的指令预期将数据写回 Vue 实例,你需要传入 twoWay: true。此选项允许在指令内部使用 this.set(value)

Vue.directive('example', {
twoWay: true,
bind: function () {
this.handler = function () {
// set data back to the vm.
// If the directive is bound as v-example="a.b.c",
// this will attempt to set `vm.a.b.c` with the
// given value.
this.set(this.el.value)
}.bind(this)
this.el.addEventListener('input', this.handler)
},
unbind: function () {
this.el.removeEventListener('input', this.handler)
}
})

acceptStatement

传入 acceptStatement:true 使你的自定义指令能够接受像 v-on 一样的内联语句

<div v-my-directive="a++"></div>
Vue.directive('my-directive', {
acceptStatement: true,
update: function (fn) {
// the passed in value is a function which when called,
// will execute the "a++" statement in the owner vm's
// scope.
}
})

不过要谨慎使用,因为通常你希望避免在模板中出现副作用。

terminal

1.0.19+

Vue 通过递归遍历 DOM 树来编译模板。但是,当它遇到一个 **终端** 指令时,它将停止遍历该元素的子元素。终端指令接管了编译该元素及其子元素的任务。例如,v-ifv-for 都是终端指令。

编写一个自定义终端指令是一个高级主题,需要对 Vue 的编译管道有相当的了解,但这是可能的。你可以通过指定 terminal: true 来指定一个自定义终端指令。你可能还需要使用 Vue.FragmentFactory 进行部分编译。以下是一个自定义终端指令的示例,它编译并将其内容模板“注入”到页面上的另一个位置

var FragmentFactory = Vue.FragmentFactory
var remove = Vue.util.remove
var createAnchor = Vue.util.createAnchor
Vue.directive('inject', {
terminal: true,
bind: function () {
var container = document.getElementById(this.arg)
this.anchor = createAnchor('v-inject')
container.appendChild(this.anchor)
remove(this.el)
var factory = new FragmentFactory(this.vm, this.el)
this.frag = factory.create(this._host, this._scope, this._frag)
this.frag.before(this.anchor)
},
unbind: function () {
this.frag.remove()
remove(this.anchor)
}
})
<div id="modal"></div>
...
<div v-inject:modal>
<h1>header</h1>
<p>body</p>
<p>footer</p>
</div>

如果你想编写一个自定义终端指令,建议你阅读内置终端指令(如 v-ifv-for)的源代码,以更好地了解 Vue 的内部机制。

priority

你可以选择为你的指令提供一个优先级数字。如果没有指定优先级,将使用默认优先级 - 对于普通指令为 1000,对于终端指令为 2000。优先级较高的指令将在同一元素上的其他指令之前处理。具有相同优先级的指令将按照它们在元素属性列表中的出现顺序进行处理,尽管该顺序不能保证在不同的浏览器中保持一致。

你可以在 API 参考 中查看一些内置指令的优先级。此外,流程控制指令 v-ifv-for 在编译过程中始终具有最高优先级。