透過Keep-Alive實現防抖&節流元件
在前一篇文章揭祕了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.
即:
防抖 節流
二、防抖&節流元件特性
以下表格第一列表示特性專案,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="include" :before="beforeCall"> <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[data] // 快取原始事件函式 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