实现防抖节流-深拷贝-事件总线

实现防抖节流-深拷贝-事件总线

1. 防抖 Debounce 函数

我们用一副图来理解一下它的过程:

  • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
  • 当事件密集触发时,函数的触发会被频繁的推迟;
  • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数;

1.1. 防抖的应用场景很多

  • ➢ 输入框中频繁的输入内容,搜索或者提交信息;
  • ➢ 频繁的点击按钮,触发某个事件;
  • ➢ 监听浏览器滚动事件,完成某些特定操作;
  • ➢ 用户缩放浏览器的 resize 事件;

image-20230116085145184

1.2. 面试版实现

function debounce(fn, delay) {
  // 1.用于记录上一次事件触发的timer
  let timer = null;

  // 2.触发事件时执行的函数
  const _debounce = function (...args) {
    // 2.1.如果有再次触发(更多次触发)事件, 那么取消上一次的事件
    if (timer) clearTimeout(timer);

    // 2.2.延迟去执行对应的fn函数(传入的回调函数)
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null; // 执行过函数之后, 将timer重新置null
    }, delay);
  };

  // 返回一个新的函数
  return _debounce;
}

1.3. 完整整版实现

  • 防抖基本功能实现:可以实现防抖效果
  • 优化一:优化参数和 this 指向
  • 优化二:优化取消操作(增加取消功能)
  • 优化三:优化立即执行效果(第一次立即执行)
  • 优化四:优化返回值
// 原则: 一个函数进行做一件事情, 一个变量也用于记录一种状态
function debounce(fn, delay, immediate = false, resultCallback) {
  // 1.用于记录上一次事件触发的timer
  let timer = null;
  let isInvoke = false;

  // 2.触发事件时执行的函数
  const _debounce = function (...args) {
    return new Promise((resolve, reject) => {
      try {
        // 2.1.如果有再次触发(更多次触发)事件, 那么取消上一次的事件
        if (timer) clearTimeout(timer);

        // 第一次操作是不需要延迟
        let res = undefined;
        if (immediate && !isInvoke) {
          res = fn.apply(this, args);
          if (resultCallback) resultCallback(res);
          resolve(res);
          isInvoke = true;
          return;
        }

        // 2.2.延迟去执行对应的fn函数(传入的回调函数)
        timer = setTimeout(() => {
          res = fn.apply(this, args);
          if (resultCallback) resultCallback(res);
          resolve(res);
          timer = null; // 执行过函数之后, 将timer重新置null
          isInvoke = false;
        }, delay);
      } catch (error) {
        reject(error);
      }
    });
  };

  // 3.给_debounce绑定一个取消的函数
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer);
    timer = null;
    isInvoke = false;
  };

  // 返回一个新的函数
  return _debounce;
}

2. 节流 Throttle 函数

我们用一副图来理解一下节流的过程

  • 当事件触发时,会执行这个事件的响应函数;
  • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;
  • 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的;

2.1. 节流的应用场景

  • ➢ 监听页面的滚动事件;
  • ➢ 鼠标移动事件;
  • ➢ 用户频繁点击按钮操作;
  • ➢ 游戏中的一些设计;

image-20230116085242164

2.2. 面试版实现

function throttle(fn, interval) {
  let startTime = 0;

  const _throttle = function (...args) {
    const nowTime = new Date().getTime();
    const waitTime = interval - (nowTime - startTime);
    if (waitTime <= 0) {
      fn.apply(this, args);
      startTime = nowTime;
    }
  };

  return _throttle;
}

2.3. 完整版实现

  • 节流函数的基本实现:可以实现节流效果
  • 优化一:节流最后一次也可以执行
  • 优化二:优化添加取消功能
  • 优化三:优化返回值问题
function hythrottle(fn, interval, { leading = true, trailing = false } = {}) {
  let startTime = 0;
  let timer = null;

  const _throttle = function (...args) {
    return new Promise((resolve, reject) => {
      try {
        // 1.获取当前时间
        const nowTime = new Date().getTime();

        // 对立即执行进行控制
        if (!leading && startTime === 0) {
          startTime = nowTime;
        }

        // 2.计算需要等待的时间执行函数
        const waitTime = interval - (nowTime - startTime);
        if (waitTime <= 0) {
          // console.log("执行操作fn")
          if (timer) clearTimeout(timer);
          const res = fn.apply(this, args);
          resolve(res);
          startTime = nowTime;
          timer = null;
          return;
        }

        // 3.判断是否需要执行尾部
        if (trailing && !timer) {
          timer = setTimeout(() => {
            // console.log("执行timer")
            const res = fn.apply(this, args);
            resolve(res);
            startTime = new Date().getTime();
            timer = null;
          }, waitTime);
        }
      } catch (error) {
        reject(error);
      }
    });
  };

  _throttle.cancel = function () {
    if (timer) clearTimeout(timer);
    startTime = 0;
    timer = null;
  };

  return _throttle;
}

3. Underscore 库的介绍

事实上我们可以通过一些第三方库来实现防抖操作:

  • lodash
  • underscore

这里使用 underscore

  • 我们可以理解成 lodash 是 underscore 的升级版,它更重量级,功能也更多;
  • 但是目前我看到 underscore 还在维护,lodash 已经很久没有更新了;

Underscore 的官网:https://underscorejs.org/

Underscore 的安装有很多种方式:

  • 下载 Underscore,本地引入;
  • 通过 CDN 直接引入;
  • 通过包管理工具(npm)管理安装;

这里我们直接通过 CDN:

Underscore 库的介绍

3.1. Underscore 实现防抖和节流

image-20230116090159179

4. 深拷贝函数

对象相互赋值的一些关系,分别包括:

  • 引入的赋值:指向同一个对象,相互之间会影响;
  • 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;
  • 对象的深拷贝:两个对象不再有任何关系,不会相互影响;

前面我们已经可以通过一种方法来实现深拷贝了:JSON.parse

  • 这种深拷贝的方式其实对于函数、Symbol 等是无法处理的;
  • 并且如果存在对象的循环引用,也会报错的;

自定义深拷贝函数:

  • 1.自定义深拷贝的基本功能;
  • 2.对 Symbol 的 key 进行处理;
  • 3.其他数据类型的值进程处理:数组、函数、Symbol、Set、Map;
  • 4.对循环引用的处理;

4.1. 基本实现

function deepCopy(originValue) {
  // 1.如果是原始类型, 直接返回
  if (!isObject(originValue)) {
    return originValue;
  }

  // 2.如果是对象类型, 才需要创建对象
  const newObj = Array.isArray(originValue) ? [] : {};
  for (const key in originValue) {
    newObj[key] = deepCopy(originValue[key]);
  }
  return newObj;
}

4.2. 完整实现

function deepCopy(originValue, map = new WeakMap()) {
  // 0.如果值是Symbol的类型
  if (typeof originValue === "symbol") {
    return Symbol(originValue.description);
  }

  // 1.如果是原始类型, 直接返回
  if (!isObject(originValue)) {
    return originValue;
  }

  // 2.如果是set类型
  if (originValue instanceof Set) {
    const newSet = new Set();
    for (const setItem of originValue) {
      newSet.add(deepCopy(setItem));
    }
    return newSet;
  }

  // 3.如果是函数function类型, 不需要进行深拷贝
  if (typeof originValue === "function") {
    return originValue;
  }

  // 4.如果是对象类型, 才需要创建对象
  if (map.get(originValue)) {
    return map.get(originValue);
  }
  const newObj = Array.isArray(originValue) ? [] : {};
  map.set(originValue, newObj);
  // 遍历普通的key
  for (const key in originValue) {
    newObj[key] = deepCopy(originValue[key], map);
  }
  // 单独遍历symbol
  const symbolKeys = Object.getOwnPropertySymbols(originValue);
  for (const symbolKey of symbolKeys) {
    newObj[Symbol(symbolKey.description)] = deepCopy(
      originValue[symbolKey],
      map
    );
  }

  return newObj;
}
// 判断一个标识符是否是对象类型
function isObject(value) {
  const valueType = typeof value;
  return value !== null && (valueType === "object" || valueType === "function");
}

5. 事件总线

自定义事件总线属于一种观察者模式,其中包括三个角色:

  • 发布者(Publisher):发出事件(Event);
  • 订阅者(Subscriber):订阅事件(Event),并且会进行响应(Handler);
  • 事件总线(EventBus):无论是发布者还是订阅者都是通过事件总线作为中台的;

当然我们可以选择一些第三方的库:

  • Vue2 默认是带有事件总线的功能;
  • Vue3 中推荐一些第三方库,比如 mitt;

当然我们也可以实现自己的事件总线:

  • 事件的监听方法 on;
  • 事件的发射方法 emit;
  • 事件的取消监听 off;
// 类EventBus -> 事件总线对象
class HYEventBus {
  constructor() {
    this.eventMap = {};
  }

  on(eventName, eventFn) {
    let eventFns = this.eventMap[eventName];
    if (!eventFns) {
      eventFns = [];
      this.eventMap[eventName] = eventFns;
    }
    eventFns.push(eventFn);
  }

  off(eventName, eventFn) {
    let eventFns = this.eventMap[eventName];
    if (!eventFns) return;
    for (let i = 0; i < eventFns.length; i++) {
      const fn = eventFns[i];
      if (fn === eventFn) {
        eventFns.splice(i, 1);
        break;
      }
    }

    // 如果eventFns已经清空了
    if (eventFns.length === 0) {
      delete this.eventMap[eventName];
    }
  }

  emit(eventName, ...args) {
    let eventFns = this.eventMap[eventName];
    if (!eventFns) return;
    eventFns.forEach((fn) => {
      fn(...args);
    });
  }
}

// 使用过程
const eventBus = new HYEventBus();

// aside.vue组件中监听事件
eventBus.on("navclick", (name, age, height) => {
  console.log("navclick listener 01", name, age, height);
});

const click = () => {
  console.log("navclick listener 02");
};
eventBus.on("navclick", click);

setTimeout(() => {
  eventBus.off("navclick", click);
}, 5000);

eventBus.on("asideclick", () => {
  console.log("asideclick listener");
});

留下回复

目录