一、前言
在前一篇文章揭秘了keep-alive
的实现原理:彻底揭秘keep-alive原理,本文将模拟keep-alive
原理实现Vue的防抖和节流组件。
本文介绍内容包含:
- 防抖/节流组件特性说明;
- 防抖/节流组件用法;
- 防抖/节流组件代码实现。
源代码链接:throttle-debounce
防抖与节流碎碎念
网上有很多关于防抖与节流定义、应用及实现的介绍,但同时也有很多不同的解释版本,特别在概念的定义理解上,就有很多的偏差,有时候我看多了网上的介绍,自己也犯晕。以下是我所认同的版本:
debounce: Grouping a sudden burst of events (like keystrokes) into a single one.
throttle: Guaranteeing a constant flow of executions every X milliseconds.
The main difference between throttling and debouncing is that throttle guarantees the execution of the function regularly, at least every X milliseconds.
即:
- 将突发的(多次)事件分组到一个事件中谓之
防抖
,譬如快速点击10次登录按钮(突发10次点击事件),通过防抖操作将其归组为一次点击事件。 - 通过每X毫秒执行一次函数保证函数有规律地运行谓之
节流
,譬如鼠标滚动事件,通过节流操作限制每50ms执行一次鼠标滚动事件。
二、防抖&节流组件特性
以下表格第一列表示特性项目,keep-alive
列是通过keep-alive源码分析得出的结论,Debounce
和Throttle
列则是我们需要模拟实现的特性效果。
特性 | keep-alive | Debounce | Throttle |
---|---|---|---|
作用对象 | 默认第一个子组件 | 默认Click/Input事件 | 默认Click/Input事件 |
include | 定义缓存组件白名单 | 定义防抖事件白名单 | 定义节流事件白名单 |
exclude | 定义缓存黑名单 | 定义防抖黑名单 | 定义节流黑名单 |
max | 定义缓存组件数量上限 | / | / |
动态监听 | 实时监听缓存名单 | 实时监听防抖名单 | 实时监听节流名单 |
定时器 | / | 定义防抖时间间隔 | 定义节流时间间隔 |
自定义钩子函数 | / | 自定义before钩子 | 自定义before钩子 |
三、用法
- 首先注册组件:
import Debounce from '@/components/Debounce'
import Throttle from '@/components/Throttle'
Vue.component('Debounce', Debounce)
Vue.component('Throttle', Throttle)
复制代码
这样注册完之后就可以全局使用了。
- 默认用法:
<Throttle>
<input type="text" class="common-input"
v-model="model"
@input="myinput" />
</Throttle>
复制代码
该例表示给input
元素的input
事件添加节流效果,节流时间间隔为默认值300ms
。
- 带参用法
<Throttle time="500" include="keyup" exclude="input" :before="beforeHook">
<input type="text" v-model="model" @keyup="keyUpCall" />
</Throttle>
复制代码
include
和exclude
参数的用法与keep-alive
相同,可以是String
、Array
、Regexp
中的任意类型;time
声明时间间隔;before
为钩子函数。
四、手撕Throttle
定义Throttle
组件的基本属性
export default {
name: 'Throttle',
abstract: true,
props: {
include: [Array, String, RegExp],
exclude: [Array, String, RegExp],
time: [String, Number],
before: Function
},
// ...
}
复制代码
设置abstract
将其定义为抽象组件,使得构建组件树的时候将其忽略;props
定义组件支持的所有参数。
定义Throuttle
组件的钩子
export default {
// ...
created () {
this.originMap = new Map // 缓存原始函数
this.throttleMap = new Map // 缓存节流函数
this.default = new Set // 缓存默认节流的事件类型
this.__vnode = null // 节流组件包裹的组件实例
},
mounted () {
this.$watch('include', val => { // 监听include参数变化,实时更新节流函数
pruneThrottle(this, name => matchs(val, name))
})
this.$watch('exclude', val => {
pruneThrottle(this, name => !matchs(val, name))
})
},
destroyed () {
this.originMap = new Map
this.throttleMap = new Map
this.default = new Set
this.__vnode = null
},
// ...
复制代码
created
钩子里面初始化缓存变量:originMap
缓存原始事件函数(节流前),throttleMap
缓存节流后的事件函数,default
缓存默认节流的事件类型,__vnode
缓存节流组件包裹的子组件;mounted
钩子里面设置include
和exclude
两个参数的监听事件;destroyed
钩子销毁变量。
看一下pruneThrottle
和matchs
const pruneThrottle = (vm, filter) => {
const { throttleMap, originMap, __vnode } = vm
Object.keys(throttleMap).filter(!filter).forEach((each) => {
Reflect.deleteProperty(throttleMap, each)
Reflect.set(__vnode.data.on, each, originMap[each])
})
}
复制代码
针对已经节流化的事件进行去节流操作,matchs
里面定义匹配逻辑:
const match = (pattern, name) => {
if(Array.isArray(pattern)) return pattern.includes(name)
if(typeof pattern === 'string') return new Set(pattern.split(',')).has(name)
if(isRegExp(pattern)) return pattern.test(name)
return false
}
复制代码
支持字符串、数组和正则三种类型的匹配。最后看render
的定义:
export default {
// ...
render () {
const vnode = this.$slots.default[0] || Object.create(null)
this.__vnode = vnode
// 针对不同的元素类型设置默认节流事件
if(vnode.tag === 'input') {
this.default.add('input')
} else if(vnode.tag === 'button') {
this.default.add('click')
}
const { include, exclude, time } = this
const evts = Object.keys(vnode.data.on)
const timer = parseInt(time)
evts.forEach((each) => {
if(
(include && match(include, each))
|| (exclude && !match(exclude, each))
|| (!match(exclude, each) && this.default.has(each))
) {
this.originMap.set(each, vnode.data.on[each]) // 缓存原始事件函数
this.throttleMap.set(each, throttle.call(vnode, vnode.data.on[each], timer, this.before)) // 缓存节流事件函数
vnode.data.on[each] = this.throttleMap.get(each) // 重新赋值组件实例的事件函数
}
})
return vnode
}
}
复制代码
核心逻辑是,先获取第一个被包裹的子组件实例及其定义的全部事件类型;其次根据子组件的tag
设置默认节流的事件类型(input
元素是input
事件,button
元素是click
事件);接着经过黑白名单的匹配规则后,将指定的事件函数通过throttle
函数节流化。
再看throttle
的定义:
const throttle = (func, wait, before) => {
let isInvoking = false
wait = wait || 300
return (arg) => {
if (isInvoking) return
isInvoking = true
before && before.call(this)
window.setTimeout(async () => {
if(!Array.isArray(func)) {
func = [func]
}
for(let i in func) {
await func[i].call(this, arg)
}
isInvoking = false
}, wait)
}
}
复制代码
核心逻辑就是,设置一个等待事件,在这等待时间内,通过闭包变量isInvoking
控制,指定时间内只执行一次函数。
五、一网打尽:Debounce
Emmm...其实Debounce的实现原理与Throttle完全一样,只是代码上有一些差异,详细实现看代码即可:Demo