第3天:JavaScript核心(二)——事件循环、Promise、async/await 与防抖节流
本文系统梳理了JavaScript异步编程核心概念,重点解析了事件循环机制(同步→微任务→宏任务的执行顺序)、Promise用法(then/catch/finally)及async/await原理,并对比了宏任务(setTimeout等)与微任务(Promise.then等)的区别。同时提供了Promise.all的手写实现,以及防抖(延迟触发)和节流(固定频率)两种性能优化方案的具体代码实现。这
今天继续 JavaScript 核心复习,重点放在事件循环、宏任务/微任务以及 Promise/async/await 上,最后还整理了防抖节流的手写实现。这些概念是面试中的高频考点,也是理解异步编程和性能优化的关键。
以下是当天的问答整理。
Q1:事件循环(Event Loop)是什么?请简述执行顺序。
我的回答:
- 先执行所有同步代码 → 执行所有微任务(比如接口调用、数据请求) → 渲染更新页面 → 执行下一个宏任务(比如 DOM 点击事件、setTimeout)。
- JS 代码是单线程,异步操作是由浏览器(或 Node.js)在幕后进行的,当同步代码执行完后,再把异步操作的结果回调到主线程执行。
补充:
- “接口调用、数据请求”本身不是微任务(同步代码),它们的回调(如 fetch().then() 里的 .then)才是微任务。setTimeout 的回调是宏任务。
- 微任务可以“插队”。
Q2:常见的宏任务和微任务有哪些?如果任务中产生新的微任务,会怎么处理?
我的回答:
- 常见的宏任务:DOM 点击事件、setTimeout()。
- 常见的微任务:promise.then()、async/await 等。
- 如果宏任务中产生新的微任务,那么先会把微任务清空再执行下一个宏任务。
补充:
- 宏任务(MacroTask) 还包括:setInterval、I/O(文件读写、网络请求完成后的回调)、requestAnimationFrame(严格说它属于渲染前执行,但常被归为宏任务)、postMessage、MessageChannel 等。
- 微任务(MicroTask) 还包括:queueMicrotask、MutationObserver、process.nextTick(Node.js 环境)。
- 在当前宏任务执行过程中,如果产生了新的微任务(比如在 Promise.then 中再注册一个 then),这些新微任务会追加到微任务队列末尾,并在当前宏任务结束前、下一个宏任务开始前被清空。也就是说,微任务可以连续嵌套,但必须清空整个队列才会执行下一个宏任务。
Q3:Promise 的基本用法是什么?then、catch、finally 的作用?
我的回答:
- 具体概念相对理解较差,then 可以请求结果,catch 是错误处理。
补充:
- Promise 是一个对象,代表一个异步操作的最终完成(或失败)及其结果值。
- 三种状态:pending(进行中)fulfilled(已成功)rejected(已失败)
- then 接收两个可选参数:onFulfilled 和 onRejected,分别处理成功和失败。它返回一个新的 Promise,支持链式调用。
- catch 是 .then(null, onRejected) 的语法糖,专门处理错误。
- finally 无论成功或失败都会执行,通常用于清理操作(如关闭加载动画),不接收参数。
const p = new Promise((resolve, reject) => { // 异步操作 if (成功) resolve(value); else reject(error); }); p.then(value => { /* 处理成功 */ }) .catch(error => { /* 处理失败 */ }) .finally(() => { /* 清理 */ });
Q4:async/await 的原理和使用?请结合代码解释。
我的回答:
- async 返回一个 Promise 对象。
- await 代码旨在 Promise 成功之后执行。
async function fetchData() { try { const res = await fetch('/api/data'); const data = res.data; } catch (err) { console.error(err); } }
补充:
- async 函数总是返回一个 Promise。如果返回值不是 Promise,会被 Promise.resolve 包装成Promise状态。
- await 只能用在 async 函数内部(顶级 await 已在部分环境支持)。它会暂停当前 async 函数的执行,等待 Promise 完成,然后继续执行后面的代码。这个“暂停”是非阻塞的,不会阻塞事件循环。
// 测试:await 不阻塞定时器(宏任务) async function test() { console.log('test 开始'); await new Promise(resolve => setTimeout(resolve, 1000)); console.log('test 结束'); } test(); // 定时器会正常执行,说明事件循环没被阻塞 setTimeout(() => { console.log('定时器执行'); }, 500); // 输出顺序: // test 开始 // 定时器执行(500ms 后,test 还在暂停) // test 结束(1000ms 后) - await 后面通常跟一个 Promise,如果不是 Promise,则会被转换为立即 resolved 的 Promise。
- 错误处理:推荐用 try/catch 捕获 await 的 reject,也可以在 async 函数后面链式调用 .catch()。
- async/await 是 Promise 的语法糖,使异步代码看起来像同步,更易读。
Q5:看代码写输出(事件循环综合题)
题目:
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3);
});
}, 0);
Promise.resolve().then(() => {
console.log(4);
setTimeout(() => {
console.log(5);
}, 0);
});
console.log(6);
我的回答:顺序是 1, 6, 4, 2, 3, 5。
解析:
-
同步代码:输出
1,然后setTimeout回调(宏任务)被放入宏任务队列,Promise.then(微任务)被放入微任务队列,最后输出6。此时输出顺序:1, 6。 -
当前宏任务结束,清空微任务队列:微任务中有第一个 Promise.then(输出 4),执行它,输出 4,并且它内部又有一个 setTimeout(宏任务)被加入宏任务队列。此时微任务队列清空,输出 4。
-
取出第一个宏任务:第一个
setTimeout回调执行,输出2,内部有Promise.resolve().then(微任务),加入微任务队列。此时输出2。 -
再次清空微任务:微任务队列中的 then 执行,输出 3。
-
取出第二个宏任务:第二个 setTimeout 回调执行,输出 5。
手写题1:实现 Promise.all
function myPromiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'));
}
const results = [];
let completed = 0;
const total = promises.length;
if (total === 0) {
return resolve(results);
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
value => {
results[index] = value;
completed++;
if (completed === total) {
resolve(results);
}
},
reason => {
reject(reason);
}
);
});
});
}
// 测试
const p1 = Promise.resolve(1);
const p2 = new Promise(resolve => setTimeout(() => resolve(2), 100));
const p3 = 3;
myPromiseAll([p1, p2, p3]).then(console.log); // 约100ms后输出 [1, 2, 3]
要点:
- 返回一个新的 Promise。
- 用 Promise.resolve 统一处理非 Promise 项。
- 保持结果顺序与输入一致(通过 results[index] 赋值)。
- 任何一个失败立即 reject(Promise 的错误会向上冒泡)。
手写题 2:防抖与节流
防抖和节流是前端性能优化的常用手段,面试中也常考手写。
防抖(Debounce)
作用:在事件被触发 delay 毫秒后执行回调,如果在这段时间内再次触发,则重新计时。适用于输入框搜索、窗口 resize ,支付等场景。
我的代码:
function debounce(func, delay) {
let time;
return function (...arg) {
clearTimeout(time);
time = setTimeout(() => {
//等价于 handleClick.apply(this, [e])
func.apply(this, arg);
}, delay);
};
}
解释:
- 返回一个闭包函数,防止每次还没操作就调用,每次事件触发时清除之前的定时器,重新设置。
- 使用 apply 确保原函数的 this 和参数正确传递。
- 最后一次触发后的 delay 毫秒内没有新触发,则执行原函数。
节流(Throttle)
作用:保证在 delay 毫秒内最多执行一次回调。适用于滚动、鼠标移动等高频事件。
我的代码:
function throttle(func, delay) {
let beginTime = 0;
return function (...arg) {
let cur = new Date().getTime();
if (cur - beginTime > delay) {
func.apply(this, arg);
beginTime = cur;
}
};
}
解释:
- 记录上次执行时间 beginTime。
- 每次触发判断当前时间与上次执行时间差是否大于 delay,是则执行并更新 beginTime。
- 第一次触发会立即执行(因为 beginTime 为 0)。
使用示例
const button = document.getElementById('input');
const play = () => console.log('已点击');
// 防抖:连续点击只会在最后一次点击后 1 秒执行
button.addEventListener('click', debounce(play, 1000));
// 节流:每秒最多执行一次
button.addEventListener('click', throttle(play, 1000));
补充:节流还有定时器版,可以保证最后一次触发也执行(但可能会延迟),时间戳版适合需要立即响应的场景。
今日知识点总结
| 概念 | 要点 |
|---|---|
| 事件循环 | 同步代码 → 清空微任务 → 取一个宏任务执行 → 循环 |
| 宏任务 | setTimeout、setInterval、I/O、UI 事件、requestAnimationFrame |
| 微任务 | Promise.then/catch/finally、queueMicrotask、MutationObserver |
| Promise | 三种状态(pending/fulfilled/rejected),链式调用,错误传递 |
| async/await | async 函数返回 Promise,await 暂停执行直到 Promise 完成 |
| Promise.all | 所有成功则返回结果数组,任一失败则立即 reject |
| 防抖 | 延迟执行,期间重新计时 |
| 节流 | 固定频率执行,保证间隔 |
更多推荐

所有评论(0)