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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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;通过“确认问题-定位磁盘-锁定进程-深入分析”四个步骤…