开门见山
按照惯例,从上帝视角看一下自定义指令在 Vue 3 中发生了哪些改变:
- 自定义指令的 API 改了名字,名字更贴近组件的生命周期
- 自定义指令可以通过子组件的
v-bind="$attr"
传递
Vue2.x 的自定义指令
在 Vue 2 中,自定义指令通过以下可选的钩子去创建:
- bind:当指令绑定在对应元素时触发。只会触发一次。
- inserted:当对应元素被插入到 DOM 的父元素时触发。
- update:当元素更新时,这个钩子会被触发(此时元素的后代元素还没有触发更新)。
- componentUpdated:当整个组件(包括子组件)完成更新后,这个钩子触发。
- unbind:当指令被从元素上移除时,这个钩子会被触发。也只触发一次。
来看个例子:
<h1 v-highlight="red">这是一串被高亮为红色的字</h1>
Vue.directive('highlight', {
bind(el, binding, vnode) {
el.style.background = binding.value;
}
});
如上是一个很灵活的做法,通过指令传值的做法,可以供开发者根据使用场景的不同提供不同的参数,以达到不同的效果。
Vue 3 的自定义指令
在 Vue 3 中,官方团队将自定义指令的 API 打造的更加“被人所熟悉”。正如你马上会看到的那样,尽管它们和组件的生命周期完全是两回事,但为了更有助于代码的可读性和风格统一,还是选择了比较接近的钩子名称:
- bind => beforeMount
- inserted => mounted
- beforeUpdate: 新的钩子,会在元素自身更新前触发
- update => 移除!
- componentUpdated => updated
- beforeUnmount: 新的钩子,当元素自身被卸载前触发
- unbind => unmounted
所以,新版的自定义指令大概会长这个样子:
const NewDirective = {
beforeMount(el, binding, vnode, prevVnode) {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeUnmount() {},
unmounted() {},
}
呐,上面的那个例子就会被改写成:
const app = Vue.createApp({});
app.directive('highlight', {
beforeMount(el, binding, vnode) {
el.style.background = binding.value;
},
});
改了名字之后,是不是更好记了呢?
细节,注意细节
Vue 3 是支持 fragments 的,也就是说,我们可以在一个组件中保留多个根节点。
<template>
<li>你好</li>
<li>我的</li>
<li>粉丝</li>
</template>
于是自定义指令就遇到了一个新的问题:自定义指令有多个根节点?
因此,现在的自定义指令已经是 Virtual DOM 节点的一部分了。当组件上挂载了自定义指令时,它的钩子会作为一个外部属性传递进组件内,最终“落地”于组件的 this.$attr
。
这也就意味着,像下面这种直接在元素上挂载生命周期钩子的写法,即将同样适用于自定义指令的钩子:
<div @vnodeMounted="myHook"></div>
自定义指令向组件内传递 hook 的行为,和组件树中自上而下传递 prop 的行为是一致的。当一个组件给自身内部的元素绑定了 v-bind="$attr"
时,也会将自身的所有自定义指令都传递给自己的子元素。