本篇记录Vue指令相关的要点。
指令的基本定义
Vue的组件是面向数据编程的一层服务,Vue指令提供的能力除了有数据相关的之外,更多的是让我们能够在恰当地时机对dom进行处理。比如我曾经经常使用到一个库:vue-infinite-scroll,这个库提供的能力就是帮助我们在列表页面进行滚动翻页地处理。列表的滚动翻页,虽然说做成自定义组件也是一种实现方式,但是做成指令可能更合适,因为这个服务跟任何组件都毫无关系。在官方指南中,提到的另外一个例子v-pin
,提供的能力是bootstrap
框架中插件affix
一模一样的能力,就是把某个内容在页面滚动的时候,自动钉在距离页面顶部或底部的位置。
要定义一个指令,非常简单,它是一个普通的js对象,就像前面了解的Vue插件一样,只要给这个普通的js对象,按指令的要求,定义它需要的钩子方法,然后就能注册到Vue中。指令所需的钩子方法有以下几个:
- bind
- inserted
- update
- componentUpdated
- unbind
这几个钩子方法不一定全部都要定义,有的指令甚至只需要用到其中一个钩子方法就能实现。比如这个让文本框自动聚焦的指令v-focus
:1
2
3
4
5
6
7
8// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
上面是一种定义全局指令的方式。也可以在组件的选项对象内通过directives
注册局部指令:1
2
3
4
5
6
7
8directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
指定注册好以后,在它有效的模板内,就能在组件或元素的标签上使用了:1
<input v-focus>
这个简单指令的完整演示,可点此查看。
Vue官方指令:1
2
3
4
5
6
7
8
9
10
11
12
13
14v-text
v-html
v-show
v-if
v-else
v-else-if
v-for
v-on
v-bind
v-model
v-slot
v-pre
v-cloak
v-once
这些是vue官方的指令,它们的源码,对于学习指令的用法比较有借鉴价值。
指令的钩子函数详解
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
bind与unbind比较好掌握,它们是一组对称api,如果在bind里面做了一些资源的获取,那么在unbind里面对应的就要做一些资源的释放,这是常见一种设计方式。另外三个钩子方法在使用的时候需要注意:
- inserted 不保证父节点已经插入到文档中,意外着此处做dom操作将变地不可靠,比如获取dom的宽高等操作都可能不是最终的渲染大小;
- update 也不保证子的 VNode 完成了更新,现在而言VNode更新意味着什么,还需要进一步学习才能知道,此处既然说子的VNode不一定完成了更新,那这个钩子函数也不是非常可靠的;
- componentUpdated 这个钩子函数保证当前VNode和子的VNode都已完成更新,相对而言,比update要可靠一些;但是这也意味着它的调用时机要比 update 晚一些
查看vue-infinite-scroll这个指令的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36export default {
bind(el, binding, vnode) {
el[ctx] = {
el,
vm: vnode.context,
expression: binding.value
};
const args = arguments;
el[ctx].vm.$on('hook:mounted', function () {
el[ctx].vm.$nextTick(function () {
if (isAttached(el)) {
doBind.call(el[ctx], args);
}
el[ctx].bindTryCount = 0;
var tryBind = function () {
if (el[ctx].bindTryCount > 10) return; //eslint-disable-line
el[ctx].bindTryCount++;
if (isAttached(el)) {
doBind.call(el[ctx], args);
} else {
setTimeout(tryBind, 50);
}
};
tryBind();
});
});
},
unbind(el) {
if (el && el[ctx] && el[ctx].scrollEventTarget)
el[ctx].scrollEventTarget.removeEventListener('scroll', el[ctx].scrollListener);
}
};
发现它实现这个翻页滚动的功能,压根就没用到inserted componentUpdate update
这三个钩子函数,而且为了保证拿到最终渲染后的dom,它还利用了mounted
这个生命周期钩子,以及nextTick
的api。inserted componentUpdate update
这三个钩子函数的作用,还得看到真正使用到它们的例子,才能更进一步掌握。
钩子函数的参数
每一个指令的钩子函数,都会传入以下参数:
- el:指令所绑定的元素,可以用来直接操作 DOM 。
- binding:一个对象,包含以下属性:
- name:指令名,不包括 v- 前缀。
- value:指令的绑定值,例如:v-my-directive=”1 + 1” 中,绑定值为 2。
- oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
- expression:字符串形式的指令表达式。例如 v-my-directive=”1 + 1” 中,表达式为 “1 + 1”。
- arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 “foo”。
- modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
- vnode:Vue 编译生成的虚拟节点。
- oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
重点来看binding
这个参数,以v-on:click.stop.prevent="disabled ? save : noop"
来说:
on
是binding.name
save or noop
是binding.value
disabled ? save : noop
是binding.expression
click
是binding.arg
{stop: true, prevent: true}
是binding.modifiers
oldValue
这个肯定是在VNode有更新的时候,才会传入,所以oldValue
这个数据,只有在update componentUpdated
这两个钩子函数中使用才有价值,因为这两个函数通过比较oldValue
与value
就能判断,当前指令是否要针对VNode的更新做出响应。
官方用来演示这些参数作用的例子(点此查看):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<div id="vue" v-demo:foo.a.b="message"></div>
<script type="text/javascript">
Vue.directive('demo', {
bind: function (el, binding, vnode) {
var s = JSON.stringify
el.innerHTML =
'name: ' + s(binding.name) + '<br>' +
'value: ' + s(binding.value) + '<br>' +
'expression: ' + s(binding.expression) + '<br>' +
'argument: ' + s(binding.arg) + '<br>' +
'modifiers: ' + s(binding.modifiers) + '<br>' +
'vnode keys: ' + Object.keys(vnode).join(', ')
}
})
new Vue({
el: '#vue',
data: {
message: 'hello!'
}
})
</script>
显示结果:1
2
3
4
5
6name: "demo"
value: "hello!"
expression: "message"
argument: "foo"
modifiers: {"a":true,"b":true}
vnode keys: tag, data, children, text, elm, ns, context, fnContext, fnOptions, fnScopeId, key, componentOptions, componentInstance, parent, raw, isStatic, isRootInsert, isComment, isCloned, isOnce, asyncFactory, asyncMeta, isAsyncPlaceholder
动态指令参数
前面的binding
参数是一个对象,arg
属性描述的是指令的参数值,比如v-on:click
这个指令生效后,binding.arg
就是click
。Vue现在支持动态指令参数,如:v-mydirective:[argument]="value"
,这意味着在使用指令的时候,参数可以写成vm实例的响应式数据。 当动态参数对应的响应数据发生更新时,指令的update以及componentUpdated这两个钩子函数会被触发,指令应当根据binding.arg
的最新值做出调整。
我认为这个特性,会增加指令开发的工作,但是对于指令的使用,会变得更加灵活可控。
对象字面量
指令使用的时候,值可以写任意有效的js表达式,比如对象字面量:1
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
钩子函数参数中的binding
参数的value
属性,始终能访问到指令绑定的数据表达式的值:1
2console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
函数简写
在很多时候,你可能想在 bind 和 update 时触发相同行为,而不关心其它的钩子。比如这样写:
1
2
3 Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})