Vue 2.0学习笔记:自定义指令

发布于 大漠

在Vue中为了更好的操作DOM元素,其内置了一些指令,比如v-modelv-ifv-showv-textv-htmlv-forv-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>

当页面加载时就触发了bindinserted两个钩子函数:

自定义指令

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

自定义指令

这个时候触发了updatecomponentUpdated两个钩子函数。

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

自定义指令

此时触发了unbind钩子函数。

最后我们再点击“安装”按钮,message将会重新被赋值,触发myComponent组件重新安装。

自定义指令

这个时候触发了bindinserted两个钩子函数。

通过上面这个简单的示例,我们对五个钩子函数的触发时间有了一个初步的认识。对于我这样的初学者而言,对bindinsertedupdatecomponentUpdated之间的区别还是存在一定的疑问。为了解惑,在上面的示例的基础上,再来做简单的测试。

首先来看bindinserted之间的区别,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钩子函数触发时,父节点是存在的。

再来看updatecomponentUpdated两个钩子函数间的区别,同样先来看官方规范的描述:

  • update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
  • componentUpdated:指令所在组件的VNode及其子VNode全部更新后调用

从描述来看,这两个钩子函数都和组件更新周期有关。同样的,基于前面的示例,修改一下updatecomponentUpdated两个钩子函数中的代码:

update: function (el) {
    console.log(el.parentNode)
    console.log(el.innerHTML)
    console.log('触发update钩子函数!')
},
componentUpdated: function (el) {
    console.log(el.parentNode)
}

从控制台输出的结果可以看出updatecomponentUpdated两个钩子函数就是组件更新前和更新后的区别。

自定义指令

前面也提到过了,创建Vue的自定义指令的这五个钩子函数都是可选的,不一定要全部出现。而这其中bindupdate两个钩子函数是最有用的。在实际使用的时候,我们应该根据需求做不同的选择。比如在恰当的时间通过bind钩子函数去初始化实例,update钩子函数去做对应的参数更新和使用unbind钩子函数去释放实例资源占用等。

另外,这些钩子函数都带有参数,即elbindingvnodeoldVnode

  • 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:上一个虚拟节点,仅在updatecomponentUpdated两个钩子函数中可用

binding参数是一个对象,其包含以下一些属性:

  • name:指令名,不包括v-前缀
  • value:指令的绑定值,如例v-hello = "1 + 1"中,绑定值为2
  • oldValue:指令绑定的前一个值,仅在updatecomponentUpdated钩子中可用,无论值是否改变都可用
  • expression:字符串形式的指令表达式。例如v-hello = "1 + 1"中,表达式为"1 + 1"
  • arg:传给指令的参数,可选。例如v-hello:message中,参数为"message"
  • modifiers:一个包含修饰符的对象。例如v-hello.foo.bar中,修饰符对象为{foo:true, bar:true}

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。

在很多时候,你可能想在 bindupdate 时触发相同行为,而不关心其它的钩子。比如这样写:

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.fedev.cn/vue/custom-directive.htmlTravis Scott x Jordan 1 Backwards Swoosh Mocha