Vue组件实现函数防抖

2,012 阅读2分钟

最近在掘金看到两篇非常不错的文章:

这两篇文章中作者都分享了关于把函数防抖/函数节流包装成通用组件的经验。

在这里我就不介绍函数防抖/函数节流的概念了,将这样的功能封装是组件真的是非常实用。

通过HOC(高阶组件)的方式进行封装的思路我也很喜欢,这里也想分享一个类似的封装方法

抽象组件

这里我使用了abstract: true来创建一个抽象组件。

我们常用的transitionkeep-alive就是一个抽象组件。抽象组件是无状态的,同样也是“不存在的”,它自己并不会被渲染为实际的DOM,而是直接返回以及操作它的子元素。

例如对于模板(Debounce是一个抽象组件):

<Debounce>
    <button>123</button>
</Debounce>
复制代码

会被渲染成:

<button>123</button>
复制代码

实现

这里直接贴出组件代码:

const debounce = (func, time, ctx) => {
    let timer
    const rtn = (...params) => {
        clearTimeout(timer)
        timer = setTimeout(() => {
            func.apply(ctx, params)
        }, time)
    }
    return rtn
}

Vue.component('Debounce', {
    abstract: true,
    props: ['time', 'events'],
    created () {
      this.eventKeys = this.events.split(',')
      this.originMap = {}
      this.debouncedMap = {}
    },
    render() {
        const vnode = this.$slots.default[0]

        this.eventKeys.forEach((key) => {
            const target = vnode.data.on[key]
            if (target === this.originMap[key] && this.debouncedMap[key]) {
                vnode.data.on[key] = this.debouncedMap[key]
            } else if (target) {
                this.originMap[key] = target
                this.debouncedMap[key] = debounce(target, this.time, vnode)
                vnode.data.on[key] = this.debouncedMap[key]
            }
        })
        
        return vnode
    },
})
复制代码

Debounce组件会接受timeevents(用逗号分隔)的两个参数。

render函数中,Debounce组件修改了子VNode的事件,再将其返回回去。

使用

然后我们来使用一下:

<div id="app">
    <Debounce :time="1000" events="click">
        <button @click="onClick($event, 1)">click+1 {{val}}</button>
    </Debounce>
    <Debounce :time="1000" events="click">
        <button @click="onClick($event, 2)">click+2 {{val}}</button>
    </Debounce>
    <Debounce :time="1000" events="mouseup">
        <button @mouseup="onAdd">click+3 {{val}}</button>
    </Debounce>
    <Debounce :time="1000" events="click">
        <button @mouseup="onAdd">click+3 {{val}}</button>
    </Debounce>
</div>
复制代码
const app = new Vue({
    el: '#app',
    data () {
        return {
            val: 0,
        }
    },
    methods: {
        onClick ($ev, val) {
            this.val += val
        },
        onAdd () {
            this.val += 3
        }
    }
})
复制代码

使用指令

使用自定义指令也是一种思路,不过指令的bind发生在created的回调中,也就是晚于事件的初始化的,这样的话就不能通过修改vnode.data.on来改变绑定的事件回调,只能自己来绑定事件了:

Vue.directive('debounce', {
    bind (el, { value }, vnode) {
        const [target, time] = value
        const debounced = debounce(target, time, vnode)
        el.addEventListener('click', debounced)
        el._debounced = debounced
    },
    destroy (el) {
        el.removeEventListener('click', el._debounced)
    }
})
复制代码

这里要注意的一点是,指令binding.value的求值过程和事件绑定是不同的,并不支持onClick($event, 2)的写法,因此如果这样的绑定就只能再包一层了:

<button v-debounce="[($ev) => { onClick($ev, 4) }, 500]">click+4 {{val}}</button>
复制代码

小结

使用抽象组件的好处是提高了组件的通用性,不会因为组件的使用而污染DOM(添加并不想要的div标签等)、可以包裹任意的单一子元素,当然也有缺点,比如使用时要注意子元素只能包含一个根,使用起来也比较啰嗦(参考文章中ButtonHoc在使用时更简洁一些,但相应的是只能作为Button渲染)。