likes
comments
collection
share

vue3自定义指令从入门到不离不弃

作者站长头像
站长
· 阅读数 18

指令

在vue中,我们在写模版语法的时候用过很多v-开头的一些特殊属性,这些特殊属性就是指令(Directives)。Vue提供了许多内置指令,包括我们经常使用的v-bindv-ifv-for等。

指令的期望值为一个JavaScript表达式 (除了少数几个例外,比如v-forv-onv-slot)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。以 v-if 为例:

<p v-if="seen">Now you see me</p>

这里,v-if指令会基于表达式seen的值的真假来移除/插入该

元素。

以上就是vue官方文档中对指令的解释,除了vue官方提供的一些内置指令外,还允许我们写一写自定义指令,今天呢就带大家了解并深入如何去自定义指令来满足自己的开发需求。

自定义指令

在日常开发中,我们经常会需要去封装一些针对DOM操作的重复的逻辑到指令中,学会如何自定义指令可以提高我们的开发效率。接下来会从指令的注册到使用开始介绍,并且会介绍在使用指令的时候的注意事项。

注册

vue中指令的注册和组件的注册一样,提供了全局注册和局部注册两种方式,全局注册就是指我们定义的指令可以在项目中任意地方都可以使用,局部注册则意味着只能在注册的当前组件中使用。接下来我们就来学习这两种注册指令的方式。

全局注册

我们通过app.directive的方式全局注册指令,它有两个参数,一个是指令名称,一个是钩子函数对象。 指令名称:

  • 短横线:my-focus
  • 驼峰式:MyFocus 钩子函数对象:我们会在下面介绍
import { createApp } from 'vue'
import App from './App.vue'const app = createApp(App)
// 自动聚焦的指令
app.directive('my-focus', {
  mounted(el) {
    el.focus();
  }
})
app.mount('#app')

上面我们就注册了一个全局的my-focus指令,注册好后我们在全局都可以使用。

<template>
  <input v-my-focus />
</template>

给这个input框加上v-my-focus指令后,这个输入框渲染的时候就会自动聚焦。

在注册指令的时候,指令名称不需要加v-前缀,但是在使用的时候需要加。

局部注册

指令的局部注册就是在组件内部定义指令,使用的范围也只局限在这个组件内部。下面我们还是以自动聚焦的指令来做示范:

<script setup>
  const vMyFocus = {
    mounted: (el) => el.focus()
  }
</script><template>
  <input v-my-focus />
</template>

<script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令。上面我们定义了一个 vMyFocus 的指令,可以直接在模版中使用,但是在组件外使用的话是没效果的。

在没有使用 <script setup> 的情况下,自定义指令需要通过 directives 选项注册:

<script>
  setup() {}
  directives: {
    'my-focus': {
      mounted: (el) => el.focus()
    }
  },
</script>
<template>
  <input v-my-focus />
</template>

实际的效果和上面是一样的。

指令钩子

上面我们介绍注册指令的时候的第二个参数是钩子函数对象,我们接下来就来介绍下什么是钩子函数。 我们知道vue组件都有生命周期,在不同阶段会触发不同的生命周期函数,指令的钩子函数大家也可以简单的类比为指令的生命周期,不同的钩子函数会在绑定指令的那个元素的不同阶段去触发,一个指令的定义对象可以提供几种钩子函数 (都是可选的):

const myDirective = {
  // 在绑定元素的 attribute 前 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
}

我们上面在写自动聚焦指令的时候用到了mounted这个钩子,所以这个时候输入框已经挂载完成了,就可以实现它的聚集,但如果在beforeMount这个钩子中写的话就实现不了,因为此时输入框还没有挂载到页面中。

钩子参数

上面我们看到指令狗子函数有四个参数,我们接下来看下这四个参数的作用:

  • el:指令绑定到的元素。这可以用于直接操作 DOM。

  • binding:一个对象,包含以下属性。

    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。
    • oldValue:之前的值,仅在 beforeUpdateupdated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。

  • prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdateupdated 钩子中可用。

我们在写自动聚焦指令的时候用到了el这个参数,这个参数代表了我们绑定指令的那个元素,接下来我们主要通过案例来重点学习下binding和它的几个属性。 我们来写一个改变元素字体颜色的指令:

<script setup>
  import { ref } from 'vue';
​
  const color = ref('red')
​
  const vColor = {
    mounted: (el, binding) => {
      el.style.color = binding.value
    },
    beforeUpdate: (el, binding) => {
      el.style.color = binding.value
    }
  }
​
  const handleClick = () => {
    color.value = 'yellow'
  }
</script><template>
  <h1 v-color="color">Hello, Vue!!!</h1>
  <button @click="handleClick">点击</button>
</template>

在这个案例中,我们定义了一个v-colo指令,在元素挂载和更新前修改元素的字体颜色,我们在使用v-color指令的时候传进去了一个变量color,然后通过binding.value在指令钩子中进行了接收,这就是value属性的作用。

接下来,我们再写一个案例,我不光想要改变字体颜色,我还要这个指令支持我改变背景颜色,那么该怎么修改这个指令呢?

<script setup>
  import { ref } from 'vue';
​
  const color = ref('red')
​
  const vColor = {
    mounted: (el, binding) => {
      el.style[binding.arg] = binding.value
    },
    beforeUpdate: (el, binding) => {
      el.style[binding.arg] = binding.value
    }
  }
​
  const handleClick = () => {
    color.value = 'yellow'
  }
</script><template>
  <h1 v-color:color="color">Hello, Vue!!!</h1>
  <h1 v-color:backgroundColor="color">Hello, Vue!!!</h1>
  <h1 v-color:borderColor="color">Hello, Vue!!!</h1>
  <button @click="handleClick">点击</button>
</template>

在这个案例中,我们在三个元素的v-colo指令后面分别加了不同的样式属性,在指令钩子函数中通过binding.arg拿到了传进去的属性,并且修改了对应的样式,这个就是binding.arg的用法。 还有modifiers修饰符这个参数,我们以官方文档中的例子来介绍:

<div v-example:foo.bar="baz">

像上面的那个指令获取到的binding对象就是:

{
  arg: 'foo',
  modifiers: { bar: true },
  value: /* `baz` 的值 */,
  oldValue: /* 上一次更新时 `baz` 的值 */
}

简写

对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:

app.directive('color', (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  el.style.color = binding.value
})

注意事项

当在组件上使用自定义指令时,它会始终应用于组件的根节点,和透传 attributes 类似。

<template>
  <MyComponent v-demo="test" />
</template>
    
<!-- MyComponent 的模板 -->
<template>
  <div> <!-- v-demo 指令会被应用在此处 -->
    <span>My component content</span>
  </div>
</template>

在vue3中如果组件有多个根结点,则指令将会被忽略且抛出一个警告。

<!-- MyComponent 的模板 -->
<template>
  <!-- v-demo 将会被忽略且抛出一个警告 -->
  <div></div>
  <div></div>
</template>

所以我们尽量不要在自定义组件上使用自定义指令,除非能确定这个组件只有一个根节点。

案例

一键复制

const copy = {
  beforeMount(el, binding) {
    // 将要复制的内容当作全局变量存储
    el.copyContent = binding.value;
    el.addEventListener('click', () => {
      if (!el.copyContent) {
        return console.warn('没有要复制的内容!!!');
      }
      // 创建textarea标签
      const textarea = document.createElement('textarea');
      // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
      textarea.readOnly = 'readonly';
      textarea.style.position = 'fixed';
      textarea.style.top = '-99999px';
      // 把目标内容赋值给它的value属性
      textarea.value = el.copyContent;
      document.body.appendChild(textarea);
      // 选中值并复制
      textarea.select();
      const res = document.execCommand('Copy');
      if (res) {
        console.log('复制成功!!!')
      }
      // 移除textarea标签
      document.body.removeChild(textarea);
    })
  },
  updated(el, binding) {
    // 内容更新时及时更新全局变量的值
    el.copyContent = binding.value;
  },
  unmounted(el) {
    // 元素移除时卸载点击事件监听
    el.removeEventListener('click', () => {})
  }
}

分析:

  1. 给指令绑定的元素加上点击事件监听
  2. 在点击事件中获取传入的内容并且赋值给动态创建的textarea标签并选中
  3. 调用document.execCommand复制选中的内容,移除textarea
  4. 更新是即使替换内容,移除元素时卸载监听 使用:
<template>
  <p v-copy="要复制的内容">复制</p>
</template>

文字溢出省略

const ellipsis = {
  beforeMount(el, binding) {
    el.style.width = `${binding.arg || 100}px`
    el.style.whiteSpace = 'nowrap'
    el.style.overflow = 'hidden';
    el.style.textOverflow = 'ellipsis';
  },
}

分析:通过修改绑定元素的样式来实现文字溢出省略的效果

使用:

<template>
  <p v-ellipsis:200>内容内容内容内容内容内容内容</p>
</template>

防抖

const debounce = {
  mounted(el, binding) {
    let timer
    el.addEventListener('click', () => {
      if (timer) {
        clearTimeout(timer)
      }
      timer = setTimeout(() => {
        binding.value()
      }, 1000)
    })
  }
}

分析:给绑定的元素添加点击事件,并且定义一个定时器,每次触发会判断定时器有没有打开,打开的话就清除然后重新计时

使用:

<template>
  <button v-debounce="debounceClick">点击</button>
</template><script setup>
  const debounceClick = () => {
    console.log('防抖函数')
  }
</script>