【源码共读·Day2】跟着underscore学防抖

146 阅读4分钟

♥️♥️♥️ 李哈哈要奋发啦~冲冲冲!!!相信只要坚持学习会有所回报的!♥️♥️♥️

1. 前言

前段时间刚好看完冴羽的专栏《JavaScript专题系列》的内容,里面就有《JavaScript专题之跟着underscore学防抖》,趁着源码共读活动,来做个总结,加深印象!

2.为什么要使用函数防抖

前端开发中,会遇到一些事件会在短时间内频繁触发,例如调整窗口大小(resize)或页面滚动(scroll),鼠标移动mousemovemousedown等等。不做限制的话一秒就可能执行几十次,几百次。

如果在这些函数内部执行了其他函数,尤其是执行了操作 DOM 的函数(浏览器操作 DOM 是很耗费性能的),那不仅会浪费计算机资源,还会降低程序运行速度,甚至造成浏览器卡死、崩溃。除此之外,短时间内重复的 ajax 调用不仅会造成数据关系的混乱,还会造成网络拥塞,增加服务器压力。

3.函数防抖的原理

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

4.使用案例

<!-------html代码------->
<div id="container"></div>
//<------ js代码 ------>
var count = 1;
var container = document.getElementById('container');

function getUserAction() {
    container.innerHTML = count++;
};

container.onmousemove = debounce(getUserAction, 1000);

5.代码实现

简单版:首先会想到肯定要用到定时器setTimeout(),触发事件后在n秒后调用要执行的函数,如果在n秒内再次被触发则清除定时器,重新计时。注意使用debounce函数后,this的指向问题 && event对象*(参数)获取问题。

/*
** fn:要执行的函数
** wait:执行等待的时间
*/
function debounce(func, wait){
    var timeout, args, context;
    return function(){
        context = this;
        args = aguments;
        
        clearTimeout(timeout);
        timeout = setTimeout(function(){
            //this指向windows,所以通过apply改变this的指向
            func.apply(context, args)
            
        },wait);
    }
}

豪华版:上面代码已经很完善了,但想要更加的完美。希望函数可选择立即执行,而不是等到n秒后。加个 immediate 参数判断是否是立刻执行。且函数可能有返回值。

/*
** immediate:判断函数是否立即执行
*/
function debounce(func, wait, immediate){
    var timeout, args, result, context;
    return function(){
        context = this;
        args = aguments;
        
        if(timeout) clearTimeout(timeout);
        if(immediate){
            var callNow = !timeout;
            timeout = setTimeout(function(){
                  timeout = null;
            }, wait)
            if(callNow){
                result = func.apply(context, args);   
            }
        }else{
            timeout = setTimeout(function(){
                //this指向windows,所以通过apply改变this的指向
                func.apply(context,args);

            },wait);
        }
        return result
    }
}

⚠️注意:当 immediate 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,所以我们只在 immediate 为 true 的时候返回函数的执行结果。

终极版:增加取消的功能,当时间间隔设置太长,立即执行后不想等,有个按钮可以取消debounce函数重新执行。

/*
** immediate:判断函数是否立即执行
*/
function debounce(func, wait, immediate){
    var timeout, args, result, context;
    var debounced = function(){
        context = this;
        args = aguments;
        
        if(timeout) clearTimeout(timeout);
        if(immediate){
            var callNow = !timeout;
            timeout = setTimeout(function(){
                  timeout = null;
            }, wait)
            if(callNow){
                result = func.apply(context, args);   
            }
        }else{
            timeout = setTimeout(function(){
                //this指向windows,所以通过apply改变this的指向
                func.apply(context,args);

            },wait);
        }
        return result
    }
    debounced.cancel = function(){
        clearTimeout(timeout);
        timeout = null;
    }
    return debounced; 
}

//使用
//let setAction = debounce(getUserAction, 1000, true);
//setAction.cancel();

以上是跟着冴羽前辈分享的文章学习后的复盘,思路清晰,由简单到复杂,易懂。让我们再学习下underscore的源码,看是否理解。

6.underscore源码

引入的now.js

export default Date.now || function(){
    return new Date().getTime();
}

引入了restArgunments,github地址:🔗链接

import restArguments from './restArguments.js';
import now from './now.js';

export default function debounce(func, wait, immediate){
    var timeout, previous, args, result, context;
    var later = function(){
        var passed = now() - previous; //前后点击的时间间隔
        if(wait > passed){
            timeout = setTimeout(later, wait - passed); //***问题一***
        }else{
            timeout = null;
            if(!immediate) result = func.apply(context,args); //非立即执行时调用函数
            //这个检查是必须的,因为func可以递归调用debounced
            if(!timeout) args = context = null; //***问题二***
        }
    }
    var debounced = restArguments(function(_args){
        context = this;
        args = _args;
        previous = now(); //每次调用debounce函数都会获取到新的时间
        if(!timeout){
            timeout = setTimeout(later, wait);
            if(immediate) result = func.apply(context, args);  //立即执行时调用函数
        }
        return result;
    })
    debounced.cancel = function(){
        clearTimeout(timeout);
        timeout = args = context = null;
    }
    return debounced;
}

知识点:

  1. setTimeout是有返回值的,表示当前的setTimeout在页面中的所有setTimeout中的序号。
  2. function.length指的是函数的形参个数,也就是函数定义时的参数个数,而不是函数实际接受的参数个数。arguments为函数接收到的所有实参组成的(类)数组。且箭头函数没有arguments。
function add(a,b){
    console.log("fun.length:",add.length); //2
    console.log("arguments",arguments); // Arguments(4)[1,2,3,4,callee:f,...]
    return a+b
}
add(1,2,3,4)
  1. 剩余参数函数restArguments。

思考: (问题对应上面代码中标志系列)

  1. 问题一:当调用debounce函数wait>passed时,是不是都会创建一个定时器,这些定时器创建的越来越多,且没有及时调用clearTimeout清除,这样是不是不好。
  2. 问题二:上面已经有 timeout = null,不就说明!timeout肯定为真吗,为什么这还要用if判断一下