手写工具函数(防抖-节流-深浅拷贝-时间总线)
认识防抖debounce函数
通常事件触发之后,会立即执行相对应的函数,而防抖就是,事件触发之后,过一段时间才会触发相应的函数
事件不断的触发,执行函数会无限制的延后
- 当事件频繁触发的时候,相对应的函数不会立即执行
- 只有事件停止触发后,等待一段时间,才会触发相应的执行函数
- 防抖的应用场景
- 输入框中频繁的输入内容,搜索或者提交信息
- 频繁的点击按钮,触发某个事件
- 监听浏览器滚动事件,完成某些特定的操作
- 用户缩放浏览器的resize事件
基本实现
- 防抖的核心就是,事件频繁触发的情况下,控制执行函数的执行时机
- 下面的代码只是实现了防抖的核心,但是其中的this指向并不完善
let inputElement = document.getElementById("input");
inputElement.oninput = zcdebounce(function () {
console.log(this.value);
}, 1000);
function zcdebounce(fn, delay) {
//接收要执行的函数,以及延迟时间
//定义延迟定时器timer
let timer = null;
//这里需要定义一个新函数,用于作为返回值
const _debounce = () => {
//判断timer是否存在,有的话就清除
if (timer) clearTimeout(timer);
//通过延迟时间,来确定要执行的时机
timer = setTimeout(() => {
fn();
//执行完应当执行的函数,应该将timer置为null
timer = null;
}, delay);
};
return _debounce;
}
实现this绑定
- 在以上代码运行的时候,会发现this.value是 undefined
- 通过代码可以看出,真正执行回调函数的位置是在定时器中的 fn(),默认调用的,因此this指向windows
- 现在我们想让this的指向为inputElement元素
- 通过观察我们知道, inputElement.oninput隐式调用了oninput,因此 oninput指向的就是inputElement
- 而 oninput对应的函数是防抖工具中的_debounce
- 因此只要把 _debounce改成function函数,用apply调用fn即可
let inputElement = document.getElementById("input");
inputElement.oninput = zcdebounce(function () {
console.log(this.value);
}, 1000);
function zcdebounce(fn, delay) {
//接收要执行的函数,以及延迟时间
//定义延迟定时器timer
let timer = null;
//这里需要定义一个新函数,用于作为返回值
const _debounce = function(){
//判断timer是否存在,有的话就清除
if (timer) clearTimeout(timer);
//通过延迟时间,来确定要执行的时机
timer = setTimeout(() => {
fn.apply(this);
//执行完应当执行的函数,应该将timer置为null
timer = null;
}, delay);
};
return _debounce;
}
实现参数传递
- 上述代码完成了基本的功能
- 现在需要考虑,回调函数中,有时候会有参数的传递,那么应该怎么传递参数
- 在回调函数中传入参数,实际上要在 _debounce中接收参数
let inputElement = document.getElementById("input");
let count = 1;
inputElement.oninput = zcdebounce(function (event) {
console.log(this.value, event);
}, 1000);
function zcdebounce(fn, delay) {
//接收要执行的函数,以及延迟时间
//定义延迟定时器timer
let timer = null;
//这里需要定义一个新函数,用于作为返回值
const _debounce = function (...arg) {
console.log(arg);
//判断timer是否存在,有的话就清除
if (timer) clearTimeout(timer);
//通过延迟时间,来确定要执行的时机
timer = setTimeout(() => {
fn.apply(this, arg);
//执行完应当执行的函数,应该将timer置为null
timer = null;
}, delay);
};
return _debounce;
}
实现取消操作
- 在使用防抖工具的时候,有时候我们在等待函数执行时,会对其进行取消操作
- 诸如,我们在输入完要查询的内容,等待函数执行的期间,突然不想让其执行了,需要进行取消操作
- 首先在 _debounce中增加一个cancle属性,该属性是一个函数,用于取消定时器
- 之后在点击取消按钮的时候,调用cancle即可
let inputElement = document.getElementById("input");
let cancleBtn = document.getElementById("btn");
//将防抖工具函数的返回值返回
const debounceFn = zcdebounce(function () {
console.log(this.value, event);
}, 1000);
//对输入框使用防抖函数
inputElement.oninput = debounceFn;
//对取消按钮,增加事件,该事件用于取消防抖函数的执行
cancleBtn.onclick = function () {
debounceFn.cancle();
};
function zcdebounce(fn, delay) {
//接收要执行的函数,以及延迟时间
//定义延迟定时器timer
let timer = null;
//这里需要定义一个新函数,用于作为返回值
const _debounce = function (...arg) {
//判断timer是否存在,有的话就清除
if (timer) clearTimeout(timer);
//通过延迟时间,来确定要执行的时机
timer = setTimeout(() => {
fn.apply(this, arg);
//执行完应当执行的函数,应该将timer置为null
timer = null;
}, delay);
//给_debounce增加一个属性
_debounce.cancle = function () {
if (timer) clearTimeout(timer);
};
};
return _debounce;
}
立即执行功能(基本用不到)
增加一个立即执行的功能:即当输入第一个字母或者单词的时候,不会等待,会立即执行,之后才会利用防抖机制
比如,我们输入macbook,当输入第一个m的时候,就会立即执行一次函数,等待输入完成acbook再次执行
- 首先在防抖函数中,再定义一个参数 immediate,用于记录是否是立即执行,true代表立即执行,false代表非立即执行
- 第一次立即执行完成之后,需要有一个变量 isDone 记录立即执行是否完成此次立即执行,完成则设置为true
- 当防抖函数执行完成之后,需要将isDone设置为false,方便下一次立即执行
注意这里在设计的时候有一个原则:一个函数最好只做一件事,一个变量只存储一种类别的状态;
若我们用immediate既作为是否立即执行,又作为立即执行是否完成,会对代码造成逻辑错误,因此引入了变量isDone
同时,对于用户传进来的变量,我们最好不要进行更改
let inputElement = document.getElementById("input");
let cancleBtn = document.getElementById("btn");
//将防抖工具函数的返回值返回
const debounceFn = zcdebounce(function () {
console.log(this.value, event);
}, 1000);
//对输入框使用防抖函数
inputElement.oninput = debounceFn;
//对取消按钮,增加事件,该事件用于取消防抖函数的执行
cancleBtn.onclick = function () {
debounceFn.cancle();
};
function zcdebounce(fn, delay, immediate = true) {
//接收要执行的函数,以及延迟时间
//定义延迟定时器timer
let timer = null;
let isDone = false;
//这里需要定义一个新函数,用于作为返回值
const _debounce = function (...arg) {
//如果是立即执行,则直接执行函数,同时return
if (immediate && !isDone) {
fn.apply(this, arg);
isDone = true;
return;
}
//判断timer是否存在,有的话就清除
if (timer) clearTimeout(timer);
//通过延迟时间,来确定要执行的时机
timer = setTimeout(() => {
fn.apply(this, arg);
//将是否完成当次防抖,设置为flase
isDone = false;
//执行完应当执行的函数,应该将timer置为null
timer = null;
}, delay);
};
//给_debounce增加一个属性
_debounce.cancle = function () {
if (timer) {
clearTimeout(timer);
//将是否完成当次防抖,设置为flase
isDone = false;
//执行完应当执行的函数,应该将timer置为null
timer = null;
}
};
return _debounce;
}
获取返回值
在某些场景下,我们需要获取函数的返回值,那么这个返回值应当怎么进行获取
- 先看以下代码
const debounceFn = zcdebounce(function () {
console.log(this.value, event);
return "zhangcheng"
}, 1000);
//我们主动执行这个函数,实际内部执行的是_debounce函数,因此不能获得return "zhangcheng"的返回值
debounceFn()
- 思路一,在防抖函数中,再次传入一个回调函数,用于获取返回值
//将防抖工具函数的返回值返回
const debounceFn = zcdebounce(
function () {
return "zhangcheng";
},
1000,
true,
function (res) {
console.log(res);
}
);
debounceFn();
function zcdebounce(fn, delay, immediate = true, resCallBack) {
//接收要执行的函数,以及延迟时间
//定义延迟定时器timer
let timer = null;
let isDone = false;
let res = undefined;
//这里需要定义一个新函数,用于作为返回值
const _debounce = function (...arg) {
//如果是立即执行,则直接执行函数,同时return
if (immediate && !isDone) {
res = fn.apply(this, arg);
if (resCallBack) resCallBack(res);
isDone = true;
return;
}
//判断timer是否存在,有的话就清除
if (timer) clearTimeout(timer);
//通过延迟时间,来确定要执行的时机
timer = setTimeout(() => {
res = fn.apply(this, arg);
if (resCallBack) resCallBack(res);
//将是否完成当次防抖,设置为flase
isDone = false;
//执行完应当执行的函数,应该将timer置为null
timer = null;
}, delay);
};
//给_debounce增加一个属性
_debounce.cancle = function () {
if (timer) {
clearTimeout(timer);
//将是否完成当次防抖,设置为flase
isDone = false;
//执行完应当执行的函数,应该将timer置为null
timer = null;
}
};
return _debounce;
}
- 思路二:将防抖函数,当作Promise返回出去,在外界调用的时候,使用.then获取返回值
//将防抖工具函数的返回值返回
const debounceFn = zcdebounce(
function () {
return "zhangcheng";
},
1000,
true
);
debounceFn().then((res) => {
console.log(res);
});
function zcdebounce(fn, delay, immediate = true, resCallBack) {
//接收要执行的函数,以及延迟时间
//定义延迟定时器timer
let timer = null;
let isDone = false;
let res = undefined;
//这里需要定义一个新函数,用于作为返回值
const _debounce = function (...arg) {
return new Promise((resolve, reject) => {
//如果是立即执行,则直接执行函数,同时return
if (immediate && !isDone) {
res = fn.apply(this, arg);
if (resCallBack) resCallBack(res);
resolve(res);
isDone = true;
return;
}
//判断timer是否存在,有的话就清除
if (timer) clearTimeout(timer);
//通过延迟时间,来确定要执行的时机
timer = setTimeout(() => {
res = fn.apply(this, arg);
if (resCallBack) resCallBack(res);
resolve(res);
//将是否完成当次防抖,设置为flase
isDone = false;
//执行完应当执行的函数,应该将timer置为null
timer = null;
}, delay);
//给_debounce增加一个属性
_debounce.cancle = function () {
if (timer) {
clearTimeout(timer);
//将是否完成当次防抖,设置为flase
isDone = false;
//执行完应当执行的函数,应该将timer置为null
timer = null;
}
};
});
};
return _debounce;
}
认识节流throttle函数
函数的执行,会按照固定的频率来执行
可以看做游戏中,角色的攻击速度,角色的发出去的攻击,不会按照玩家点击的速度发射子弹,而是又固定发射子弹的频率
- 与防抖的最大区别就是,函数执行是按照固定频率来执行的
基本实现
- 使用定时器方式实现
- 此种方式,与防抖函数不同的是,对存在的定时器处理方式不同
- 防抖对于存在的定时器,是进行清除操作,而节流是对存在的定时器,进行return操作,并不会清除
inputElement.oninput = zcthrottle(function () {
console.log(123);
}, 1000);
function zcthrottle(fn, interval) {
let timer = null;
const _throttle = function () {
if (timer) return;
timer = setTimeout(function () {
fn();
timer = null;
}, interval);
};
return _throttle;
}
- 使用公式进行实现
- 由原理我们知道,节流函数执行的频率(interval)是固定的
- 因此我们知道事件触发的起始时间(startTime)和当前时间(currentTime),即可计算出函数是否要进行执行
- 公式为 interval-(currentTime-startTime),当结果小于等于此时间,函数就会执行,否则不会执行
inputElement.oninput = zcthrottle(function () {
console.log(123);
}, 1000);
function zcthrottle(fn, interval) {
let startTime = 0;
const _throttle = function () {
const currentTime = new Date().getTime();
const resTime = interval - (currentTime - startTime);
if (resTime <= 0) {
fn();
startTime = currentTime;
}
};
return _throttle;
}
节流函数中的this和参数的绑定
与防抖函数的原理一致
inputElement.oninput = zcthrottle(function (event) {
console.log(this, event);
}, 1000);
function zcthrottle(fn, interval) {
let startTime = 0;
const _throttle = function (...args) {
const currentTime = new Date().getTime();
const resTime = interval - (currentTime - startTime);
if (resTime <= 0) {
fn.apply(this, args);
startTime = currentTime;
}
};
return _throttle;
}
立即执行的控制
通过以上代码可以发现,节流函数默认就是立即执行的,但是想设置为第一次不立即执行,应当怎么操作
- 通过引入变量 leading进行立即执行的控制
- 但是只用 leading进行控制,则函数将不会执行,因此需要用startTime进行控制
inputElement.oninput = zcthrottle(
function (event) {
console.log(this, event);
},
2000,
false
);
function zcthrottle(fn, interval, leading = true) {
let startTime = 0;
const _throttle = function (...args) {
const currentTime = new Date().getTime();
//立即执行的控制
//默认情况下是立即执行的,所以只需要考虑不立即执行的情况
//若只有leading进行控制,且leading为false的时候,则函数不会执行,因此引入startTime
if (!leading && startTime === 0) {
startTime = currentTime;
}
const resTime = interval - (currentTime - startTime);
if (resTime <= 0) {
fn.apply(this, args);
startTime = currentTime;
}
};
return _throttle;
}
手写拷贝-事件总线
深拷贝-浅拷贝-引用赋值的关系
网上的一些文章,对于深拷贝和浅拷贝的概念会有些混淆,深拷贝和浅拷贝都会创建新的引用类型,不同的是,对于引用类型中包含引用类型的数据处理不同
- 引用赋值:直接用等号进行赋值,就属于引用赋值,两个变量引用的同一个引用类型的地址
- **浅拷贝:**首先明确的一点就是 浅拷贝肯定会生成一个新的引用类型,但是引用类型的内部还有 引用类型,改变其中一个另外一个会收到影响
- **深拷贝:**通过深拷贝的方法创建一个新的 **引用类型,**两者都不会收到影响
let info = {
a: 100,
b: "200",
c: {
d: 300,
},
};
//引用赋值
let info1 = info;
//浅拷贝
let info2 = { ...info };
//深拷贝
//通过JSON方式实现的深拷贝,对于引用类型中存在函数的,symbol类型的,会默认删除,无法实现拷贝
let info3 = JSON.parse(JSON.stringify(info));
深拷贝实现
基本实现
- 在实现深拷贝之前,我们应当写一个工具函数,判断传入的参数的类型
- 对于普通类型直接返回false
- 对于部分引用类型返回true
- null–>object object–>object array–>object function–>function
- 针对以上,null应当返回false, 其余的返回true
function isObject(originValue){
let valueType = typeof originValue
return originValue != null && (valueType === object || valueType === function)
}
- 接下来我们实现简单的深拷贝
- 暂时以都是对象的情况实现
- 在深拷贝函数内部,首先创建一个新的对象
- 之后对传入的对象进行遍历
- 当传入对象的内部,依旧有对象的时候,应当再次创建一个新的对象,循环往复
function deepCopy(originValue) {
//应当对传入的参数进行判断,为普通类型,null均应该直接返回
if (!isObject(originValue)) return originValue;
// 若传入的为对象类型,则创建一个新的对象
let newObj = {};
//对传入的对象进行遍历
for (const key in originValue) {
//应当对传入对象的value进行递归操作
newObj[key] = deepCopy(originValue[key]);
}
return newObj;
}
数组拷贝
- 上面的代码,没有考虑到传入的参数是数组,或者内部是数组的情况
- 因此需要对传入的参数进行判断,若传入的参数是 对象,则创建{},若传入的参数是 数组,则创建[]
function deepCopy(originValue) {
//应当对传入的参数进行判断,为普通类型,null均应该直接返回
if (!isObject(originValue)) return originValue;
// 判断传入参数的类型,是对象还是数组
//对象与数组的判断,这只是其中一种方法
let newObj = Array.isArray(originValue) ? [] : {};
//对传入的对象进行遍历
for (const key in originValue) {
//应当对传入对象的value进行递归操作
newObj[key] = deepCopy(originValue[key]);
}
return newObj;
}
其他类型
set类型、函数类型、值和key是Symbol类型等
- 若传入的数据是set类型,需要特殊处理
- 首先类型的判断,使用intanceof进行判断,同时new Set()
- 其次,对于set遍历的时候,应当使用of进行遍历
function deepCopy(originValue) {
//应当对传入的参数进行判断,为普通类型,null均应该直接返回
if (!isObject(originValue)) return originValue;
//对set类型的数据进行判断
if (originValue instanceof Set) {
let newSet = new Set();
//对传进来的set数据进行遍历
for (const item of originValue) {
//为了防止set中有对象的存在,所以再次使用递归
newSet.add(deepCopy(item));
}
return newSet;
}
// 判断传入参数的类型,是对象还是数组
//对象与数组的判断,这只是其中一种方法
let newObj = Array.isArray(originValue) ? [] : {};
//对传入的对象进行遍历
for (const key in originValue) {
//应当对传入对象的value进行递归操作
newObj[key] = deepCopy(originValue[key]);
}
return newObj;
}
- 若传入的参数是函数类型,则直接返回即可
- 因为函数没有必要再进行深拷贝,会浪费内存
function deepCopy(originValue) {
//应当对传入的参数进行判断,为普通类型,null均应该直接返回
if (!isObject(originValue)) return originValue;
//对set类型的数据进行判断
if (originValue instanceof Set) {
let newSet = new Set();
//对传进来的set数据进行遍历
for (const item of originValue) {
//为了防止set中有对象的存在,所以再次使用递归
newSet.add(deepCopy(item));
}
return newSet;
}
//如果传入的参数是函数类型,则直接return出去
if (typeof originValue === "function") {
return originValue;
}
// 判断传入参数的类型,是对象还是数组
//对象与数组的判断,这只是其中一种方法
let newObj = Array.isArray(originValue) ? [] : {};
//对传入的对象进行遍历
for (const key in originValue) {
//应当对传入对象的value进行递归操作
newObj[key] = deepCopy(originValue[key]);
}
return newObj;
}
- 若传入的参数,值是symbol类型的
- 需要提前进行判断,返回一个新的Symbol()类型
function deepCopy(originValue) {
//若值是一个Symbol类型,则返回一个Symbol
if (typeof originValue === "symbol") {
return Symbol();
}
//应当对传入的参数进行判断,为普通类型,null均应该直接返回
if (!isObject(originValue)) return originValue;
//对set类型的数据进行判断
if (originValue instanceof Set) {
let newSet = new Set();
//对传进来的set数据进行遍历
for (const item of originValue) {
//为了防止set中有对象的存在,所以再次使用递归
newSet.add(deepCopy(item));
}
return newSet;
}
//如果传入的参数是函数类型,则直接return出去
if (typeof originValue === "function") {
return originValue;
}
// 判断传入参数的类型,是对象还是数组
//对象与数组的判断,这只是其中一种方法
let newObj = Array.isArray(originValue) ? [] : {};
//对传入的对象进行遍历
for (const key in originValue) {
//应当对传入对象的value进行递归操作
newObj[key] = deepCopy(originValue[key]);
}
return newObj;
}
- 若传入的key值是一个symbol
- 当key值是symbol的时候,无法通过for in 遍历得到key
- 需要使用 Object.getOwnPropertySymbols进行获取
- 之后再 newObj中增加key
function deepCopy(originValue) {
//若值是一个Symbol类型,则返回一个Symbol
if (typeof originValue === "symbol") {
return Symbol(originValue.description);
}
//应当对传入的参数进行判断,为普通类型,null均应该直接返回
if (!isObject(originValue)) return originValue;
//对set类型的数据进行判断
if (originValue instanceof Set) {
let newSet = new Set();
//对传进来的set数据进行遍历
for (const item of originValue) {
//为了防止set中有对象的存在,所以再次使用递归
newSet.add(deepCopy(item));
}
return newSet;
}
//如果传入的参数是函数类型,则直接return出去
if (typeof originValue === "function") {
return originValue;
}
// 判断传入参数的类型,是对象还是数组
//对象与数组的判断,这只是其中一种方法
let newObj = Array.isArray(originValue) ? [] : {};
//对传入的对象进行遍历
for (const key in originValue) {
//应当对传入对象的value进行递归操作
newObj[key] = deepCopy(originValue[key]);
}
//当key是Symbol的时候,要单独获取其key
let symbolKeys = Object.getOwnPropertySymbols(originValue);
for (const item of symbolKeys) {
newObj[Symbol(item.description)] = deepCopy(originValue[item]);
}
return newObj;
}
事件总线
做跨文件、跨组件的操作
- 当我们用vue开发的时候,需要进行组件与组件之间的传值,这时候就需要用到事件总线
//我们要将env中的数据,传递给main中去
--------env.vue
zcEventBus.emit("envClick","zhangcheng",198)
--------main.vue
zcEventBus.on("envClick",function(name,age){
console.log("监听到了")
})
- 以上是事件总线的使用方法
- on方法主要是用于将事件与回调函数做一个映射关系,一个事件可以对应多个回调函数
- emit方法主要是对回调函数的执行,以及参数的传递
class zcEventBus {
constructor() {
this.obj = {};
}
//on事件需要接收两个参数,事件名称,回调函数
on(eventName, callBackFn) {
//采用对象的结构,一个事件名称,后面跟着数组,可以包含多个事件
//{eventName:[fn1,fn2]}
//但是第一次执行的时候,this.obj[eventName]不是一个数组,需要进行判断
let eventArr = this.obj[eventName];
//第一次肯定是undefined,当!eventArr的时候,为true
if (!eventArr) {
//将this.obj[eventName]初始化数组
eventArr = [];
this.obj[eventName] = eventArr;
}
//相当于在this.obj[eventName]中push了事件
eventArr.push(callBackFn);
}
//emit事件需要接收两个参数,事件名称,以及参数
emit(eventName, ...arg) {
console.log(eventName);
//需要遍历eventName对用的事件数组,并依次执行
//首先判断是否存在,不存在直接返回
let eventArr = this.obj[eventName];
if (!eventArr) return;
//若存在,则直接遍历执行
for (const fn of eventArr) {
fn(...arg);
}
}
}