重学JavaScript高级(十四): 手写工具函数(防抖-节流-深浅拷贝-时间总线)

发布时间:2024年01月24日

手写工具函数(防抖-节流-深浅拷贝-时间总线)

认识防抖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函数

函数的执行,会按照固定的频率来执行

可以看做游戏中,角色的攻击速度,角色的发出去的攻击,不会按照玩家点击的速度发射子弹,而是又固定发射子弹的频率

  • 与防抖的最大区别就是,函数执行是按照固定频率来执行的

image.png

基本实现

  • 使用定时器方式实现
    • 此种方式,与防抖函数不同的是,对存在的定时器处理方式不同
    • 防抖对于存在的定时器,是进行清除操作,而节流是对存在的定时器,进行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;
}

手写拷贝-事件总线

深拷贝-浅拷贝-引用赋值的关系

网上的一些文章,对于深拷贝和浅拷贝的概念会有些混淆,深拷贝和浅拷贝都会创建新的引用类型,不同的是,对于引用类型中包含引用类型的数据处理不同

  • 引用赋值:直接用等号进行赋值,就属于引用赋值,两个变量引用的同一个引用类型的地址
  • **浅拷贝:**首先明确的一点就是 浅拷贝肯定会生成一个新的引用类型,但是引用类型的内部还有 引用类型,改变其中一个另外一个会收到影响
  • **深拷贝:**通过深拷贝的方法创建一个新的 **引用类型,**两者都不会收到影响

image.png

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);
    }
  }
}
文章来源:https://blog.csdn.net/weixin_55041125/article/details/135828222
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。