Vue 自定义指令教程
什么是指令
指令(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:上一个虚拟节点,仅在
update
和componentUpdated
钩子中可用。
除了 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 指令,给元素添加水印 实现原理:
- 使用 canvas 生成文本填充的 base64 格式的图片文件,设置其字体大小,颜色等。
- 将其设置为背景图片,并插入 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 交互的特点为我们提供了许多便利, 关于自定义指令还有很多应用场景,如:拖拽指令、权限校验、长按、防抖节流等,因此我们在实际开发过程中,也需要多思考如何合理正确的应用指令提高我们的开发效率。
参考
转载自:https://juejin.cn/post/7239715562833166393