Vue 2.0学习笔记:自定义指令
在Vue中为了更好的操作DOM元素,其内置了一些指令,比如v-model、v-if、v-show、v-text、v-html、v-for和v-bind等。除此之外,Vue也允许注册自定义指令。这些自定义指令可以说我们对普通DOM元素进行底层操作。比如@SARAH DRASNER写的一篇有关于Vue自定义指令的文章,简单易懂。今天自己也仔细撸了一下Vue中怎么实现自定义的指令。
钩子函数
创建自定义指令,在Vue中一个指令定义对象可以提供下面几个钩子函数,而这几个钩子函数都是可选的:
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新componentUpdated:指令所在组件的VNode及其子VNode全部更新后调用unbind:只调用一次,指令与元素解绑时调用
上图来自于《The Power of Custom Directives in Vue》一文。
来看一个简单的示例,看看这些钩子函数的触发时机。
至于怎么自定义一个指令,先不阐述。在Vue中通过Vue.directive('directiveName', {...})方式来注册一个指令。在实际调用的时候需要前面添加v-来使用。有关于这方面的细节,我们稍后再阐述。
Vue.directive('hello', {
// 只调用一次,指令第一次绑定到元素时调用
bind: function (el) {
console.log('触发bind钩子函数!')
},
// 被绑定元素插入父节点时调用
inserted: function (el) {
console.log('触发inserted钩子函数!')
},
// 所在组件的`VNode`更新时调用,但是可能发生在其子元素的`VNode`更新之前
update: function (el) {
console.log('触发update钩子函数!')
},
// 所在组件的`VNode`及其子元素的`VNode`全部更新时调用
componentUpdated: function (el) {
console.log('触发componentUpdated钩子函数!')
},
// 只调用一次,指令与元素解绑时调用
unbind: function (el) {
console.log('触发unbind钩子函数!')
}
})
let myComponent = {
template: '<h1 v-hello>{{ message }}</h1>',
props: {
message: String
}
}
let app = new Vue({
el: '#app',
data () {
return {
message: 'Hello! 大漠'
}
},
components: {
myComponent: myComponent
},
methods: {
update: function () {
this.message = 'Hi! 大漠'
},
uninstall: function () {
this.message = ''
},
install: function () {
this.message = 'Hello!W3cplus'
}
}
})
<div id="app">
<my-component v-if="message" :message="message"></my-component>
<div class="actions">
<button @click="update">更新</button>
<button @click="uninstall">卸载</button>
<button @click="install">安装</button>
</div>
</div>
当页面加载时就触发了bind和inserted两个钩子函数:

当我们点击“更新”按钮,将会更改message的值,会触发组件myComponent更新。

这个时候触发了update和componentUpdated两个钩子函数。
接下来我们再点击“卸载”按钮,message的数据将会置空,这个时候传给v-if的值为false,将会触发组件myComponent组件卸载。

此时触发了unbind钩子函数。
最后我们再点击“安装”按钮,message将会重新被赋值,触发myComponent组件重新安装。

这个时候触发了bind和inserted两个钩子函数。
通过上面这个简单的示例,我们对五个钩子函数的触发时间有了一个初步的认识。对于我这样的初学者而言,对bind和inserted、update和componentUpdated之间的区别还是存在一定的疑问。为了解惑,在上面的示例的基础上,再来做简单的测试。
首先来看bind和inserted之间的区别,Vue的官网文档是这样对两个钩子函数进行描述的:
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)
修改一下上例中的代码:
bind: function (el) {
console.log(el.parentNode)
console.log('触发bind钩子函数!')
},
inserted: function (el) {
console.log(el.parentNode)
console.log('触发inserted钩子函数!')
}

从上图中我们可以看出,在bind钩子函数被触发时,其父节点为null,而inserted钩子函数触发时,父节点是存在的。
再来看update和componentUpdated两个钩子函数间的区别,同样先来看官方规范的描述:
update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新componentUpdated:指令所在组件的VNode及其子VNode全部更新后调用
从描述来看,这两个钩子函数都和组件更新周期有关。同样的,基于前面的示例,修改一下update和componentUpdated两个钩子函数中的代码:
update: function (el) {
console.log(el.parentNode)
console.log(el.innerHTML)
console.log('触发update钩子函数!')
},
componentUpdated: function (el) {
console.log(el.parentNode)
}
从控制台输出的结果可以看出update和componentUpdated两个钩子函数就是组件更新前和更新后的区别。

前面也提到过了,创建Vue的自定义指令的这五个钩子函数都是可选的,不一定要全部出现。而这其中bind和update两个钩子函数是最有用的。在实际使用的时候,我们应该根据需求做不同的选择。比如在恰当的时间通过bind钩子函数去初始化实例,update钩子函数去做对应的参数更新和使用unbind钩子函数去释放实例资源占用等。
另外,这些钩子函数都带有参数,即el、binding、vnode和oldVnode。
bind(el, binding, vnode)inserted(el, binding, vnode)update(el, binding, vnode, oldVnode)componentUpdated(el, binding, vnode, oldVnode)unbind(el, binding, vnode)
接下来我们看看钩子函数的参数。
钩子函数参数
指令钩子函数会被传入以下参数:
el:指令所绑定的元素,可以用来直接操作DOMbinding:一个对象,这个对象包含一些属性,稍后列出每个属性的含义vnode:Vue编译生成的虚拟节点。有关于VNode更多的资料,可以阅读VNode相关的APIoldVnode:上一个虚拟节点,仅在update和componentUpdated两个钩子函数中可用
binding参数是一个对象,其包含以下一些属性:
name:指令名,不包括v-前缀value:指令的绑定值,如例v-hello = "1 + 1"中,绑定值为2oldValue:指令绑定的前一个值,仅在update和componentUpdated钩子中可用,无论值是否改变都可用expression:字符串形式的指令表达式。例如v-hello = "1 + 1"中,表达式为"1 + 1"arg:传给指令的参数,可选。例如v-hello:message中,参数为"message"modifiers:一个包含修饰符的对象。例如v-hello.foo.bar中,修饰符对象为{foo:true, bar:true}
除了
el之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的dataset来进行。
在很多时候,你可能想在 bind 和 update 时触发相同行为,而不关心其它的钩子。比如这样写:
Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
Vue.directive('demo', function (el, binding) {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
创建一个自定义指令
接下来,咱们来通过实例来看看怎么创建Vue的自定义指令。
模态(Modal)组件是我们经常看得到的组件。该Modal组件包含了一些文本输入框,而这个输入框是根据v-for指令动态生成。当切换到可见状态时,第一个输入框将会自动获得焦点。
实现自动获取第一个输入框得到焦点,此时需要一个自定义指令,这里把他称为v-focus指令。
// 首先需要注册一个新指令,它会自动添加v-前缀
Vue.directive('focus', {
componentUpdated: function (el, binding, vnode) {
// 当binding.value 绑定的值为true,文本框获取焦点
if (binding.value) {
el.focus()
}
}
})
添加Modal组件的基本代码:
let app = new Vue({
el: '#app',
data () {
return {
fields: [
'姓名',
'邮箱',
'地址',
'电话'
],
modalIsOpen: false
}
},
methods: {
toggleModal: function () {
this.modalIsOpen = !this.modalIsOpen
}
},
computed: {
buttonText: function () {
return this.modalIsOpen ? '关闭' : '打开'
}
}
})
在HTML中写入所需要的模板:
<div id="app">
<button @click="toggleModal">{{ buttonText }}</button>
<transition name="modal-toggle">
<div class="modal" v-show="modalIsOpen">
<input type="text" v-for="(field, key) in fields" v-focus="key === 0" :placeholder="field" />
</div>
</transition>
</div>
最终效果如下:
打开Modal框时,第一个输入框将自动会获得焦点:

大家记得<textarea>这样的表单控件元素,我们有一个属性maxlength属性,用来控制表单控件最长输入的字符数。如果不设置这个属性,我们可以使用Vue自定义的指令来完成,比如我们创建一个v-maxchars的指令。
Vue.directive('maxchars', {
bind: function (el, binding, vnode) {
let maxChars = binding.expression
let handler = function (e) {
if (e.target.value.length > maxChars) {
e.target.value = e.target.value.substr(0, maxChars)
}
}
el.addEventListener('input', handler)
}
})
比如我们来做一个Twitter发推的小组件:
let app = new Vue({
el: '#app',
data () {
return {
imgUrl: '//pbs.twimg.com/profile_images/468783022687256577/eKHcWEIk_normal.jpeg',
content: '',
totalcount: 140
}
},
computed: {
reduceCount () {
return this.totalcount - this.content.length
}
}
})
对应的模板:
<div id="app">
<div class="twitter">
<img :src="imgUrl" />
<div class="content">
<textarea v-model="content" v-maxchars="140">有什么新鲜事情?</textarea>
<p>您还可以输入{{ reduceCount }}字</p>
</div>
</div>
</div>
效果如下:
感兴趣的同学,可以体验一下。当你输入到第141个字的时候,将无法继续输入。
上面我们看到的都是通过Vue.directive('your-directive-name', {...})注删的全局自定义指令。除此之外,还可以在组件中使用directives的选项,注册一个局部的自定义指令。
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
总结
在Vue中除框架自带的一些内置指令可以操作DOM之外,还可以通过Vue的Vue.directive()和组件的directives选项,分别注册一个全局指令和局部指令。每个指令都有五个钩子函数。这些函数可以帮助我们实现所需要的自定义指令功能。文章末尾通过两个简单的示例,演示了在Vue中怎么创建自定义的指令,以及如何使用这些自定义的指令。
由于自身是Vue的初学者,如果文章中有不对之处,还请各路大婶拍正,如果你在这方面有更好的建议或经验,欢迎在下面的评论中与我们一起分享。
如需转载,烦请注明出处:https://www.w3cplus.com/vue/custom-directive.html




