涨知识了,防抖节流原来还可以这么使用!

38 阅读2分钟

前言

  1. 什么是防抖?什么是节流?
  2. 有什么用途?
  3. 常规解决方案
  4. Vue2 高阶组件用法实现
  5. 拓展

防抖和节流定义

防抖可以简单的理解为再一定时间内,只触发一次操作事件,那么即便你再这段时间内疯狂的点击操作也是无效的

节流是在某一段时间内必然会触发一次操作,如果这段时间内频繁触发此动作是无效的,待时间重置后继续这一系列动作

用途

  • 防抖:
    • 比如按钮的频繁操作
    • 输入框的动态频繁响应请求
  • 节流:
    • 滚动事件
    • 防抖中定义的一般也可以实现

一般这两种方案都是可以起到优化页面交互的一种手段

简单实现这两种方案

一般面试的时候大多会让你手写

防抖
function debounce(fn, delay) {
  let timer = null;

  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.call(this, args);
    }, delay);
  };
}
节流
function throttle(fn, delay) {
  let timer = null;
  let startDate = Date.now();
  return function () {
    let args = Array.prototype.slice.call(arguments);
    let endDate = Date.now();
    if (endDate - startDate > delay) {
      startDate = endDate;
      if (timer) {
        clearTimeout(timer);
      }
      fn.call(this, args);
    } else {
      clearTimeout(timer);
      timer = setTimeout(() => {
        fn.call(this, args);
      }, delay);
    }
  };
}

补充一句更多的时候我们会借用第三方的库去实现,比如大名鼎鼎的lodash, debouncethrottle这些API都可以很好兼容处理

Vue@2另类的实现方案

  • 前段时间因为自己在思考vue中如何定义高阶组件,后来查阅了一些资料,想到了一些解决方案
  • 借助这些思路可以去拓展一下

好了,我们直接开门见山吧

源码

  • lodash使用方案

image.png

  • 基础版本
<script>
export default {
  name: 'MyThrottle',
  // 在组件配置项中添加一个abstract的选项设置为true,就可以让组件成为一个抽象组件,抽象组件它自身不会渲染一个DOM元素,也不会出现在组件的父组件链中。
  // 在抽象组件的生命周期过程中,我们可以对包裹的子组件监听的事件进行拦截,也可以对子组件进行Dom 操作,从而可以对我们需要的功能进行封装,而不需要关心子组件的具体实现。
  abstract: true,
  props: {
    time: Number,
    events: {
      deafult: '',
      type: String
    }
  },
  created() {
    this.eventKeys = this.events.split(',');
    this.originMap = {};
    this.throttledMap = {};
  },
  methods: {
    _throttle(fn, wait = 50, ctx) {
      let lastCall = 0;
      return function (...params) {
        const now = new Date().getTime();
        if (now - lastCall < wait) return;
        lastCall = now;
        fn.apply(ctx, params);
      };
    },
    isOriginHtmlNode(vnode) {
      return !vnode.tag.includes('vue-component');
    }
  },
  // render函数直接返回slot的vnode,避免外层添加包裹元素
  render() {
    const vnode = this.$slots.default[0]; //遍历单个节点
    const getTagetEvents = this.isOriginHtmlNode(vnode)
      ? vnode.data.on
      : vnode.componentOptions.listeners;
    this.eventKeys.forEach((key) => {
      //获取当前vnode 自身的事件
      const target = this.isOriginHtmlNode(vnode)
        ? vnode.data.on[key]
        : vnode.componentOptions.listeners[key];
      if (target === this.originMap[key] && this.throttledMap[key]) {
        //如果存在则直接赋值
        getTagetEvents[key] = this.throttledMap[key];
      } else if (target) {
        // 将原本的事件处理函数替换成throttle节流后的处理函数
        this.originMap[key] = target;
        this.throttledMap[key] = this._throttle(target, this.time, vnode);
        //重写vnode 节点的绑定事件
        getTagetEvents[key] = this.throttledMap[key];
      }
    });
    return vnode;
  }
};
</script>

使用

image.png 注意: 这种方式只能监听一个插槽的情况,默认只能为一个子元素包裹 通过传入events 绑定监听元素事件,time为时间线

扩展

  • 需求:希望一次性监听多个子元素(多个插槽的情况),并且可能涵盖子元素的情况(深层遍历子元素)
  • 希望可以自定义事件属性(那么希望可以通过参数配置)
<script>
export default {
  name: 'MultiThrottle',
  props: {
    // eventKeys = 'click,blur'
    // eventKeys = {
    //   click: {
    //     time: 2000,
    //     ...args
    //   },
    //   blur: {
    //     time: 5000,
    //     ...args
    //   }
    // }
    events: {
      type: [Object, String],
      default: ''
    },
    time: Number
  },
  created() {
    this.throttledEventsMap = {};
  },
  computed: {
    _eventKeys() {
      let eventKeyNames = '';
      if (typeof this.events === 'string') {
        eventKeyNames = this.events;
      } else {
        eventKeyNames = Object.keys(this.events).join(',');
      }
      // console.log('eventKeyNames', eventKeyNames);
      return eventKeyNames.split(',');
    }
  },
  mounted() {
    console.log('this.throttledEventsMap', this.throttledEventsMap);
  },
  methods: {
    isOriginHtmlNode(vnode) {
      return !vnode?.tag?.includes('vue-component');
    },
    // 对传入的属性进行赋值操作
    getRestTime(key, time) {
      if (typeof this.events === 'object') {
        return this.events[key] || {time};
      }
      return {
        time
      };
    },
    _throttle(fn, wait = 50, ctx, key) {
      const self = this;
      let lastCall = 0;
      return function (...params) {
        const now = new Date().getTime();
        if (now - lastCall < self.getRestTime(key, wait).time) return;
        lastCall = now;
        fn.apply(ctx, params);
      };
    }
  },
  // render函数直接返回slot的vnode,避免外层添加包裹元素
  render() {
    const _vnode = this.$slots.default; //遍历所有子节点
    // console.log('_vnode', _vnode);
    // console.log('vnode', vnode);
    this._eventKeys?.forEach((key) => {
      //获取当前vnode 自身的事件
      const loopEvent = (vnode) => {
        if (vnode && vnode.length) {
          //如果节点存在
          vnode.forEach((n) => {
            if (n.children) {
              //如果有子节点递归
              // console.log('vnode.children', n.children);
              loopEvent(n.children);
            }
            const getTagetEvents = this.isOriginHtmlNode(n)
              ? n.data?.on
              : n.componentOptions?.listeners;
            const target = this.isOriginHtmlNode(n)
              ? n.data?.on[key]
              : n.componentOptions?.listeners[key];
            if (this.throttledEventsMap[key]) {
              // 默认存储一个表类型
              // this.throttledEventsMap = {
              //   originEvent: target,
              //   transEvent: fn,
              // }
              const getTransEventObj = this.throttledEventsMap[key].find(
                (fn) => fn.originEvent === target
              );
              if (getTransEventObj) {
                getTagetEvents[key] = getTransEventObj.transEvent;
              } else if (target) {
                this.throttledEventsMap[key].push({
                  key,
                  originEvent: target,
                  transEvent: this._throttle(target, this.time, n, key)
                });
                getTagetEvents[key] = this.throttledEventsMap[key].find(
                  (item) => item.originEvent === target
                ).transEvent;
              }
            } else if (target) {
              this.throttledEventsMap[key] = [
                {
                  key,
                  originEvent: target,
                  transEvent: this._throttle(target, this.time, n, key)
                }
              ];
              getTagetEvents[key] = this.throttledEventsMap[key].find(
                (item) => item.originEvent === target
              ).transEvent;
            }
          });
        }
      };

      loopEvent(_vnode);
    });
    return <div>{_vnode}</div>;
  }
};
</script>

使用:

image.png

config配置(组件传入)

image.png

更加个性化的配置可以配置config,从而个性化设置参数

参考

封装Vue组件的一些技巧