在 Vue2.0,除了核心功能默认内置的指令 ( v-model 和 v-show ),Vue 也允许注册自定义指令。在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
Vue 自定义指令有全局注册和局部注册两种方式。全局注册指令的方式,通过 Vue.directive( id, [definition] ) 方式注册全局指令。如果想注册局部指令,组件中也接受一个directives的选项。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Vue.directive('focus', { inserted: function (el) { el.focus() } })
directives: { focus: { inserted: function (el) { el.focus() } } }
|
然后我们可以在模板中任何元素上使用心得v-focus property,如下:
当我们需要批量注册自定义指令时,写很多个``Vue.directive( id, [definition] ) 会导致代码冗余,所以我们可以利用Vue.use()` 的特性,完成批量注册。
批量注册指令,新建 directives/directive.js 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import debounce from './debounce' import throttle from './throttle'
const directives = { debounce, throttle, }
export default { install(Vue) { Object.keys(directives).forEach((key) => { Vue.directive(key, directives[key]) }) }, }
|
在 main.js 引入,并Vue.use() 调用完成批量注册。
1 2 3
| import Vue from 'vue' import Directives from './directives/directive.js' Vue.use(Directives)
|
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
- bind: 只调用一次,指令第一次绑定到元素时调用,可以定义一个在绑定时执行一次的初始化动作,此时获取父节点为null。
- inserted: 被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中),此时可以获取到父节点。
- update: 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
- componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind: 只调用一次, 指令与元素解绑时调用。
接下来我们来看一下钩子函数的参数 (即 el、binding、vnode 和 oldVnode)。
指令钩子函数会被传入以下参数:
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 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
注意:除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。
下面分享几个实用的 Vue 自定义指令
- 长按指令
v-longpress
- 函数防抖指令
v-debounce
- 函数节流指令
v-throttle
- 点击元素外部指令
v-click-out
- 弹窗限制外部滚动指令
v-scroll-pop
- 神策埋点指令
v-sensor
1. v-longpress
需求:当用户按下鼠标左键或移动端单指触碰,并按住按钮几秒钟时,视为一次长按,触发对应的函数。
思路:
- 定义一个计时器, n 秒后执行函数,n作为参数。
- 当用户按下按钮时触发
mousedown 或touchstart 事件,启动计时器。
- 如果
click 、 mouseup 、touchend 或 touchcancel 事件在 n 秒内被触发,则清除计时器,视为普通点击事件。
- 如果计时器没有在 n秒内清除,则视为一次长按,触发对应的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| const longpress = { bind: function (el, {value:{fn,time}}) { if (typeof fn !== 'function') return el._timer = null el._start = (e) => { if ( (e.type === 'mousedown' && e.button && e.button !== 0) || (e.type === 'touchstart' && e.touches && e.touches.length > 1) ) return; if (el._timer === null) { el._timer = setTimeout(() => { fn() }, time) el.addEventListener('contextmenu', function(e) { e.preventDefault(); }) } } el._cancel = (e) => { if (el._timer !== null) { clearTimeout(el._timer) el._timer = null } } el.addEventListener('mousedown', el._start) el.addEventListener('touchstart', el._start) el.addEventListener('click', el._cancel) el.addEventListener('mouseout', el._cancel) el.addEventListener('touchend', el._cancel) el.addEventListener('touchcancel', el._cancel) }, unbind(el) { el.removeEventListener('mousedown', el._start) el.removeEventListener('touchstart', el._start) el.removeEventListener('click', el._cancel) el.removeEventListener('mouseout', el._cancel) el.removeEventListener('touchend', el._cancel) el.removeEventListener('touchcancel', el._cancel) }, }
export default longpress
|
使用:给 Dom 加上 v-longpress 及参数即可
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <button v-longpress="{fn: longpress,time:2000}">长按</button> </template>
<script> export default { methods: { longpress () { console.log('长按指令生效') } } } </script>
|
2. v-debounce
背景:在开发中,有时遇到要给input或者滚动条添加监听事件,需要做防抖处理。
需求:防止input或scroll事件在短时间内被多次触发,使用防抖函数限制一定时间后触发。
思路:
- 定义一个延迟执行的方法,如果在延迟时间内再调用该方法,则重新计算执行时间。
- 将事件绑定在传入的方法上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const debounce = { inserted: function (el, {value:{fn, event, time}}) { if (typeof fn !== 'function') return el._timer = null el.addEventListener(event, () => { if (el._timer !== null) { clearTimeout(el._timer) el._timer = null } el._timer = setTimeout(() => { fn() }, time) }) }, } export default debounce
|
使用:给 Dom 加上 v-debounce 及回调函数即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <template> <input v-debounce="{fn: debounce, event: 'input', time: 5000}" /> <div v-debounce="{fn: debounce, event: 'scroll', time: 5000}"> <p>文字文字文字文字...</p> </div> </template>
<script> export default { methods: { debounce(){ console.log('debounce 防抖') }, } } </script>
|
3. v-throttle
背景:在开发中,有些提交保存按钮有时候会在短时间内被点击多次,这样就会多次重复请求后端接口,造成数据的混乱,比如立即购买按钮,多次点击就会多次调用创建订单接口。
需求:防止按钮在短时间内被多次点击,使用节流函数限制规定时间内只能点击一次。
思路:
- 定义一个由开关(默认为开)控制是否执行的方法,第一次执行函数时将开关关闭,在规定时间内再调用该方法,则不会再次执行,直至规定时间过后开关打开。
- 将事件绑定在 click 方法上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const throttle = { bind:function (el,{value:{fn,time}}) { if (typeof fn !== 'function') return el._flag = true; el._timer = null el.handler = function () { if (!el._flag) return; el._flag && fn() el._flag = false if (el._timer !== null) { clearTimeout(el._timer) el._timer = null } el._timer = setTimeout(() => { el._flag = true; }, time); } el.addEventListener('click',el.handler) }, unbind:function (el,binding) { el.removeEventListener('click',el.handler) } }
export default throttle
|
使用:给Dom加上v-throttle 及回调函数即可。
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <button v-throttle="{fn: throttle,time:3000}">throttle节流</button> </template>
<script> export default { methods: { throttle () { console.log('throttle 节流 只触发一次') } } } </script>
|
4. v-clickOut
背景:在我们的项目里,经常会出现一个弹窗,需要点击弹窗外部关闭该弹窗。
需求:实现一个指令,点击目标区域外部,触发指定函数。
思路:
- 判断点击的元素是否为目标元素,是则不作为,否则触发指定函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const clickOut = { bind(el,{value}){ function clickHandler(e) { if (el.contains(e.target)) return; if (typeof value === 'function') { value() } } el.handler = clickHandler; setTimeout(() => { document.addEventListener('click',el.handler); }, 0); }, unbind(el){ document.removeEventListener('click',el.handler); } }
export default clickOut
|
使用,将需要用到该指令的元素添加 v-click-out
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template> <div> <button @click="isImgShow = true">展示弹窗</button> <div v-click-out="clickImgOut" v-if="isImgShow" class="pop"> <img src="https://xxx.jpg" alt=""> <p>文字文字文字文字文字文字文字文字文字文字文字文字文字文字文字文字</p> </div> </div> </template>
<script> export default { data(){ return { isImgShow : false } }, methods:{ clickImgOut(){ this.isImgShow = false; console.log('点击弹窗外部') } } } </script>
|
背景:在我们的项目中,经常使用弹窗展示活动规则,活动规则过长需要滚动时,时长会导致外部滚动。这时针对这种情况,我们可以通过全局自定义指令来处理。
需求:自定义一个指令,使得弹窗内部内容可以滚动,外部无法滚动。
思路:
- 当弹窗展示时,记录滚动条滚动距离,然后给body和html设置固定定位,高度100%,top值为滚动距离。
- 当弹窗解除时,恢复原先样式,并把滚动距离设置成原来的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const scrollPop = { bind(el) { el.st = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop let cssStr = `overflow: hidden;width: 100%; height: 100%; position: fixed; top: ${- el.st}px;` document.querySelector('html').cssText = cssStr document.body.style.cssText = cssStr }, unbind(el,{value}) { let cssStr = 'overflow: auto; height: 100%; position: relative; top: 0px;scroll-behavior: auto' document.querySelector('html').cssText = cssStr document.body.style.cssText = cssStr document.querySelector('html').style.scrollBehavior = 'auto' document.documentElement.scrollTop = el.st document.body.scrollTop = el.st if (value !== 'smooth')return; let timer = setTimeout(() => { cssStr = `overflow: auto; height: 100%; position: relative; top: 0px; scroll-behavior: ${value||'smooth'}` document.querySelector('html').cssText = cssStr document.querySelector('html').style.scrollBehavior = value || 'smooth' document.body.style.cssText = cssStr }, 1); } }
export default scrollPop
|
使用:给需要限制的弹窗绑定v-scroll-pop属性,并设置scroll-behavior 值即可。
1 2 3 4 5 6
| <div class="scroll-pop" v-if="isScrollPopShow" v-scroll-pop="'smooth'"> <div class="content"> <p>这是很长一段文字,请耐心读完,然后你会发现这段文字并没有什么意义。</p> ... </div> </div>
|
6. v-sensor
背景:目前前端埋点代码大量入侵业务,埋点代码量大且难以区分和维护,现做出优化方案以减少其代码量。
埋点类型:
- ElementShow:页面元素显示
- PopupTrack:弹窗显示
- $WebClick:点击页面按钮
- PopupBtnClick:点击弹窗中按钮
- 自定义事件
优化方案:
1.自定义指令 v-sensor=” {el :’Btn_XXX_Tag_Common’,elClick:’Btn_XXX_Tag_Common’} “
注册封装自定义指令的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| const sensor = { inserted: function (el,{value: sensorObj}) { let showObj={} ,clickObj={} if (!Object.prototype.toString.call(sensorObj) === '[object Object]'|| JSON.stringify(sensorObj) == "{}") return for (const key in sensorObj) { if (Object.hasOwnProperty.call(sensorObj, key)) { switch (key) { case 'el': showObj= { name:'ElementShow', value: sensorObj[key] }; break; case 'pop': showObj= { name:'PopupTrack', value: sensorObj[key] }; break; case 'elClick': clickObj= { name:'$WebClick', value: sensorObj[key] }; break; case 'popClick': clickObj= { name:'PopupBtnClick', value: sensorObj[key] }; break; default: break; } } } showObj.value && sensors.track(showObj.name, { FileName: showObj.value }); if (clickObj.value) { el.handler = function () { clickObj.name === '$WebClick' && sensors.track(clickObj.name, { $element_name: clickObj.value }); clickObj.name === 'PopupBtnClick' && sensors.track(clickObj.name, { FileName: clickObj.value }); } el.addEventListener('click',el.handler) } }, unbind(el) { el.handler && el.removeEventListener('click', el.handler) } } export default sensor
|
对于除自定义事件以外的埋点事件,较好的优化办法就是使用自定义指令。使用 v-sensor=” {el :’Btn_XXX_Tag_Common’,elClick:’Btn_XXX_Tag_Common’} “ 。v-sensor接收一个对象作为参数,对象的key为事件标识,对象的value为事件属性,key值具体对应关系如下。
- el:ElementShow
- pop:PopupTrack
- elClick:$WebClick
- popClick:PopupBtnClick
1 2 3 4 5 6 7 8 9 10 11 12
| //单独使用ElementShow或$WebClick <div v-sensor="{el :'Btn_XXX_Tag_CXXXon'}">我是一个么得感情的标签</div> <div v-sensor="{elClick:'Btn_XXX_Tag_Common'}">俺也一样</div> //ElementShow和$WebClick组合使用方法 <div v-sensor="{el :'Btn_XXX_Tag_Common',elClick:'Btn_XXX_Tag_Common'}">俺也一样</div> //单独使用PopupTrack和PopupBtnClick <div v-sensor="{pop :'Pop_XXX_Tag_Common'}">俺也一样</div> <div v-sensor="{popClick:'Pop_XXX_Tag_Common'}">俺也一样</div> //PopupTrack和PopupBtnClick组合使用方法 <div v-sensor="{pop :'Pop_XXX_Tag_Common',popClick:'Pop_XXX_Tag_Common'}">俺也一样</div> //变量使用方法 <div v-sensor="{pop :`${sensorVal}`}">俺也一样</div>
|
提示:
由于该自定义指令是在元素插入页面DOM中时执行的,所以如果事件属性值使用变量的话,请在created生命周期内操作完毕,或给该元素绑定v-if为对应变量。