likes
comments
collection
share

Vue 自定义指令教程

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

什么是指令

指令(Directive)是 Vue 中的一个重要概念,在 Vue 中提供了一套为数据驱动视图更为方便的操作,这些操作被称为指令系统,当我们想对普通 DOM 元素进行底层操作时,就会用到自定义指令。它是一种带有特殊前缀 v- 的特殊行内属性。 指令的作用是当其表达式的值改变时,将其产生的连带影响,响应式地作用于DOM上。 Vue.js内置了很多指令,例如 v-if、v-for、v-bind、v-on等等,这些指令可以帮助我们更方便地操作DOM。 除了内置指令以外,Vue.js还支持自定义指令。通过自定义指令,我们可以扩展Vue.js的功能,实现一些特殊的需求。

自定义指令

除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。 自定义指令,为我们在实现功能时提供了更高的可配置化方式,拓展了 Vue 的功能。 Vue 自定义指令有全局注册和局部注册两种方式:

全局注册

通过 Vue.directive() 实现指令的全局注册,在入口文件中进行 Vue.use() 调用。 来实现一个简单的表单自动聚焦功能:

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()  // 页面加载完成之后自动让输入框获取到焦点的小功能
  }
})

Vue.directive 的使用方式

  • 指令的名字(不需要写上v-前缀)
  • 对象数据,也可以是一个指令函数

局部注册

在组件中的 option 配置项中设置 directive 属性

directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能
    }
  }
}

不论是通过全局注册还是组件内部注册完成的指令,都可以使用 v-focus 行内属性去使实现表单的自动聚焦。

<!-- 当页面加载时,该元素将获得焦点 -->
<input v-focus />

指令中的钩子函数

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

  • bind:只调用一次,指令第一次绑定到元素时调用。定义绑定时的初始化操作,如样式的设置。
// html
<div v-red></div>

// js
Vue.directive('red', {
  bind: (el, binding) => {
    el.style.background = 'red';
  }
});
  • inserted:

被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定会被插入 DOM 中)。这里一般执行和 JS 行为有关的操作,比如用来给元素添加一些监听事件

// html
<span v-down={ url: 'xx', name: 'xx'} />

// js
Vue.directive('down', {
  inserted: (el, binding) => {
    el.addEventListener('click', () => {
      // 执行下载事件
    });
  }
});
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。

  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • unbind:只调用一次,指令与元素解绑时调用。

钩子函数的执行顺序:

bind =>> inserted =>> updated =>> componentUpdated =>> bind

如果仅在 bind 和 update 时触发相同行为,而不关心其它的钩子,可以进行函数简写:

Vue.directive('bg-color', function (el, binding) {
  el.style.backgroundColor = binding.value
})

钩子函数参数

在钩子函数中,我们可以通过参数访问到元素、指令的值、绑定的组件实例等信息, 并在其中执行一些操作,例如修改元素的样式、绑定事件监听器等。 指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM。
  • binding:一个对象,包含以下 property:
    • 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:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

除了 el 之外,其它参数都应该是只读属性,切勿进行修改。

vnode返回的是一个对象,以下是常用的属性:

  • tag:当前节点标签名。文本也会被视为一个 vnode,保存在 children 中,并且其 tag 值为undefined
  • data:当前节点数据(VNodeData类型),class、id等HTML属性都放在了data中
  • children:当前节点子节点
  • text:节点文本信息
  • elm:当前节点对应的真实DOM节点
  • context:当前节点上下文,指向了Vue实例
  • parent:当前节点父节点
  • componentOptions:组件配置项

注意:在钩子函数中使用 this 关键字无法找到 Vue 实例,此时需要使用 vnode.context

如果指令需要多个值,可以传入一个 JavaScript 对象

因为在指令函数中能够接受所有合法的 JavaScript 表达式。

<div v-demo="{ color: 'white', text: 'hello!' }"></div>

Vue.directive('demo', function (el, binding) {
  console.log(binding.value.color) // => "white"
  console.log(binding.value.text)  // => "hello!"
})

动态指令参数

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

<div id="dynamicexample">
  <h3>Scroll down inside this section ↓</h3>
  <p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>

可以通过 binding.arg 获取动态设置的指令进行初始化设置。

Vue.directive('pin', {
  bind: function (el, binding, vnode) {
    el.style.position = 'fixed'
    var s = (binding.arg == 'left' ? 'left' : 'top')
    el.style[s] = binding.value + 'px'
  }
})

new Vue({
  el: '#dynamicexample',
  data: function () {
    return {
      direction: 'left'
    }
  }
})

钩子间的数据共享

在Vue自定义指令的钩子函数之间使用指令上下文对象(directive context object)来实现数据的共享。 指令上下文对象是一个包含在整个指令执行期间传递的对象,可以在钩子函数之间传递数据。 在钩子函数中设置指令上下文对象的属性,然后在另一个钩子函数中访问该属性以获取数据。

Vue.directive('my-directive', {
  bind: function(el, binding, vnode) {
    vnode.context.$myData = 'some data';
  },
  update: function(el, binding, vnode) {
    var myData = vnode.context.$myData;
    // do something with myData
  }
});

在 bind 钩子中设置了指令上下文对象的 $myData 属性, 在 update 钩子函数中访问该属性以获取数据。 注意:我们将上下文对象存储在 vnode.context 中,是因为 vnode 是虚拟节点对象,它包含了指令所在的组件实例。

应用场景

图片懒加载

在之前 长列表优化的具体实现 !这篇文章中提到过图片懒加载的两种方式,

  • 监听 scroll + 节流
  • IntersectionObserver (交叉观察器)+ 自定义指令

这里就让我们再熟悉一下自定义指令的实现吧

当网页中出现了大量请求图片资源时,往往会影响页面加载速度,造成用户的不良体验。 为解决这个问题,我们大多数情况下会采用懒加载的方式来减轻服务器的压力,优先加载可视区域的内容,只对进入可视区的图片进行请求从而提高性能。

懒加载实现原理: 通过 IntersectionObserver (交叉观察器),监听元素是否进入可视区,当元素进入可视区时设置图片的 src 属性请求图片资源。

通过图片懒加载指令,为 Dom 元素绑定 IntersectionObserver 监听器

import Vue from 'vue'

// 懒加载
const lazyLoad = (el, binding) => {
  // 初始化监听器
  const observer = new IntersectionObserver((entries, observe) => {
        console.log('监听的',el)
        //  元素进入可视区时触发的回调 
        entries.forEach(item => {
            let target = item.target
            if(item.isIntersecting) {
               // 为图片设置传入 src 属性 
              target.src = binding.value
              // 取消观察,后续图片将不再触发 IntersectionObserver 的回调
              observe.unobserve(item.target)
                
            }
        })
    },{root:document.getElementById('imgWarp')})
  // 为IntersectionObserver绑定监听元素 
  observer.observe(el)
}

Vue.directive('lazy', {
    inserted: lazyLoad,
    updated: lazyLoad
})

将配置的指令引入 main.js 中。

import '@/directive/lazyLoad'

懒加载指令的使用

<template>
    <div class="imgWarp" ref="wrapper" >
      <img alt="加载"
           v-for="(item,index) of urls" :key="index"
           v-lazy="item" class="lazyload" >
    </div>
</template>

<script>
export default {
  data() {
    return {
      urls: [
        'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg',
        'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg',
        'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg',
        'https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg',
        'https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg',
        'https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg',
        'https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg'
      ]
    }
  },
}
</script>

<style lang='scss' scoped>
.imgWarp {
    height: 200px;
    overflow: auto;
    background-color: pink;
    .lazyload {
      position: relative;
      display: block;
      margin-top: 30px;
      width: 200px;
      height: 200px;
    }
    .lazyload:after {
      position: absolute;
      content: "";
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: block;
      background-color: #ccc;
    }
  }
</style>

当然懒加载的实现还可以采用 监听 scroll + 节流 的方法去实现,但是由于scroll事件密集发生,计算量很大,容易造成性能问题,所以建议选择使用 IntersectionObserver 去实现。

添加水印

在这 vue3 中播放 m3u8 格式视频 篇文章里我曾介绍过局部水印和全局水印的实现方式,那么这一次就用指令的方式来实现一次 。通过 v-waterMark 指令,给元素添加水印 实现原理:

  1. 使用 canvas 生成文本填充的 base64 格式的图片文件,设置其字体大小,颜色等。
  2. 将其设置为背景图片,并插入 v-waterMark 指令所在的子节点从而实现页面或组件水印效果。
import Vue from 'vue'
 
function setWaterMark(str, currentNode, font, textColor){
  const can = document.createElement('canvas')
  // 设置画布的长宽
  can.width = 200
  can.height = 200
  const cans = can.getContext('2d')
   // 旋转角度
   cans.rotate((-20 * Math.PI) / 180)
   cans.font = '16px Vedana'
  // 字体设置
   cans.font = font || '16px Microsoft JhengHei'
   // 设置填充绘画的颜色
   cans.fillStyle = textColor ||  'rgba(196, 198, 203,.4)'
 
   // 设置文本内容的当前对齐方式
   cans.textAlign = 'left'
 
   // 在画布上填充文本内容(输出的文本,开始绘制文本的 x、y 坐标位置)
   cans.fillText(str, can.width / 8, can.height / 2)
   const waterMark = document.createElement('div')
   waterMark.style.pointerEvents = 'none'
   waterMark.style.top = '0px'
   waterMark.style.left = '0px'
   waterMark.style.position = 'absolute'
   waterMark.style.zIndex = '100000'
   waterMark.style.width = 200+ 'px'
   waterMark.style.height = 200  + 'px' 
   waterMark.style.background = 'url(' + can.toDataURL('image/png') + ') left top '
  
  // 将 waterMark 作为子节点插入到 v-waterMark 所在的当前节点下
   currentNode.appendChild(waterMark)

}
const waterMarker = {
  bind: function (el, binding) {
    setWaterMark(binding.value.text, el, binding.value.font, binding.value.textColor)
  },
}

Vue.directive('waterMarker', waterMarker)

这里是水印结合懒加载的使用:

<template>
  <div>
    <div class="imgWarp" ref="wrapper">
      <div v-for="(item,index) of urls" :key="index"  v-waterMarker="{text:'lzg版权所有',textColor:'rgba(180, 180, 0, 1)'}" class="imgWrapper">
        <img alt="加载"
          class="lazyload"
          v-lazy="item">
        </div>
    </div>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        urls: [
          'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg',
          'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg',
          'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg',
          'https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg',
          'https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg',
          'https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg',
          'https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg'
        ]
      }
    },
  }
</script>

<style lang='scss' scoped>
  .imgWarp {
    height: 200px;
    overflow: auto;
    .imgWrapper {
      position: relative;
      width: 200px;
      height: 200px;
    }
    .lazyload {
      position: relative;
      display: block;
      margin-top: 30px;
      width: 100%;
    }
    .lazyload:after {
      position: absolute;
      content: "";
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: block;
      background-color: #ccc;
    }
  }
</style>

限制输入数字

在项目的开发过程中,常常会碰到表单提交功能,而对于一些输入项我们需要限制用户输入的合法性, 像输入手机号、年龄等信息时,我们需要限制用户只能输入数字。 限制数字输入的方式也很简单:通过自定义指令监听用户的输入内容,通过正则匹配替换

import Vue from 'vue'
Vue.directive('numberOnly', {
  bind: function (el) {
    el.handler = function () {
      // 匹配所有非数字的字符将其替换成空
      el.value=el.value.replace(/\D+/g, '')
    }
    // 这里使用 keyup 监听键盘的输入,触发输入的校验
    el.addEventListener('keyup', el.handler) 
  },
  unbind: function (el) {
    el.removeEventListener('keyup', el.handler)
  },
})

注意: 在使用 ElementUI 提供的 input 组件时只用 el.value 是无法直接获取输入的内容的,需要找到它子节点中的第一项,这才是我们校验的输入信息

el.childNodes[1].value = el.childNodes[1].value.replace(/\D+/g, '')

v-numberOnly 的使用

//  html 
<input type="text" v-model="number" v-numberOnly>


//  js
data() {
  return {
    number:''
  }
},

总结

自定义指令支持 DOM 交互的特点为我们提供了许多便利, 关于自定义指令还有很多应用场景,如:拖拽指令、权限校验、长按、防抖节流等,因此我们在实际开发过程中,也需要多思考如何合理正确的应用指令提高我们的开发效率。

参考

  1. vue.js 官方文档
  2. 8个非常实用的Vue自定义指令
转载自:https://juejin.cn/post/7239715562833166393
评论
请登录