Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【跟着大佬学JavaScript】之节流 #2

Open
yihan12 opened this issue Jul 5, 2022 · 0 comments
Open

【跟着大佬学JavaScript】之节流 #2

yihan12 opened this issue Jul 5, 2022 · 0 comments
Labels
【跟着大佬学JavaScript】 跟着大佬学系列

Comments

@yihan12
Copy link
Owner

yihan12 commented Jul 5, 2022

前言

js的典型的场景

  • 监听页面的scroll事件
  • 拖拽事件
  • 监听鼠标的 mousemove 事件
    ...

这些事件会频繁触发会影响性能,如果使用节流,降低频次,保留了用户体验,又提升了执行速度,节省资源。

原理

节流的原理:持续触发某事件,每隔一段时间,只执行一次。

通俗点说,3 秒内多次调用函数,但是在 3 秒间隔内只执行一次,第一次执行后 3 秒 无视后面所有的函数调用请求,也不会延长时间间隔。3 秒间隔结束后则开始执行新的函数调用请求,然后在这新的 3 秒内依旧无视后面所有的函数调用请求,以此类推。

简单来说:每隔单位时间( 3 秒),只执行一次。

实现方式

目前比较主流的实现方式有两种:时间戳、定时器。

时间戳实现

使用时间戳实现:首先初始化执行事件的时间previous为0,然后将当前的时间戳减去上次执行时间(now - previous),如果大于wait,则直接执行函数,并且将此时的执行时间now赋给previous(previous = now)。

由于首次previous = 0,则此时函数第一次触发就会立即执行。

后续则每隔wait时间执行一次,如果停止触发,则不会再执行函数。

// 由于一开始now - 0 > wait,则这个写法,时间会立即执行,没过一秒会执行一次,停止触发,则不会再执行事件
function throttle(func, wait = 500) {
    let context, now;
    let previous = 0; // 设置过去的执行时间初始值为0
    return function (...args) {
        context = this;
        now = +(Date.now() || new Date().getTime());
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    };
}

定时器实现

使用定时器实现:首先初始化timeout,然后定义!timeout为true的情况下,直接执行setTimeout,,等待wait时间后执行函数,然后清空timeout,以此类推,重新进入也会按上述执行。

由于进入函数,就执行setTimeout,所以不会立即触发函数执行。

后续则每隔wait时间执行一次,如果停止触发,而后还会触发执行一次函数。

// 由于一进入就创建了定时器,所以不会立即触发函数执行
function throttle(func, wait = 500) {
    let context, timeout;
    
    return function (...args) {
        context = this;
        
        if (!timeout) {
            timeout = setTimeout(function () {
                timeout = null;
                func.apply(context, args);
            }, wait);
        }
    };
}

合并版本

如果,我们需要既刚开始就立即执行,停止触发后,还会触发执行一次函数。

下面,我们将定时器和时间戳合并,组成一个全新的节流版本。

function throttle(func, wait = 500) {
    let context, timeout, result;
    let previous = 0;
    const throttled = function (...args) {
        context = this;
        const now = +(Date.now() || new Date().getTime()); // 当前时间
        // 下次触发 func 剩余时间
        const remaining = wait - (now - previous);
        
        // 如果没有剩余时间或者改了系统时间,这时候不需要等待,直接立即执行,这样就会第一次就执行
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
        } else if (!timeout) {
            // 剩余的情况就是remaining<=wait的情况,这里使用setTimeout就可以最后也会执行一次
            timeout = setTimeout(function () {
                timeout = null;
                previous = +(Date.now() || new Date().getTime()); // 这里是将previous重新赋值当前时间
                func.apply(context, args);
            }, remaining);
        }
    };
    return throttled;
}

合并版本优化

由于合并后的版本并没用返回值的优化+取消功能。

下面对代码进行返回值+取消功能优化:

function throttle(func, wait = 500) {
    let context, timeout, result;
    let previous = 0;
    
    const showResult = function (e1, e2) {
        result = func.apply(e1, e2);
        return result;
    };
    
    const throttled = function (...args) {
        context = this;
        const now = +(Date.now() || new Date().getTime()); // 当前时间
        // 下次触发 func 剩余时间
        const remaining = wait - (now - previous);
        
        // 如果没有剩余时间或者改了系统时间,这时候不需要等待,直接立即执行,这样就会第一次就执行
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            return showResult(context, args);
        } else if (!timeout) {
            // 剩余的情况就是remaining<=wait的情况,这里使用setTimeout就可以最后也会执行一次
            timeout = setTimeout(function () {
                timeout = null;
                previous = +(Date.now() || new Date().getTime()); // 这里是将previous重新赋值当前时间
                return showResult(context, args);
            }, remaining);
        }
        retrun result
    };
    
    throttled.cancel = function () {
        if (timeout !== undefined) {
            clearTimeout(timeout);
        }
        previous = 0;
        context = timeout = result = undefined;
    };
    return throttled;
}

功能性优化

有时候,我们也希望无头有尾,或者有头无尾。

function throttle(func, wait = 500, options = {}) {
    let context, timeout, result;
    let previous = 0;
    
   // 如果同时设置无头无尾,则直接使用默认设置,其他情况,则走下述操作
    if (!(options.leading === false && options.trailing === false)) {
        leading = !!options.leading; // 默认去除立即执行部分
        trailing = "trailing" in options ? !!options.trailing : true; // 默认保留尾部
    }
    
    // 返回原函数的return
    const showResult = function (e1, e2) {
        result = func.apply(e1, e2);
        return result;
    };
    
    // 获取当前时间
    const getNow = function () {
        return +(Date.now() || new Date().getTime());
    };
    
    const throttled = function (...args) {
        context = this;
        const now = getNow(); // 当前时间
        // 下次触发 func 剩余时间
        if (!previous && leading === false) previous = now;
        const remaining = wait - (now - previous);
        
        // 如果没有剩余时间或者改了系统时间,这时候不需要等待,直接立即执行,这样就会第一次就执行
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            return showResult(context, args);
        } else if (!timeout && trailing !== false) {
            // 剩余的情况就是remaining<=wait的情况,这里使用setTimeout就可以最后也会执行一次
            timeout = setTimeout(function () {
                timeout = null;
                previous = options.leading === false ? 0 : getNow(); // 这里是将previous重新赋值当前时间
                return showResult(context, args);
            }, remaining);
        }
        return result;
    };
    
    throttled.cancel = function () {
        if (timeout !== undefined) {
            clearTimeout(timeout);
        }
        previous = 0;
        context = timeout = result = undefined;
    };
    return throttled;
}

这里,如果options不传参数,函数默认设置

let leading = false
let trailing = true

也就是无头有尾。

如果同时设置无头无尾,则会直接采用默认设置,无头有尾。

// 如果同时设置无头无尾,则直接使用默认设置,其他情况,则走下述操作
if (!(options.leading === false && options.trailing === false)) {
    leading = !!options.leading; // 默认去除立即执行部分
    trailing = "trailing" in options ? !!options.trailing : true; // 默认保留尾部
}

演示地址

可以去Github仓库查看演示代码

跟着大佬学系列

主要是日常对每个进阶知识点的摸透,跟着大佬一起去深入了解JavaScript的语言艺术。

后续会一直更新,希望各位看官不要吝啬手中的赞。

❤️ 感谢各位的支持!!!

❤️ 如果有错误或者不严谨的地方,请务必给予指正,十分感谢!!!

❤️ 喜欢或者有所启发,欢迎 star!!!

参考

@yihan12 yihan12 added the 【跟着大佬学JavaScript】 跟着大佬学系列 label Jul 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
【跟着大佬学JavaScript】 跟着大佬学系列
Projects
None yet
Development

No branches or pull requests

1 participant