Vue指南的要点笔记(十二)

本篇记录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
8
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}

指定注册好以后,在它有效的模板内,就能在组件或元素的标签上使用了:

1
<input v-focus>

这个简单指令的完整演示,可点此查看

Vue官方指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
v-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
36
export 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这三个钩子函数的作用,还得看到真正使用到它们的例子,才能更进一步掌握。

钩子函数的参数

每一个指令的钩子函数,都会传入以下参数:

  1. el:指令所绑定的元素,可以用来直接操作 DOM 。
  2. 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 }。
  3. vnode:Vue 编译生成的虚拟节点。
  4. oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

重点来看binding这个参数,以v-on:click.stop.prevent="disabled ? save : noop"来说:

  • onbinding.name
  • save or noopbinding.value
  • disabled ? save : noopbinding.expression
  • clickbinding.arg
  • {stop: true, prevent: true}binding.modifiers

oldValue这个肯定是在VNode有更新的时候,才会传入,所以oldValue这个数据,只有在update componentUpdated这两个钩子函数中使用才有价值,因为这两个函数通过比较oldValuevalue就能判断,当前指令是否要针对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
6
name: "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
2
console.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
})