为什么要通过指令实现?
参考lodash/_
,和underscore
库,节流和防抖均是通过高阶函数的形式实现的,这种方式需要手动绑定this
,对于 Vue 的单组件文件来说,在methods
外层使用会导致this
的指向不清,直接指向window
的情况,举例:
methods: {
doSomethings () {
console.log('log')
}
},
mounted () {
document.querySelector('.test').addEventListener('click', function() {
_.throttle(this.doSomethings, 200)
}) // 无法正常触发,此写法报错非函数,箭头函数则完全没有触发内部函数
}
methods: {
doSomethings: _.throttle(function() {
console.log('log') // 可以正常节流,这里this的指向是该组件
})
}
虽然第二种方法正确实现了节流,但是这种写法不方便进行组合合并,如果触发的函数有不同的情况,在内部的函数需要多层嵌套或者传参,举例:
<template>
<div id="app">
olas
<span @click="test(1)">1111</span>
<span @click="test(2)">2222</span>
<span @click="test(3)">3333</span>
<span @click="test(4)">4444</span>
</div>
</template>
<script>
methods: {
test: function(i) {
console.log(i)
},
doSomething:_.throttle(function(i) {
this.test(i)
}, 2000)
},
mounted () {
}
</script>
这种情况下无法实现参数值的传递,需要借助 data,并不是比较好的选择,因此采用自定义指令的方式
这种实现有什么不足?
最大的一点不足就是 SSR
需要通过判断window
或者提供一个服务端专用版本来避免获取 dom,但是需要节流/防抖的一般都是在客户端的用户交互事件,往往不需要提供服务端版本,因此我这里采用window
判断的方式,只在客户端挂载监听事件
通过自定义指令,代理 v-on 直接挂载的方式,需要传入较多的参数
防抖实现
参数:
func
: methods 内的函数名;args
: 向该方法传递的参数,形式为数组;opt
: 防抖选项,包含 seconds 和 immediate
Vue.directive('debounce', {
inserted: function(el, binding) {
const event = binding.arg
const funcArgs = binding.value.args || []
const options = binding.value.opt || {}
const wait = options.seconds || 300
if (typeof window !== 'undefined') {
let timer = null
el.addEventListener(event, e => {
if (timer) {
clearTimeout(timer)
}
if (options.immediate) {
let callNow = !timer
if (callNow) {
binding.value.func.apply(this, funcArgs)
}
timer = setTimeout(() => {
timer = null
}, wait)
} else {
timer = setTimeout(() => {
timer = null
binding.value.func.apply(this, funcArgs)
}, wait)
}
})
}
}
})
节流实现
参数:
func
: methods 内的函数名;args
: 向该方法传递的参数,形式为数组;opt
: 防抖选项,包含 seconds 和 leading(未实现),trailing(未实现)
Vue.directive('throttle', {
inserted: function(el, binding) {
const event = binding.arg
const funcArgs = binding.value.args || []
const options = binding.value.opt || {}
const wait = options.seconds || 300
if (typeof window !== 'undefined') {
let previous = 0
el.addEventListener(event, e => {
if (!previous) {
previous = new Date()
binding.value.func.apply(this, funcArgs)
} else {
let now = new Date()
if (now - previous > wait) {
previous = now
binding.value.func.apply(this, funcArgs)
}
}
})
}
}
})