异步与事件
约 2196 字大约 7 分钟
2025-11-24
不会冒泡的事件
问题
不会冒泡的事件有哪些?
在 JavaScript 中,大多数事件都会按照 DMO 事件流模型冒泡,即事件会从目标元素开始向上冒泡到父元素。但也有一些事件直接在目标元素上触发,不会向上传播。
| 事件 | 简单描述 |
|---|---|
focus | 元素获得焦点时触发(如点击输入框),不冒泡 |
focusin | 与 focus 类似,但会在父元素上触发,冒泡 |
focusout | 与 blur 类似,但会在父元素上触发,冒泡 |
blur | 元素失去焦点时触发(如点击别处),不冒泡 |
mouseenter | 鼠标首次进入元素边界时触发,不冒泡,且不因进入子元素重复触发 |
mouseleave | 鼠标完全离开元素边界时触发,不冒泡,与 mouseenter 成对 |
load | 资源(如 <img>、<script> 或 window)加载完成时触发,不冒泡 |
unload | 页面或资源即将卸载(如关闭页面)时触发,不冒泡 |
mouseenter 与 mouseover
问题
mouseenter 与 mouseover 有什么区别?
| 特性 | mouseenter | mouseover |
|---|---|---|
| 是否冒泡 | ❌ 不冒泡 | ✅ 会冒泡 |
| 进入子元素时是否再次触发 | ❌ 不会 | ✅ 会(因为事件冒泡) |
| 触发时机 | 仅当鼠标首次进入目标元素整体区域 | 鼠标进入目标元素 或其任何子元素 时都会触发 |
| 常用于 | 需要“整体悬停”逻辑(如 Tooltip、下拉菜单) | 一般悬停效果(但需注意子元素干扰) |
示例说明
<div id="outer" style="padding: 20px; background: lightblue;">
外层
<div id="inner" style="padding: 20px; background: pink;">
内层
</div>
</div>const outer = document.getElementById('outer');
outer.addEventListener('mouseenter', () => {
console.log('mouseenter');
});
outer.addEventListener('mouseover', () => {
console.log('mouseover');
});操作 & 输出:
鼠标从外层空白处进入
#outer:mouseenter→ 1 次mouseover→ 1 次
鼠标从
#outer移动到#inner(子元素):mouseenter→ 无输出(因为仍在#outer区域内)mouseover→ 再次输出!(因为#inner触发了mouseover,并冒泡到#outer)
鼠标继续从
#inner移动到#outer:mouseenter→ 无输出(因为仍在#outer区域内)mouseover→ 再次输出!(因为#outer触发了mouseover)
这就是为什么 mouseover 容易“误触发”,而 mouseenter 更符合“整体悬停”的直觉。
对应的离开事件
| 进入事件 | 离开事件 |
|---|---|
mouseenter | mouseleave(不冒泡) |
mouseover | mouseout(会冒泡) |
同样,mouseleave 只在鼠标完全离开整个元素区域时触发,而 mouseout 在离开子元素时也会触发。
async 与 await 的实现原理
问题
async 与 await 的实现原理
async/await 是基于 Promise 的语法糖。async 函数会自动返回一个 Promise,而 await 会暂停函数执行,直到等待的 Promise 完成。 在引擎层面,现代 JavaScript 引擎(如 V8)将 async 函数编译为状态机,每个 await 对应一个状态,Promise 完成后通过微任务恢复执行。 这使得异步代码可以像同步一样编写,同时保持非阻塞特性。
TODO: async 与 await 的实现原理
节流与防抖
问题
什么是节流与防抖?
节流(Throttle)和防抖(Debounce)是前端开发中用于优化高频率触发事件处理的两种技术,常用于处理如窗口缩放、滚动、输入框输入等高频事件。
节流(Throttle)
节流是指在一定时间内,只执行一次函数。也就是说,在规定的时间间隔内,无论触发多少次事件,函数只会执行一次。就像水龙头的节流阀,控制水的流速,在一定时间内只放出一定量的水。
应用场景
- 窗口缩放(
resize事件):当用户调整浏览器窗口大小时,resize事件会频繁触发。如果在这个事件处理函数中进行一些复杂的布局计算或重新渲染操作,会导致性能问题。使用节流可以限制在一定时间内只执行一次布局计算,减轻浏览器负担。 - 滚动加载(
scroll事件):在网页滚动加载更多内容的场景中,scroll事件会随着用户滚动页面不断触发。通过节流可以控制在一定时间内只进行一次数据加载请求,避免频繁请求导致服务器压力过大。
实现方式
function throttle(func, delay) {
let timer = null;
return function() {
if (!timer) {
func.apply(this, arguments);
timer = setTimeout(() => {
timer = null;
}, delay);
}
};
}
// 使用示例
function handleScroll() {
console.log('Scroll event triggered');
}
window.addEventListener('scroll', throttle(handleScroll, 500));防抖(Debounce)
概念
防抖是指在一定时间内,只有最后一次触发事件才会执行函数。如果在这个时间间隔内再次触发事件,则重新计时。就像电梯门,当有人不断进出时,电梯门不会关闭,直到一段时间内没有人再触发开门或关门操作,电梯门才会关闭。
应用场景
- 输入框搜索提示:在搜索框中输入关键词时,每次输入都会触发
input事件。如果实时根据输入内容进行搜索请求,会产生大量不必要的请求。使用防抖可以在用户停止输入一段时间后,才进行搜索请求,减少请求次数。 - 按钮点击防止重复提交:当用户快速多次点击提交按钮时,使用防抖可以确保只有最后一次点击才会触发提交操作,避免重复提交数据。
实现方式
function debounce(func, delay) {
let timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
// 使用示例
function handleInput() {
console.log('Input event triggered');
}
const input = document.querySelector('input');
input.addEventListener('input', debounce(handleInput, 300));总结
- 节流:适用于需要限制函数执行频率的场景,保证在一定时间内函数只执行一次,常用于处理高频触发的事件,如窗口缩放、滚动等。
- 防抖:适用于需要等待用户操作结束后再执行函数的场景,避免因用户频繁操作而导致函数多次执行,常用于输入框搜索提示、按钮点击等场景。
竞态问题
问题
什么是竞态问题?
如何解决竞态问题?
竞态问题 是指多个异步操作的执行顺序不确定,导致最终结果依赖于它们完成的相对时间,从而产生非预期行为。
常见场景
- 快速输入内容,后发起的请求先返回,覆盖了先发起但后返回的正确结果。
- 多次点击按钮触发多个相同请求。
示例
<input autocomplete="off" type="text" id="query" placeholder="输入搜索词..." />
<div id="result"></div>// 模拟不确定请求:延迟 0.5 ~ 2 秒
function mockRequest(query) {
const delay = 500 + Math.random() * 1500;
return new Promise((resolve) => {
setTimeout(() => {
resolve(`显示结果: "${query}" (实际耗时 ${delay.toFixed(0)}ms)`);
}, delay);
});
}
const input = document.getElementById("query");
const resultEl = document.getElementById("result");
input.addEventListener("input", async (e) => {
const q = e.target.value.trim();
if (!q) {
resultEl.textContent = "";
return;
}
resultEl.textContent = `请求已发出: "${q}"...`;
// 没有取消机制:每个输入都发请求,且都会更新 UI
const result = await mockRequest(q);
resultEl.textContent = result; // 可能被“旧但晚到”的请求覆盖
});/* css 代码 */在上面的例子当中,我们使用 mockRequest 模拟一个耗时的异步请求,请求时间 0.5 秒到 2 秒之间,每输入一个搜索词,会触发一个请求。
可以发现,当用户输入多个搜索词时,多个请求会并发发出,实际结果会显示耗时最短的。
解决方案
在 JS 当中,可以使用 AbortController 来取消过期的请求。
<input autocomplete="off" type="text" id="query" placeholder="输入搜索词..." />
<div id="result"></div>// 模拟不确定请求:延迟 0.5 ~ 2 秒
function mockRequest(query, signal) {
const delay = 500 + Math.random() * 1500;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
if (signal?.aborted) {
clearTimeout(timer);
reject(new DOMException("Aborted", "AbortError"));
} else {
resolve(`结果: "${query}" (耗时 ${delay.toFixed(0)}ms)`);
}
}, delay);
});
}
const input = document.getElementById("query");
const resultEl = document.getElementById("result");
let abortController = null;
input.addEventListener("input", async (e) => {
const q = e.target.value.trim();
resultEl.textContent = "加载中...";
// 取消上一个请求
if (abortController) abortController.abort();
abortController = new AbortController();
try {
const res = await mockRequest(q, abortController.signal);
resultEl.textContent = res;
} catch (err) {
if (err.name !== "AbortError") {
resultEl.textContent = "请求出错";
console.error(err);
}
// 若是 AbortError,说明是过期请求,静默丢弃
}
});/* css 代码 */示例中,我们使用了 AbortController 来取消过期的请求。请求发起之前,先取消上一次的请求,然后创建一个新的 AbortController,并绑定到当前请求。这样,当用户输入新的搜索词时,上一个请求将被取消,新的请求将开始执行,最终显示结果将总是最后一次请求结果。
在 Vue3 中,使用 watch 或者 watchEffect 来触发请求也需要注意竞态问题,可以使用 onCleanup() 忽略过时请求。在 React 中,可以在 Effect 中返回一个清理函数,来处理竞态问题。