JavaScript事件機制與性能優化:防抖 / 節流 / 事件委托 / Passive Event Listeners 全解析

目標:把“為什么慢、卡頓從哪來、該怎么寫”一次說清。本文先講事件傳播與主線程瓶頸,再給出四件法寶(防抖、節流、事件委托、被動監聽),最后用一套可復制的工具函數 + 清單收尾。


1)先理解“為什么會卡”:事件、傳播與主線程

1.1 事件傳播三階段

  • 捕獲(capturing):從 windowdocument → … → target,找目標元素。

  • 目標(at target):到達真正觸發的元素。

  • 冒泡(bubbling):從 target → … → documentwindow 反向冒泡。

常用屬性:

el.addEventListener('click', handler, { capture: false }); // 冒泡階段
// e.target:事件最初觸發的元素
// e.currentTarget:當前正在運行回調的元素
// e.stopPropagation():阻止后續傳播
// e.preventDefault():阻止默認行為(僅在事件可取消時)

1.2 UI 線程瓶頸(滾動與觸摸最敏感)

滾動、觸摸等事件是高頻的:瀏覽器可能在一秒內觸發幾十到上百次回調。如果回調里:

  • 改樣式導致強制同步布局(layout thrashing);

  • 做了重活(復雜計算、DOM 大量操作、同步 XHR);

  • 或者阻塞滾動(在可取消的滾動相關事件里做了 preventDefault),
    就會看到掉幀與卡頓。

核心策略:能少綁就少綁、能延后就延后(防抖/節流/rAF)、能復用就復用(委托)、能不阻塞就不阻塞(passive)。


2)防抖(debounce):過濾“高頻抖動”,保留“最后一次/第一次”

場景:搜索輸入聯想、窗口尺寸變化、表單校驗、復雜篩選。
思想:一段時間內多次觸發→只在最后(或第一次)執行

2.1 可直接用的防抖函數(含 leading / trailing / maxWait)

function debounce(fn, wait = 200, { leading = false, trailing = true, maxWait } = {}) {let timer = null, lastCall = 0, lastInvoke = 0, result;const invoke = (ctx, args) => {lastInvoke = Date.now();result = fn.apply(ctx, args);};const debounced = function (...args) {const now = Date.now();const ctx = this;if (!lastCall && leading && !timer) {invoke(ctx, args);}lastCall = now;const remaining = wait - (now - lastCall);const timeSinceLastInvoke = now - lastInvoke;const shouldInvokeMax = maxWait !== undefined && timeSinceLastInvoke >= maxWait;clearTimeout(timer);timer = setTimeout(() => {timer = null;if (trailing && (!leading || (now - lastInvoke >= wait))) {invoke(ctx, args);}}, remaining > 0 ? remaining : 0);if (shouldInvokeMax) {clearTimeout(timer);timer = null;invoke(ctx, args);}return result;};debounced.cancel = () => { clearTimeout(timer); timer = null; lastCall = 0; };debounced.flush  = () => { if (timer) { clearTimeout(timer); timer = null; invoke(this, []); } };return debounced;
}

用法示例(輸入搜索,尾觸發)

const onQuery = debounce((q) => fetchList(q), 300);
searchInput.addEventListener('input', e => onQuery(e.target.value));

常見坑

  • 同時 leadingtrailingtrue 時,注意一次“觸發周期”內最多執行兩次。

  • 防抖時間太長會造成感知延遲;交互型輸入建議 200–300ms 左右。


3)節流(throttle):控制執行頻率,平滑且可預測

場景scroll / resize / mousemove / pointermove 等連續事件;滾動吸頂、進度計算、拖拽反饋。
思想每隔固定間隔最多執行一次。

3.1 兩種常見實現

  • 時間戳法:更“實時”,首觸發立即執行。

  • 定時器法:更“平滑”,末尾補一次。

3.2 一個實戰可用的節流(支持 leading/trailing/cancel/flush)

function throttle(fn, wait = 100, { leading = true, trailing = true } = {}) {let lastCall = 0, timer = null, lastArgs, lastThis;const invoke = () => {lastCall = Date.now();timer = null;fn.apply(lastThis, lastArgs);lastArgs = lastThis = null;};const throttled = function (...args) {const now = Date.now();lastArgs = args; lastThis = this;if (!lastCall && leading === false) lastCall = now;const remaining = wait - (now - lastCall);if (remaining <= 0 || remaining > wait) {if (timer) { clearTimeout(timer); timer = null; }invoke();} else if (!timer && trailing !== false) {timer = setTimeout(invoke, remaining);}};throttled.cancel = () => { clearTimeout(timer); timer = null; lastCall = 0; lastArgs = lastThis = null; };throttled.flush  = () => { if (timer) { clearTimeout(timer); invoke(); } };return throttled;
}

3.3 rAF 節流(渲染節拍對齊,適合動畫/滾動讀寫)

function rafThrottle(fn) {let ticking = false;return function (...args) {if (ticking) return;ticking = true;requestAnimationFrame(() => {fn.apply(this, args);ticking = false;});};
}
// 示例:滾動時讀一次 scrollTop,寫一次 transform(避免布局抖動)
window.addEventListener('scroll', rafThrottle(() => {const y = window.scrollY || document.documentElement.scrollTop;header.style.transform = `translateY(${Math.min(y, 80)}px)`;
}), { passive: true });

選擇建議

  • 僅渲染相關 → rafThrottle

  • 需要確定的時間頻率 → throttle

  • 僅在停止后處理 → debounce


4)事件委托(Event Delegation):少綁監聽,動態內容更省心

思想:把子元素的監聽“上移”到父容器,在冒泡階段一個回調搞定所有子項。
收益

  • 海量列表只綁一個事件處理器;

  • 動態插入/刪除子節點無需重綁

  • 更易做統一攔截/鑒權/打點

4.1 一個可復用的 onDelegate 工具

function onDelegate(container, type, selector, handler, options) {const listener = (e) => {// 使用 closest 適配嵌套:匹配到最近的祖先const target = e.target.closest(selector);if (target && container.contains(target)) {handler.call(target, e, target); // this 指向匹配元素}};container.addEventListener(type, listener, options);return () => container.removeEventListener(type, listener, options); // 便于解綁
}

示例:列表點擊/鍵盤交互

const off = onDelegate(document.querySelector('#todo'), 'click', 'button.remove', (e, btn) => {const li = btn.closest('li');li?.remove();
});// 動態新增 li 無需額外綁定

4.2 委托的注意點

  • 不是所有事件都冒泡:focus/blur 不冒泡(可用 focusin/focusout),mouseenter/leave 不冒泡(用 mouseover/out + relatedTarget)。

  • e.stopPropagation() 會截斷冒泡,盡量在局部回調里少用或控制邊界。

  • Shadow DOM 下要理解 composed path,委托到 shadow root 外需要事件是 composed: true 的。


5)Passive Event Listeners:不阻塞滾動的監聽

在觸發滾動相關事件(如 touchstart/touchmove/wheel)時,瀏覽器需要知道你的監聽器會不會 preventDefault() 來阻止滾動。如果不確定,瀏覽器可能等待你的回調,從而產生卡頓。

被動監聽passive: true)告訴瀏覽器:我不會調用 preventDefault()。這樣瀏覽器可以立刻滾動,顯著改善滾動流暢度。

// 正確:滾動/觸摸相關事件一般用 passive
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('touchmove', onTouchMove, { passive: true });
window.addEventListener('wheel', onWheel, { passive: true });

警告:被動監聽里調用 e.preventDefault() 會被忽略(并可能在控制臺收到提示)。
如果你必須阻止默認行為(例如自定義手勢),就不要把這個監聽設為 passive,或采用雙通道策略(僅在需要時單獨注冊非被動監聽)。

附:一次性、捕獲階段

el.addEventListener('click', onceHandler, { once: true });
el.addEventListener('click', capHandler,  { capture: true });

6)組合拳:一個綜合示例(滾動 + 搜索 + 列表)

<header id="header">Header</header>
<input id="search" placeholder="輸入關鍵詞..."/>
<ul id="list"></ul>
// 1) 滾動:rAF 節流 + passive
const header = document.getElementById('header');
window.addEventListener('scroll', rafThrottle(() => {const y = window.scrollY || document.documentElement.scrollTop;header.style.opacity = Math.max(0, 1 - y / 300);
}), { passive: true });// 2) 輸入:防抖
const search = document.getElementById('search');
const query = debounce(async (kw) => {const data = await fetch(`/api/search?q=${encodeURIComponent(kw)}`).then(r => r.json());renderList(data);
}, 300);
search.addEventListener('input', e => query(e.target.value));// 3) 列表:事件委托(刪除 & 點贊)
const list = document.getElementById('list');
function renderList(items = []) {list.innerHTML = items.map(it => `<li data-id="${it.id}"><span class="title">${it.title}</span><button class="like">👍 ${it.likes}</button><button class="remove">刪除</button></li>`).join('');
}
const offRemove = onDelegate(list, 'click', 'button.remove', (e, btn) => {btn.closest('li')?.remove();
});
const offLike = onDelegate(list, 'click', 'button.like', (e, btn) => {const n = parseInt(btn.textContent.replace(/\D/g,'')) || 0;btn.textContent = `👍 ${n + 1}`;
});

7)性能與可維護性補充

  • 讀寫分離:在同一個幀里,先讀所有布局值,再寫樣式,避免反復讀寫導致強制回流。

  • 減少監聽數量:能委托就委托;不要給每個子項都綁監聽。

  • 用觀測 API 替代輪詢/滾動監聽

    • 元素進入視口:IntersectionObserver

    • 元素尺寸變化:ResizeObserver

  • Pointer Events:用 pointer* 合并鼠標與觸摸邏輯,代碼更少;配合 getCoalescedEvents() 獲得更平滑的指針軌跡。

  • 易清理的監聽:使用 AbortController 一鍵解綁:

    const ac = new AbortController();
    window.addEventListener('scroll', onScroll, { passive: true, signal: ac.signal });
    // 需要時
    ac.abort(); // 自動移除所有注冊在該 signal 上的監聽
    

8)不同事件的“推薦組合”速查

事件建議說明
scrollpassive: true + rafThrottle滾動讀/寫渲染屬性時對齊幀率
touchstart/movepassive: true(若不阻止默認)需要自定義手勢且要 preventDefault 時改為非 passive
wheelpassive: true(不阻止默認)要自定義滾動邏輯時禁用 passive
resizethrottle 100–200ms計算布局較多時適度加大間隔
inputdebounce 200–300ms搜索聯想等
mousemovethrottlerafThrottle拖拽/繪圖更推薦 rAF
列表 item 點擊事件委托動態增刪子項最省心
focus/blurfocusin/focusout 代替做委托這兩個才冒泡

9)常見坑與對策

  1. 在 passive 監聽里調用 preventDefault
    → 無效且會有警告。確認是否真的需要阻止默認;需要時把該監聽改為非 passive,僅作用于需要阻止的場景。

  2. 委托 + stopPropagation 沖突
    → 下層組件阻斷冒泡,會讓上層委托失效。團隊約定:組件層盡量少用 stopPropagation,或在容器委托前移到捕獲階段

  3. 高頻事件里讀寫混雜
    → 先讀后寫,或將寫入放 requestAnimationFrame,把讀操作緩存到局部變量。

  4. 誤用 mouseenter/leave 做委托
    → 它們不冒泡。改用 mouseover/out + 判斷 relatedTarget,或把監聽直接綁到目標元素。

  5. 匿名函數難以解綁
    → 封裝返回 off() 的注冊函數,或使用 AbortController 統一收束。


10)可復制的“最小工具集”

// 1) debounce(上文已給全量版,可直接復用)
// 2) throttle(上文已給全量版)
// 3) rAF 節流
function rafThrottle(fn) {let ticking = false;return function (...args) {if (ticking) return;ticking = true;requestAnimationFrame(() => { fn.apply(this, args); ticking = false; });};
}
// 4) 事件委托
function onDelegate(container, type, selector, handler, options) {const listener = (e) => {const target = e.target.closest(selector);if (target && container.contains(target)) handler.call(target, e, target);};container.addEventListener(type, listener, options);return () => container.removeEventListener(type, listener, options);
}
// 5) 安全注冊(帶 AbortController)
function on(el, type, handler, { passive, capture, once, signal } = {}) {el.addEventListener(type, handler, { passive, capture, once, signal });return () => el.removeEventListener(type, handler, { capture });
}

結語

  • 先理解事件傳播與主線程瓶頸,再對癥下藥:

    • 高頻 → 節流/防抖/rAF

    • 海量節點 → 事件委托

    • 滾動/觸摸 → passive 默認化。

  • 用一套可復用的工具函數和小清單,把“性能與流暢”變成默認選項,而不是事故后的補救。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/96529.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/96529.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/96529.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【Chrome】chrome 調試工具的network選項卡,如何同時過濾出doc js css

通過類型按鈕快速篩選&#xff08;更直觀&#xff09;在 Network 選項卡中&#xff0c;找到頂部的 資源類型按鈕欄&#xff08;通常在過濾器搜索框下方&#xff09;。按住 Ctrl 鍵&#xff08;Windows/Linux&#xff09;或 Command 鍵&#xff08;Mac&#xff09;&#xff0c;同…

Elasticsearch (ES)相關

在ES中&#xff0c;已經有Term Index&#xff0c;那還會走倒排索引嗎 你這個問題問得很到位 &#x1f44d;。我們分清楚 Term Index 和 倒排索引 在 Elasticsearch (ES) 里的關系&#xff1a;1. 倒排索引&#xff08;Inverted Index&#xff09; 是 Lucene/ES 檢索的核心。文檔…

pre-commit run --all-files 報錯:http.client.RemoteDisconnected

報錯完整信息初步原因是這樣 報錯是 Python 的 http.client.RemoteDisconnected&#xff0c;意思是 在用 urllib 請求遠程 URL 時&#xff0c;遠程服務器直接斷開了連接&#xff0c;沒有返回任何響應。在你的堆棧里&#xff0c;它出現在 pre-commit 嘗試安裝 Golang 環境的時候…

【C++】STL·List

1. list的介紹及使用 1.1list介紹 List文檔介紹 1.2 list的使用 list中的接口比較多&#xff0c;此處類似&#xff0c;只需要掌握如何正確的使用&#xff0c;然后再去深入研究背后的原理&#xff0c;已 達到可擴展的能力。以下為list中一些常見的重要接口。 1.2.1 list的構造…

圖論2 圖的數據結構表示

目錄 一 圖的數據結構表示 1 鄰接矩陣&#xff08;Adjacency Matrix&#xff09; 2 鄰接表&#xff08;Adjacency List&#xff09; 3 邊列表&#xff08;Edge List&#xff09; 4 十字鏈表&#xff08;Orthogonal List / Cross-linked List, 十字鏈表&#xff09; 5 鄰接…

在Excel中刪除大量間隔空白行

在 Excel 中刪除大量間隔空白行&#xff0c;可使用定位空值功能來快速實現。以下是具體方法&#xff1a;首先&#xff0c;選中包含空白行的數據區域。可以通過點擊數據區域的左上角單元格&#xff0c;然后按住鼠標左鍵拖動到右下角最后一個單元格來實現。接著&#xff0c;按下快…

【C 學習】10-循環結構

“知道做不到就是不知道”一、條件循環1. while只要條件為真&#xff08;true&#xff09;&#xff0c;就會重復執行循環體內的代碼。while (條件) {// 循環體&#xff08;要重復執行的代碼&#xff09; }//示例 int i 1; while (i < 5) {printf("%d\n", i);i; …

音視頻的下一站:協議編排、低時延工程與國標移動化接入的系統實踐

一、引言&#xff1a;音視頻的基礎設施化 過去十年&#xff0c;音視頻的兩條主線清晰可辨&#xff1a; 娛樂驅動&#xff1a;直播、電商、短視頻把“實時觀看與互動”變成高頻日常。 行業擴展&#xff1a;教育、會議、安防、政務逐步把“可用、可管、可控”引入產業系統。 …

SAM-Med3D:面向三維醫療體數據的通用分割模型(文獻精讀)

1) 深入剖析:核心方法與圖示(Figure)逐一對應 1.1 單點三維提示的任務設定(Figure 1) 論文首先將3D交互式分割的提示形式從“2D逐片(每片1點,共N點)”切換為“體素級單點(1個3D點)”。Figure 1直觀對比了 SAM(2D)/SAM-Med2D 與 SAM-Med3D(1點/體) 的差異:前兩者…

【Spring】原理解析:Spring Boot 自動配置進階探索與優化策略

一、引言在上一篇文章中&#xff0c;我們對 Spring Boot 自動配置的基本原理和核心機制進行了詳細的分析。本文將進一步深入探索 Spring Boot 自動配置的高級特性&#xff0c;包括如何進行自定義擴展、優化自動配置的性能&#xff0c;以及在實際項目中的應用優化策略。同時&…

OpenCV:圖像直方圖

目錄 一、什么是圖像直方圖&#xff1f; 關鍵概念&#xff1a;BINS&#xff08;區間&#xff09; 二、直方圖的核心作用 三、OpenCV 計算直方圖&#xff1a;calcHist 函數詳解 1. 函數語法與參數解析 2. 基礎實戰&#xff1a;計算灰度圖直方圖 代碼實現 結果分析 3. 進…

docke筆記下篇

本地鏡像發布到阿里云 本地鏡像發布到阿里云流程 鏡像的生成方法 基于當前容器創建一個新的鏡像&#xff0c;新功能增強 docker commit [OPTIONS] 容器ID [REPOSITORY[:TAG]] OPTIONS說明&#xff1a; OPTIONS說明&#xff1a; -a :提交的鏡像作者&#xff1b; -m :提交時的說…

《大數據之路1》筆記2:數據模型

一 數據建模綜述 1.1 為什么要數據建模背景&#xff1a; 隨著DT時代的來臨&#xff0c;數據爆發式增長&#xff0c;如何對數據有序&#xff0c;有結構地分類組織額存儲是關鍵定義&#xff1a; 數據模型時數據組織和存儲的方法&#xff0c;強調從業務、數據存取、使用角度 合理存…

“量子能量泵”:一種基于并聯電池與電容陣的動態直接升壓架構

“量子能量泵”&#xff1a;一種基于并聯電池與電容陣的動態直接升壓架構摘要&#xff1a;本文揭示了一種革命性的高效電源解決方案&#xff0c;旨在徹底解決低電壓、大功率應用中的升壓效率瓶頸與電池一致性難題。該方案摒棄傳統磁性升壓拓撲&#xff0c;創新性地采用并聯電池…

DeepSeek實戰--自定義工具

1. 背景 當前已經有很多AI基礎平臺&#xff08;比如&#xff1a;扣子、Dify&#xff09;&#xff0c;用戶可以快速搭建Agent&#xff0c;那怎樣將已有的接口能力給大模型調用呢 &#xff1f; 今天我們來探索一個&#xff0c;非常高效、快捷的方案&#xff1a;將http接口做成Dif…

“移動零”思路與題解

給定一個數組 nums&#xff0c;編寫一個函數將所有 0 移動到數組的末尾&#xff0c;同時保持非零元素的相對順序。請注意 &#xff0c;必須在不復制數組的情況下原地對數組進行操作。思路講解&#xff1a;舉例如下&#xff1a;實現代碼是&#xff1a;class Solution { public:v…

關于行內元素,行內塊元素和塊級元素

1、什么是行內元素&#xff0c;什么是行內塊元素&#xff0c;什么是塊級元素行內元素的特點&#xff1a;不獨占一行&#xff0c;相鄰元素會在同一行顯示&#xff0c;直到一行排不下才換行。寬度和高度由內容本身決定&#xff0c;無法通過width&#xff0c;height手動設置&#…

?絡請求Axios的概念和作用

Axios 是一個基于 ??Promise?? 的輕量級、高性能 ??HTTP 客戶端庫??&#xff0c;主要用于在瀏覽器和 Node.js 環境中發起 HTTP 請求&#xff08;如 GET、POST、PUT、DELETE 等&#xff09;。它通過簡潔的 API 和強大的功能&#xff0c;簡化了前端與后端之間的數據交互過…

在AgentScope中實現結構化輸出

在AgentScope中實現結構化輸出 概述 在AgentScope框架中&#xff0c;結構化輸出功能允許開發者定義明確的輸出模式&#xff0c;確保AI模型的響應符合預期的格式和約束。本教程將介紹如何使用AgentScope的structured_model參數來實現結構化輸出。 結構化輸出的優勢 數據一致性&a…

Linux 磁盤I/O高占用進程排查指南:從定位到分析的完整流程

在Linux服務器運維工作中&#xff0c;磁盤I/O瓶頸是導致系統性能下降的常見原因之一。當服務器出現響應緩慢、應用卡頓等問題時&#xff0c;及時定位并解決高I/O占用進程就顯得尤為重要。本文將從核心思路出發&#xff0c;通過“確認問題-定位磁盤-鎖定進程-深入分析”四個步驟…