最近在掘金看到两篇非常不错的文章:
以 vue 组件或者插件的形式, 实现 throttle 或者 debounce
奇技淫巧 - Vue Mixins 高级组件 与 Vue HOC 高阶组件 实践
这两篇文章中作者都分享了关于把函数防抖 / 函数节流包装成通用组件的经验.
在这里我就不介绍函数防抖 / 函数节流的概念了, 将这样的功能封装是组件真的是非常实用.
通过 HOC(高阶组件) 的方式进行封装的思路我也很喜欢, 这里也想分享一个类似的封装方法
抽象组件
这里我使用了 abstract: true 来创建一个抽象组件.
我们常用的 transition 和 keep-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 组件会接受 time 和 events(用逗号分隔) 的两个参数.
在 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 渲染).
来源: https://juejin.im/post/5c2dc7a9e51d4573c8491e77