透過 Keep-Alive 實現防抖 & 節流組件

1、前言

在前一篇文章揭祕了keep-alive的實現原理:完全揭祕keep-alive原理,本文將模擬keep-alive原理實現Vue的防抖和節流組件。javascript

本文介紹內容包含:html

  • 防抖/節流組件特性說明;
  • 防抖/節流組件用法;
  • 防抖/節流組件代碼實現。

源代碼連接:throttle-debouncejava

防抖與節流碎碎念node

網上有不少關於防抖與節流定義、應用及實現的介紹,但同時也有不少不一樣的解釋版本,特別在概念的定義理解上,就有不少的誤差,有時候我看多了網上的介紹,本身也犯暈。如下是我所認同的版本:git

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.github

即:數組

  • 將突發的(屢次)事件分組到一個事件中謂之防抖,譬如快速點擊10次登陸按鈕(突發10次點擊事件),經過防抖操做將其歸組爲一次點擊事件。
  • 經過每X毫秒執行一次函數保證函數有規律地運行謂之節流,譬如鼠標滾動事件,經過節流操做限制每50ms執行一次鼠標滾動事件。

2、防抖&節流組件特性

如下表格第一列表示特性項目,keep-alive列是經過keep-alive源碼分析得出的結論,DebounceThrottle列則是咱們須要模擬實現的特性效果。緩存

特性 keep-alive Debounce Throttle
做用對象 默認第一個子組件 默認Click/Input事件 默認Click/Input事件
include 定義緩存組件白名單 定義防抖事件白名單 定義節流事件白名單
exclude 定義緩存黑名單 定義防抖黑名單 定義節流黑名單
max 定義緩存組件數量上限 / /
動態監聽 實時監聽緩存名單 實時監聽防抖名單 實時監聽節流名單
定時器 / 定義防抖時間間隔 定義節流時間間隔
自定義鉤子函數 / 自定義before鉤子 自定義before鉤子

3、用法

  • 首先註冊組件:
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事件添加節流效果,節流時間間隔爲默認值300msasync

  • 帶參用法
<Throttle time="500" include="keyup" exclude="input" :before="beforeHook">
    <input type="text" v-model="model" @keyup="keyUpCall" />
</Throttle>
複製代碼

includeexclude參數的用法與keep-alive相同,能夠是StringArrayRegexp中的任意類型;time聲明時間間隔;before爲鉤子函數。

4、手撕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鉤子裏面設置includeexclude兩個參數的監聽事件;destroyed鉤子銷燬變量。

看一下pruneThrottlematchs

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控制,指定時間內只執行一次函數。

5、一網打盡:Debounce

Emmm...其實Debounce的實現原理與Throttle徹底同樣,只是代碼上有一些差別,詳細實現看代碼便可:Demo

相關文章
相關標籤/搜索