每日刷题:防抖与节流 ES6

99 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

一 防抖

触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。

<!DOCTYPE html>
<html lang="zh-cmn-Hans">

<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
    <title>debounce</title>
    <style>
        #container{
            width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
        }
    </style>
</head>

<body>
    <div id="container"></div>
    <input type="text" id="inp">
</body>

</html>

第一版 this & arguments

问题一: 使用debounce之后, getUserAction函数内的this指向window,不再指向container

问题二: event 无法传递到 getUserAction

'use strict'
let count = 1;
let container = document.getElementById('container');

function getUserAction(e) {
    console.log(e);

    // console.log(this);
    // 作为setTimeout的回调函数,它的this指向window
    container.innerHTML = count++;
};


function debounce(func, wait) {
    //写一个计时器,到点才会进行下一次
    let timeout;

    return function () {
        // console.log(this);
        // 这里的this指的是container
        
        clearTimeout(timeout)

        // 解决问题二: 传递event对象
        // console.log(arguments); 
        // 除了箭头函数,普通函数内部都有arguments

        // 解决问题一: this的指向
        // 使用apply 和 call 的话,需要用匿名函数
        // () => func.apply(this,arguments) 
        // () => func.call(this,...arguments)
        // bind返回的是一个函数, 可以直接使用
        timeout = setTimeout(func.bind(this, ...arguments), wait);
    }
}

container.addEventListener('mousemove', debounce(getUserAction, 1000));

第二版 立即执行

实现效果:第一次mousemove触发后,立即执行,等n 秒之后再触发才能再执行。

// timeout: setTimeOut函数会返回一个 timerId,我们用变量timeout储存这个id
// 向 clearTimeout 传入这个timerId 来删除这个计时器。

function debounce(func, wait, immediate) {
    let timeout;

    return function () {
        if (timeout) clearTimeout(timeout);

        if (immediate) {
            // 如果已经执行过,不再执行
            let callNow = !timeout;
            timeout = setTimeout(function(){
                //1st mousemove: timeout===undefined,于是执行第一次count++
                //设置异步函数,到时间后timeout为null,--> timer1
                
                //2~n mousemove: n秒内再次触发,
                //timeout===timer1的id, 类真值 --> 清除timer1,
                //timeout===timer1的id,callNow = !timeout = false,
                //call setTimeout,设置timer2... timeout = null,等待下一个wait之后
                //才能再次立即执行
                timeout = null;
            }, wait);
            if (callNow) func.apply(this, arguments);
        } else {
            // immediate === false 执行第一版的函数
            timeout = setTimeout(func.bind(this, ...arguments), wait);
        }

    }
}

第三版 返回结果

有些函数是会返回值的,这个版本是为了满足这个需求。

function debounce(func, wait, immediate) {

    let timeout, result;

    return function () {

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            let callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(this, arguments);
        }
        else {
            timeout = setTimeout(func.bind(this, ...arguments), wait);
        }
        // 立即执行才会有 getUserAction 返回的结果
        // 否则执行异步函数,在getUserAction返回结果前,debounce就已经return了,此时返回的是undefined
        return result;
    }
}

第四版 取消

实现效果: 一键取消防抖

function debounce(func, wait, immediate) {

    let timeout, result;

    let debounced = function () {
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            let callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(this, arguments);
        }
        else {
            timeout = setTimeout(func.bind(this, ...arguments), wait);
        }
        return result;
    };

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

使用方法:

const setUseAction = debounce(getUserAction, 10000, true);

container.addEventListener('mousemove', setUseAction);

document.getElementById("button").addEventListener('click', function(){
    setUseAction.cancel();
    // 这里要在function里面call,不然会在页面渲染的时候就执行
    // 点击按钮不会再执行
});

二 节流

高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率。每次触发事件时都判断当前是否有等待执行的延时函数。

第一版 使用时间戳timestamp

// 第一版
function throttle(func, wait) {
    let previous = 0;

    return function() {
        let now = +new Date(); //把Now转化为数字保存

        if (now - previous > wait) { 
        // 1st mousemove now - previous 一定大于 wait
            func.apply(this, arguments); // 触发getuseraction
            previous = now; // 用pre保存now 2~n次间隔都需要达到wait,才能触发getuseraction
        }
    }
}

container.addEventListener('mousemove', throttle(getUserAction, 3000));

如果是在wait期间停止触发,就不会再执行事件。

第二版 使用定时器

// 第二版
function throttle(func, wait) {
    let timeout;
    
    // 第一次mouseover后,不会有任何变化显示,等待wait事件结束才会触发getUserAction

    
    return function() {
        if (!timeout) { // 1st mousemove, timeout === undefined 
            // timeout = timer1的id
            timeout = setTimeout(() => { // 这里用箭头函数, 会继承容器的this
                timeout = null; 
                func.apply(this, arguments) // 在wait结束后触发getUserAction
                // 如果在wait期间再次触发, 因为 timeout = timer1的id
                // 所以会直接结束,没有任何操作
                // 需要等到wait 结束timeout = null 之后,再次触发,进入下个循环
            }, wait)
        }

    }
}

container.addEventListener('mousemove', throttle(getUserAction, 3000));

同样是在wait期间停止触发,就不会再执行事件。

对比:

image.png

第三版 两者结合

鼠标移入能立刻执行,停止触发的时候还能再执行一次。

// 第三版
function throttle(func, wait) {
    let timeout;

    let previous = 0;

    let later = function(that) {
        // 设置time = null, 可以进入下一个Block 2
        previous = +new Date();
        timeout = null; 
        func.apply(that, arguments)
    };

    let throttled = function() {
        let now = +new Date();

        // now - previous = interval
        // remaining = wait - interval
        //下次触发 func 剩余的时间
        let remaining = wait - (now - previous);
         // 如果没有剩余的时间了或者你改了系统时间
        if (remaining <= 0 || remaining > wait) {
            // Block 1
            // 1) wait - interval <= 0  -> interval >= wait 
            //   1.1  1st mousemove,remaing 必然 <= 0
            //   1.2  later 执行后,过了超过wait时长,才再次触发
            //       因为重新赋值了pre, 如果在wait之内,会进入Block 2

            // 2) wait - interval > wait -> now - previous < 0 基本上不可能,除非是改了时间
            // console.log(1);

            if (timeout) { // 1.1的情况timeout = undefined不会进入
                // console.log(3);
                clearTimeout(timeout);
                timeout = null; 
                // 这段if block 是给 2)设置的
                // 已经设置了timer,被调了系统时间
                // 把之前的timeout清除,timeout清空,立即之后,进入正常的循环
            }

            previous = now; // pre赋值时间戳
            func.apply(this, arguments); // 1.1 1st mousemove 立即执行
        } else if (!timeout) { 
            // Block 2
            // 2nd mousemove 在wait期间内触发
            // 等wait结束后触发later, 之后在此期间内如果多次触发
            // 因为timeout有id, 不满足这两个条件,会直接结束
            // console.log(2);
            timeout = setTimeout(later.bind(null,this), remaining); 
        }
    };
    return throttled;
}

第四版 优化

有头无尾, 或者无头有尾。

没有无头无尾 {leading: false, trailing: false}

function throttle(func, wait, options) {
    let timeout;
    let previous = 0;
    if (!options) options = {};

    let later = function(that) {
        previous = options.leading === false ? 0 : new Date().getTime(); 
        // 1) leading = false pre = 0; 保证wait结束后再次触发还是进入Block 2
        timeout = null;
        func.apply(that, arguments);
    };
    
    let throttled = function() {
        let now = new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        // 1) leading = false, 1st mousemove
        // pre = 0 and leading = false
        // remaining = wait - 0 > 0 进入Block 2

        let remaining = wait - (now - previous);

        if (remaining <= 0 || remaining > wait) {
            // Block 1
            // 2) tailing = false
            // 1st mousemove remaining注定<0
            // 2~n wait 时间内, 会因为remaining > 0 且 trail = false直接走完函数
            // 超过wait时间,满足remaining <= 0,再次进入block 1

            // 3) lead = false && tail = false
            // 因为trail = false,所以不能进入Block 2
            // 1st 因为pre = 0, 所以 pre = now, 不进入任何block
            // 2nd 在wait时间内, remaing > 0, 不进入任何block
            //  超过wait时间,remaing < 0, 立即执行;
            // 只有这一种方式能使count++,但是没做到无头无尾


            // console.log(1);
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(this, arguments);
            
        } else if (!timeout && options.trailing !== false) {
            // Block 2
            // 立即执行后,timeout = null, 且 remaining > 0
            // 需要添加 trail != false 阻止进入Block 2
            // console.log(2);
            timeout = setTimeout(later.bind(null,this), remaining);
        }
    };
    return throttled;
}

第五版 取消

...
    throttled.cancel = function() {
        clearTimeout(timeout);
        previous = 0;
        timeout = null;
    }
    // 添加到return前面,用法跟防抖第四版一样
...

防抖参考文章 节流参考文章
这两篇文章都很好,但用的是ES5,所以我在分析的时候,用ES6改写了部分语句。