实现防抖节流-深拷贝-事件总线
1. 防抖 Debounce 函数
我们用一副图来理解一下它的过程:
- 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
- 当事件密集触发时,函数的触发会被频繁的推迟;
- 只有等待了一段时间也没有事件触发,才会真正的执行响应函数;
1.1. 防抖的应用场景很多
- ➢ 输入框中频繁的输入内容,搜索或者提交信息;
- ➢ 频繁的点击按钮,触发某个事件;
- ➢ 监听浏览器滚动事件,完成某些特定操作;
- ➢ 用户缩放浏览器的 resize 事件;
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. 节流的应用场景
- ➢ 监听页面的滚动事件;
- ➢ 鼠标移动事件;
- ➢ 用户频繁点击按钮操作;
- ➢ 游戏中的一些设计;
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 实现防抖和节流
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");
});