【问题标题】:How to use Throttle/Debounce with Delegated event listeners? (vanilla JS)如何将 Throttle/Debounce 与 Delegated 事件侦听器一起使用? (香草JS)
【发布时间】:2019-12-30 02:13:53
【问题描述】:

尝试优化某些 JS 的 UX,包括使用事件委托和节流 - 但似乎无法将这些方法结合起来。 我正在寻找一种方法来监听事件(滚动、鼠标移动等),识别元素,然后调用相关函数(节流)。

我尝试了各种方法,包括尝试添加包装器/中间函数(处理程序)以传递多个函数 - 但最终结果是相同的......我最终没有从 Throttle 函数返回任何内容。

我设法开始工作的唯一一件事是产生一个多节流功能,在其中我手动重复每个我希望节流的函数的节流功能。

function mythrottle() {
    var timer1, timer2;
    return function() {
        var now = Date.now();
        var last1 = timer1;
        var last2 = timer2;

        if(!last1) { timer1 = now;  callback1();  return; }
        if(!last2) { timer2 = now;  callback2();  return; }

        if(last1 + 500 > now) { return; }
        timer1 = now;  callback1();

        if(last2 + 1500 > now) { return; }
        timer2 = now;  callback2();
    }
}

function callback1(){
    console.log("callback 1 firing");
}

function callback2(){
    console.log("callback 2 firing");
}

window.addEventListener('scroll', mythrottle());

理想情况下,我愿意; 1) 每个事件一个事件监听器 2)过滤/限定事件的能力(点击按钮或链接或跨度等) 3)然后使用特定函数(取决于过滤器/限定符)调用节流函数,传递函数和延迟 4)油门处理延迟事件(立即触发,然后等待)

这将避免我目前必须做的事情,即为每个单独的事情复制并准备几个小块(我必须用限定符/过滤器包装每个块!)。

【问题讨论】:

    标签: javascript event-handling listener throttling debouncing


    【解决方案1】:

    有很多方法可以做你想做的事。无论如何,要使其动态工作(添加/删除/配置回调),您需要一个状态。这就是为什么我将所有内容都封装在一个类中。

    每个回调都将被独立跟踪(以允许根据请求有不同的延迟)。实例化后,您可以添加更多回调和/或事件。显然,这个示例可以扩展/改进,但我认为这是一个好的开始,它应该满足您的所有要求。

    如果您对下面的代码有任何疑问,请随时提问,我们很乐意为您解答。

    • 注意:我使用了现代语法,但可以轻松重写以提高兼容性。
    
    // -- THROTTLER --
    
    class Throttler {
    
        constructor(args) {
    
            this.queue = [];
            this.throttle = typeof args.throttle === "number" ? args.throttle : 500; // GLOBAL THROTTLE
            this.threshold = typeof args.threshold === "number" ? args.threshold : 50; // GLOBAL THRESHOLD
    
            // BIND METHODS
            this.addCallback = this.addCallback.bind(this);
            this.addEvent = this.addEvent.bind(this);
            this.handler = this.handler.bind(this);
    
            if (Array.isArray(args.queue)) {
                // SETUP INITIAL CALLBACKS
                args.queue.forEach(this.addCallback);
            }
    
            if (Array.isArray(args.events)) {
                // SETUP INITIAL EVENTS
                args.events.forEach(this.addEvent);
            }
    
        }
    
        addCallback(cb) {
            if (typeof cb === "function") {
                this.queue.push({
                    throttle: this.throttle,
                    threshold: this.threshold,
                    callback: cb,
                    timer: null,
                    ref: Date.now(),
                    elapsed: 0
                });
            } else if (typeof cb === "object") {
                this.queue.push({
                    throttle: cb.debounce ? undefined : this.throttle,
                    threshold: this.threshold,
                    ...cb,
                    timer: null,
                    ref: Date.now(),
                    elapsed: 0
                });
            }
        }
    
        addEvent(eventName) {
            // ATTACH HANDLER
            window.addEventListener(eventName, this.handler);
        }
    
        handler(e) {
    
            this.queue.forEach((elem) => {
    
                const NOW = Date.now();
    
                if (typeof elem.throttle === "number") { // THROTTLE
    
                    elem.elapsed += NOW - elem.ref;
                    elem.ref = NOW;
    
                    if (elem.elapsed >= elem.throttle) {
    
                        // EXECUTE CALLBACK
                        if (typeof elem.callback === "function") {
                            if (typeof elem.selector !== "string" || (typeof e.target.matches === "function" && e.target.matches(elem.selector))) {
                                if (typeof elem.eventName !== "string" || e.type === elem.eventName) {
                                    elem.callback(e);
                                }
                            }
                        }
    
                        // RESET COUNTER
                        elem.elapsed = 0;
    
                    }
    
                    // KILL TIMER
                    elem.timer && clearInterval(elem.timer);
    
                    // RE-CREATE TIMER
                    elem.timer = setTimeout(() => {
    
                        // RESET COUNTER
                        elem.elapsed = 0;
    
                        // RESET TIMER
                        elem.timer = null;
    
                    }, elem.threshold);
    
                } else if (typeof elem.debounce === "number") { // DEBOUNCE
    
                    // KILL TIMER
                    elem.timer && clearInterval(elem.timer);
    
                    // RE-CREATE TIMER
                    elem.timer = setTimeout(() => {
    
                        // EXECUTE CALLBACK
                        if (typeof elem.callback === "function") {
                            if (typeof elem.selector !== "string" || (typeof e.target.matches === "function" && e.target.matches(elem.selector))) {
                                if (typeof elem.eventName !== "string" || e.type === elem.eventName) {
                                    elem.callback(e);
                                }
                            }
                        }
    
                        // RESET TIMER
                        elem.timer = null;
    
                    }, elem.debounce);
    
                }
    
            });
    
        }
    
    }
    
    
    
    // --- USAGE ---
    
    // INSTANTIATE THROTTLER
    const myThrottler = new Throttler({
        throttle: 1500,
        events: ['scroll', 'mousemove'],
        queue: [callback1, callback2]
    });
    
    // ADD ANOTHER EVENT
    myThrottler.addEvent('resize');
    
    // ADD CONDITIONAL CALLBACK
    myThrottler.addCallback({
        callback: callback3,
        selector: '*',
        eventName: 'mousemove'
    });
    
    // ADD CUSTOM DELAY DEBOUNCED CALLBACK
    myThrottler.addCallback({
        callback: callback4,
        debounce: 2000
    });
    
    // ADD CUSTOM DELAY THROTTLED CALLBACK
    myThrottler.addCallback({
        callback: callback5,
        throttle: 3000
    });
    
    
    
    // --- CALLBACKS ---
    
    function callback1() {
        console.log("CB 1");
    }
    
    function callback2() {
        console.log("CB 2");
    }
    
    function callback3() {
        console.log("CB 3");
    }
    
    function callback4() {
        console.log("CB 4");
    }
    
    function callback5() {
        console.log("CB 5");
    }
    
    

    【讨论】:

    • 哇——难怪我在挣扎——这很复杂。我会玩一下它,看看我要多久才能打破它或感到困惑:D 谢谢!
    • 好的 - 玩得很快(无法抗拒):D 我为我想要的每个单独的东西实例化一个新的节流器,对吗? (所以我为“mousemove”创建一个节流器并分配我想要的回调,然后为“click”和我想要的回调创建一个?)。并且不确定我是否破坏了某些东西,但默认延迟的行为更像是 Debounce 而不是 Throttle?
    • @ClearlyConfusedClyde 我刚刚意识到我只是添加了去抖动。我已经编辑了我的答案以支持节流(默认情况下)。您实际上只需要一个 Throttler 实例,然后您可以将多个事件和回调附加到它。之后,使用“选择器”和“事件名称”选项设置规则。
    • @ClearlyConfusedClyde 'selector' 选项接受任何 CSS 选择器('#myId'、'.myClass'、'div > span' 等),并将针对事件目标元素进行测试。默认情况下,将为任何目标触发每个回调。 'eventName' 选项允许您仅针对特定事件触发回调(例如:'mousemove')。
    • @ClearlyConfusedClyde 我上次编辑了代码以允许在用户交互结束时自动重置限制(阈值)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-11-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-17
    • 1970-01-01
    相关资源
    最近更新 更多