透过 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

相关文章
相关标签/搜索