zoukankan      html  css  js  c++  java
  • debounced 和 throttle 函数的实现

    实现一个 debounce 函数

    先来一段 underscore 里面的函数注释

    When a sequence of calls of the returned function ends, the argument
    function is triggered. The end of a sequence is defined by the wait
    parameter. If immediate is passed, the argument function will be
    triggered at the beginning of the sequence instead of at the end.

    可以知道,在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。debounce 是一个高阶函数,返回的函数是作为某个事件回调来使用的。在给定的超时时间之内如果重复调用会重新计时。

    使用场合:

    1. 输入框监听最后的提交事件
    2. resize, mousemove, scroll 等最后的刷新重绘

    需要合并多次事件延迟触发成一次的其他场合,而不需要跟踪中间状态

    生活中的实例: 如果有人进电梯(触发事件),那电梯将在10秒钟后出发(执行事件监听器),这时如果又有人进电梯了(在10秒内再次触发该事件),我们又得等10秒再出发(重新计时)。

    初始版本

    根据上面的描述,可以知道每次调用的时候,需要重新开始计时,于是得到第一个初始版本。

    function debounce(func, wait) {
        let timeout;
        let debounced = () => {
            if (timeout) clearTimeout(timeout);
            timeout = setTimeout(function() {
                func();
                timeout = null;
            }, wait);
        }
        return debounced;
    }
    

    由于返回的 debounced 函数是有执行上下文的,例如下面的代码:

    var debounce_event_func = debounce(myMouseOver, 1000, true);
    document.querySelector("#app").addEventListener("mousemove", throttle_event_func);
    

    添加执行上下文

    throttle_event_func不仅仅需要接受 event 函数,还需要将 this 指向 Dom 元素,这里是 app 元素,所以要在 debounced 开始加上保存上下文的参数即可。

    function debounce(func, wait) {
        let timeout;
        let debounced = () => {
            let context = this;  // 保存 Dom 引用
            let args = arguments;  // 保存参数 [event, ...]
            if (timeout) clearTimeout(timeout);
            timeout = setTimeout(function() {
                func.apply(context, args);  // 这里使用 apply
                timeout = null;
            }, wait);
        }
        return debounced;
    }
    

    立即执行

    在 underscore 实现中,还有 immediate 这个参数,我们按照其语义修改函数定义

    function debounce(func, wait, immediate) {
        let timeout;
        let debounced = () => {
            let context = this; 
            let args = arguments; 
            if (timeout) clearTimeout(timeout);
            // 提取出公共部分
            let lazy = () => { 
                result = func.apply(context, args);
                timeout = null;
            };
            if (immediate) {
                let callNow = !timeout;
                timeout = setTimeout(lazy, wait);
                if (callNow) func.apply(context, args);
            } else {
                timeout = setTimeout(lazy, wait);
            }
        }
        return debounced;
    }
    

    最后优化

    最后,我们将函数执行结果返回(虽然可能没有用,但是应该返回一个值),添加 cancel 函数来取消操作。
    从而我们得到了最终的 debounced 函数。

    function debounce(func, wait, immediate) {
        let timeout, result;
        let debounced = () => {
            let context = this;
            let args = arguments;
            let lazy = () => {
                result = func.apply(context, args);
                timeout = null;
            };
            if (timeout) clearTimeout(timeout);
            if (immediate) {
                // 如果 timeout != null, 表示是中途取消状态
                let callNow = !timeout;
                timeout = setTimeout(lazy, wait);
                if (callNow) func.apply(context, args);
            } else {
                timeout = setTimeout(lazy, wait);
            }
            return result;
        }
        debounced.cancel = () => {
            clearTimeout(timeout);
            timeout = null;
        }
        return debounced;
    }
    window.debounce = debounce;
    

    实现一个 throttle 函数

    先来一段 underscore 里面的函数注释

    Returns a function, that, when invoked, will only be triggered at most once
    during a given window of time. Normally, the throttled function will run
    as much as it can, without ever going more than once per wait duration;
    but if you'd like to disable the execution on the leading edge, pass
    {leading: false}. To disable execution on the trailing edge, ditto.

    看起来和上面的 debounce 有点类似,也是限制高频操作的次数,但是并不是将多个重叠时间窗口的操作合并成一个,而是规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。

    使用场合

    1. resize, scroll, mousemove 等的动画
    2. 元素拖拽动画

    需要以低频来跟踪状态的场合,需要跟踪中间状态。也就是给大量事件按时间做平均分配触发。

    生活中的实例: 我们知道目前的一种说法是当 1 秒内连续播放 24 张以上的图片时,在人眼的视觉中就会形成一个连贯的动画,所以在电影的播放(以前是,现在不知道)中基本是以每秒 24 张的速度播放的,为什么不 100 张或更多是因为 24 张就可以满足人类视觉需求的时候,100 张就会显得很浪费资源。

    初始版本

    首先按照定义,可以使用时间戳来完成的版本。当函数触发,就检查当前时间戳减去保存的时间戳是否超出窗口。

    function throttle(func, wait) {
        let old = 0;
        let throttled = () => {
            let context = this;
            let args = arguments;
            let now = new Date().valueOf();
            if (now - old > wait) {
                func.apply(context, args);
                old = now;
            } 
        }
        return throttled;
    }
    

    窗口尾部的事件触发

    上面的实现可以完成 leading: true 同时 trailling: false 的功能,但是如果要想实现 trailling: true,就要使用定时器

    function throttle(func, wait) {
        let timeout;
    
        let throttled = () => {
            let context = this;
            let args = arguments;
            if (!timeout) {
                timeout = setTimeout(function () {
                    func.apply(context, args);
                    timeout = null;
                }, wait);
            }
        }
        return throttled;
    }
    

    将时间戳和定时器的方案结合

    上面的实现是 leading: false 同时 trailling: true 的功能,只需要将这两种方法一起使用,同时要保证时间戳触发之后,取消定时器。亦或定时器触发之后,取消时间戳即可。然后在函数参数上添加上 options 参数。

    function throttle(func, wait, options) {
        let old = 0;
        let timeout, result;
        if (!options) { options = {}; }
    
        let throttled = () => {
            let context = this;
            let args = arguments;
            let now = new Date().valueOf();
            if (options.leading === false) {
                old = now;
            }
            if (now - old > wait) {
                if (timeout) {
                    clearTimeout(timeout);
                    timeout = null;
                }
                func.apply(context, args);
                old = now;
                //////
                if (!timeout) context = args = null;
            } else if (!timeout && options.trailling === true) {
                timeout = setTimeout(function () {
                    old = new Date().valueOf();
                    func.apply(context, args);
                    timeout = null;
                }, wait);
            }
        }
        throttled.cancel = () => {
            clearTimeout(timeout);
            timeout = null;
            context = args = null;
        };
        return throttled;
    }
    
    

    最终版本

    添加 cancel 功能后,得到最后的实现。

    function throttle(func, wait, options) {
        let old = 0;
        let timeout, result;
        if (!options) { options = {}; }
    
        let throttled = () => {
            let context = this;
            let args = arguments;
            let now = new Date().valueOf();
            if (options.leading === false) {
                old = now;
            }
            if (now - old > wait) {
                if (timeout) {
                    clearTimeout(timeout);
                    timeout = null;
                }
                func.apply(context, args);
                old = now;
                //////
                if (!timeout) context = args = null;
            } else if (!timeout && options.trailling === true) {
                timeout = setTimeout(function () {
                    old = new Date().valueOf();
                    func.apply(context, args);
                    timeout = null;
                }, wait);
            }
        }
        throttled.cancel = () => {
            clearTimeout(timeout);
            timeout = null;
            context = args = null;
        };
        return throttled;
    }
    
  • 相关阅读:
    大二软件工程概论自主学习(第十一周)
    课堂练习1
    数据库的学习
    界面作业
    怎样连接连接数据库
    异常处理
    继承与多态
    String课后作业
    请查看String.equals()方法的实现代码,注意学习其实现方法。
    课后作业
  • 原文地址:https://www.cnblogs.com/yumingle/p/14125426.html
Copyright © 2011-2022 走看看