自定义指令

简介

除了核心功能默认内置的指令 (v-modelv-show),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。举个聚焦输入框的例子,如下:

当页面加载时,该元素将获得焦点 (注意:autofocus 在移动版 Safari 上不工作)。事实上,只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态。此外,你可以单击 Rerun 按钮,输入将被聚焦。

现在让我们用指令来实现这个功能:

  1. const app = Vue.createApp({})
  2. // 注册一个全局自定义指令 `v-focus`
  3. app.directive('focus', {
  4. // 当被绑定的元素插入到 DOM 中时……
  5. mounted(el) {
  6. // Focus the element
  7. el.focus()
  8. }
  9. })

如果想注册局部指令,组件中也接受一个 directives 的选项:

  1. directives: {
  2. focus: {
  3. // 指令的定义
  4. mounted(el) {
  5. el.focus()
  6. }
  7. }
  8. }

然后你可以在模板中任何元素上使用新的 v-focus property,如下:

  1. <input v-focus />

钩子函数

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用。在这里你可以做一次性的初始化设置。

  • mounted:在挂载绑定元素的父组件时调用。

  • beforeUpdate:在更新包含组件的 VNode 之前调用。

提示

我们会在稍后讨论渲染函数时介绍更多 VNodes 的细节。

  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用。

  • beforeUnmount:在卸载绑定元素的父组件之前调用

  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次。

接下来我们来看一下在自定义指令 API 钩子函数的参数 (即 elbindingvnodeprevNnode)

动态指令参数

指令的参数可以是动态的。例如,在 v-mydirective:[argument]="value" 中,argument 参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用。

例如你想要创建一个自定义指令,用来通过固定布局将元素固定在页面上。我们可以像这样创建一个通过指令值来更新竖直位置像素值的自定义指令:

  1. <div id="dynamic-arguments-example" class="demo">
  2. <p>Scroll down the page</p>
  3. <p v-pin="200">Stick me 200px from the top of the page</p>
  4. </div>
  1. const app = Vue.createApp({})
  2. app.directive('pin', {
  3. mounted(el, binding) {
  4. el.style.position = 'fixed'
  5. // binding.value is the value we pass to directive - in this case, it's 200
  6. el.style.top = binding.value + 'px'
  7. }
  8. })
  9. app.mount('#dynamic-arguments-example')

这会把该元素固定在距离页面顶部 200 像素的位置。但如果场景是我们需要把元素固定在左侧而不是顶部又该怎么办呢?这时使用动态参数就可以非常方便地根据每个组件实例来进行更新。

  1. <div id="dynamicexample">
  2. <h3>Scroll down inside this section ↓</h3>
  3. <p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
  4. </div>
  1. const app = Vue.createApp({
  2. data() {
  3. return {
  4. direction: 'right'
  5. }
  6. }
  7. })
  8. app.directive('pin', {
  9. mounted(el, binding) {
  10. el.style.position = 'fixed'
  11. // binding.arg is an argument we pass to directive
  12. const s = binding.arg || 'top'
  13. el.style[s] = binding.value + 'px'
  14. }
  15. })
  16. app.mount('#dynamic-arguments-example')

结果:

我们的定制指令现在已经足够灵活,可以支持一些不同的用例。为了使其更具动态性,我们还可以允许修改绑定值。让我们创建一个附加属性 pinPadding,并将其绑定到 <input type="range">

  1. <div id="dynamicexample">
  2. <h2>Scroll down the page</h2>
  3. <input type="range" min="0" max="500" v-model="pinPadding">
  4. <p v-pin:[direction]="pinPadding">Stick me {{ pinPadding + 'px' }} from the {{ direction }} of the page</p>
  5. </div>
  1. const app = Vue.createApp({
  2. data() {
  3. return {
  4. direction: 'right',
  5. pinPadding: 200
  6. }
  7. }
  8. })

让我们扩展我们的指令逻辑来重新计算固定元件更新的距离。

  1. app.directive('pin', {
  2. mounted(el, binding) {
  3. el.style.position = 'fixed'
  4. const s = binding.arg || 'top'
  5. el.style[s] = binding.value + 'px'
  6. },
  7. updated(el, binding) {
  8. const s = binding.arg || 'top'
  9. el.style[s] = binding.value + 'px'
  10. }
  11. })

结果:

函数简写

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

  1. app.directive('pin', (el, binding) => {
  2. el.style.position = 'fixed'
  3. const s = binding.arg || 'top'
  4. el.style[s] = binding.value + 'px'
  5. })

对象字面量

如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。

  1. <div v-demo="{ color: 'white', text: 'hello!' }"></div>
  1. app.directive('demo', (el, binding) => {
  2. console.log(binding.value.color) // => "white"
  3. console.log(binding.value.text) // => "hello!"
  4. })

在组件中使用

在 3.0 中,有了片段支持,组件可能有多个根节点。如果在具有多个根节点的组件上使用自定义指令,则会产生问题。

要解释自定义指令如何在 3.0 中的组件上工作的详细信息,我们首先需要了解自定义指令在 3.0 中是如何编译的。对于这样的指令:

  1. <div v-demo="test"></div>

将大概编译成:

  1. const vDemo = resolveDirective('demo')
  2. return withDirectives(h('div'), [[vDemo, test]])

其中 vDemo 是用户编写的指令对象,其中包含 mountedupdated 等钩子。

withDirectives 返回一个克隆的 VNode,其中用户钩子被包装并作为 VNode 生命周期钩子注入 (请参见渲染函数更多详情):

  1. {
  2. onVnodeMounted(vnode) {
  3. // call vDemo.mounted(...)
  4. }
  5. }

因此,自定义指令作为 VNode 数据的一部分完全包含在内。当在组件上使用自定义指令时,这些 onVnodeXXX 钩子作为无关的 prop 传递给组件,并以 this.$attrs 结束

这也意味着可以像这样在模板中直接挂接到元素的生命周期中,这在涉及到自定义指令时非常方便:

  1. <div @vnodeMounted="myHook" />

这与 attribute fallthrough behavior。因此,组件上自定义指令的规则将与其他无关 attribute 相同:由子组件决定在哪里以及是否应用它。当子组件在内部元素上使用 v-bind="$attrs" 时,它也将应用对其使用的任何自定义指令。