在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
:指令所绑定的元素,可以用来直接操作DOM -
binding
:一个对象,这个对象包含一些属性,稍后列出每个属性的含义 -
vnode
:Vue编译生成的虚拟节点。有关于VNode
更多的资料,可以阅读VNode
相关的API -
oldVnode
:上一个虚拟节点,仅在update
和componentUpdated
两个钩子函数中可用
binding
参数是一个对象,其包含以下一些属性:
-
name
:指令名,不包括v-
前缀 -
value
:指令的绑定值,如例v-hello = "1 + 1"
中,绑定值为2
-
oldValue
:指令绑定的前一个值,仅在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的初学者,如果文章中有不对之处,还请各路大婶拍正,如果你在这方面有更好的建议或经验,欢迎在下面的评论中与我们一起分享。

大漠
常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《 图解CSS3:核心技术与案例实战 》。
如需转载,烦请注明出处: https://www.w3cplus.com/vue/custom-directive.html
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。