denounce函數:Javascript中如何應對高頻觸發事件


在DOM Event的世界中,以scroll、resize、mouseover等為代表的高頻觸發事件顯得有些與眾不同。通常,DOM事件只有在明確的時間點才會被觸發,比如被點擊,比如XMLHttpRequest狀態更改等等;而高頻事件則是在整個動作時期內反復觸發反復調用callback,為整個APP的流暢運行留下了性能隱患。

甚至w3school在介紹mousemove事件時還為大家留下了貼心小提示:
"每當用戶把鼠標移動一個像素,就會發生一個 mousemove 事件。這會耗費系統資源去處理所有這些 mousemove 事件。因此請審慎地使用該事件。"
http://www.w3school.com.cn/jsref/event_onmousemove.asp

事實上,解決這類問題已經有了比較成熟的通用解決方案——denounce。denounce的核心思想在於,事件發生時,首先觸發一個輕量級的proxy,再由這個輕量級的proxy去管理和調用真正的業務函數。這樣的方案之所以能夠提升效率,關鍵在於proxy並不會每次都去調用業務函數。"並不會每次都去調用"又是怎么實現的呢?其實就是前端開發者們再熟悉不過的setTimeout——由於高頻函數的特點是反復快速觸發,我們可以借助setTimeout去延遲調用業務函數;如果短時間內該事件被再次觸發,由於setTimeout中的業務函數尚未被真正調用,我們尚有機會用clearTimeout取消其執行,並重新添加新的setTimeout調用;重復以上步驟,直至該動作(比如scroll或resize)結束不再觸發事件,延遲順利結束后,業務函數才被調用。

對denounce函數的實現不感興趣的同學可以移步Underscore.js官網文檔,了解一下denounce的使用方式;對實現感興趣的同學可以繼續閱讀,下文將介紹本函數的具體實現,會比Underscore.js所提供的功能稍微全面一些喲。

首先來分析一下核心函數所需要的參數:作為事件監聽機制的一種擴展,被監聽的對象肯定是需要的;然后是所要監聽的事件類型以及事件被觸發時的callback;最后是setTimeout函數等待的時間delay。參數都有了,現在就來嘗試實現核心函數:

/**
 * @param node 被監聽的DOM對象
 * @param event 事件類型,比如scroll
 * @param callback 回調函數
 * @param delay 等待的毫秒數
 */
function denounce(node, event, callback, delay) {
    // 記錄timeout id,在閉包中使用
    var timeout; 
function proxy(e) { // 如果短時間內再次被調用,則清除上次觸發時設置的timeout clearTimeout(timeout); // 調用setTimeout函數添加延遲執行的邏輯 timeout = setTimeout(callback.bind(node, e), delay); } // 將proxy函數綁定到node的指定事件上 node.addEventListener(event, proxy, false); } // 綁定事件: denounce(window, 'scroll', function (e) { console.log(e); }, 200);

具體實現還是非常簡單的,只要對setTimeout和clearTimeout有初步的理解,應該都可以輕松讀懂上面這一段代碼。當然,作為事件監聽的一種應用,除了應該提供"on"方法之外,至少還應該提供接觸事件綁定的"remove"方法。下文是相對比較全面的實現:

 

/**
 * Denounce對象,主要實現了on和remove方法,目前支持對scroll、resize和mousemove三種事件的延遲觸發
 * 注意:本對象各方法未處理DOM事件監聽的兼容性問題
 *
 * @object
 */
var Denounce = {
    // denounce id,用於記錄事件和取消事件監聽
    _id: 1,
    // 所支持的事件類型
    EVENTS: {
        SCROLL: 'scroll',
        RESIZE: 'resize',
        MOUSEMOVE: 'mousemove'
    },
    // 默認的setTimeout等待時間,可重置
    DELAY: 200,
    // 保存已有的監聽事件
    listeners: {},

    /**
     * 用於監聽事件的on方法
     *
     * @param node 被監聽的DOM對象
     * @param event 事件類型,比如scroll
     * @param callback 回調函數
     * @param delay 等待的毫秒數
     * @return denounce id,用於取消事件監聽
     */
    on: function (node, event, callback, delay) {
        var self = this;
        var id = self._id++;
delay
= !!delay ? delay : self.DELAY; self.listeners[id] = { node: node, event: event, timeout: -1, proxy: function (e) { var listener = self.listeners[id]; clearTimeout(listener.timeout); listener.timeout = setTimeout(callback.bind(node, e), delay); } }; node.addEventListener(event, self.listeners[id].proxy, false); return id; }, /** * 用於取消事件監聽的remove方法 * * @param denounce id 由on方法返回的id */ remove: function (id) { var self = this; var listener = self.listeners[id]; if (!listener) { return; } clearTimeout(listener.timeout); listener.node.removeEventListener(listener.event, listener.proxy, false); self.listeners[id] = null; } }; // 綁定事件 var id = Denounce.on(window, Denounce.EVENTS.SCROLL, function (e) { console.log(e); }, 200); // 取消綁定 Denounce.remove(id);

其中,delay設置的越大,動作結束后等待的延遲越久,反應較為“遲鈍”,但被誤判為動作結束的可能性較小;反之則延遲越低,反應較快,但被誤判為動作結束的可能性較大。同學們可以根據自己的業務場景和需求自己調節該參數。其中,delay設置的越大,資源消耗越低,但反應遲鈍;delay設置的越小,資源消耗越高,但反應較快;同學們可以根據自己的業務場景和需求自己調節該參數。 該描述容易和throttle函數的用途混淆,感謝@木的樹 同學批評指正)

 

(全文完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM