运行时
1、 事件循环
核心概率:
- 执行栈
- 任务队列(同步任务、微任务、宏任务)
- 事件循环

先看一段代码:
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log('1a'));
console.log('1b');
});
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log('2a'));
console.log('2b');
});
思考一下,当鼠标点击 btn 元素时,控制台会打印什么?
1.1、 DOM事类型
首先分析 点击事件,是在JS的事件循环中,属于宏任务的。
既然是宏任务,那么上面的代码就可以等价为:
setTimeout(() => {
Promise.resolve().then(() => console.log('1a'));
console.log('1b');
});
setTimeout(() => {
Promise.resolve().then(() => console.log('2a'));
console.log('2b');
});
// 输出
// 1b
// 1a
// 2b
// 2a
1.2、 dispatchEvent
将代码稍微调整,看如下代码:
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log('1a'));
console.log('1b');
});
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log('2a'));
console.log('2b');
});
btn.click();
分析:
btn.click() 属于 dispatchEvent 调用事件,是同步执行的。
参考文档:https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
The dispatchEvent() method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order. The normal event processing rules (including the capturing and optional bubbling phase) also apply to events dispatched manually with dispatchEvent().
所以上面的代码,等价为:
function click1() {
Promise.resolve().then(() => console.log('1a'));
console.log('1b');
}
function click2() {
Promise.resolve().then(() => console.log('2a'));
console.log('2b');
}
click1();
click2();
// 输出
// 1b
// 2b
// 1a
// 2a
1.3、 任务类型划分
宏任务包括:
脚本setTimeoutsetIntervalsetImmediaterequestAnimationFrame
微任务的:
process.nextTick(Nodejs)PromiseObject.observeMutationObserver
1.4、 面试真题
setTimeout(function () {
console.log("1");
}, 0);
async function async1() {
console.log("2");
const data = await async2();
console.log("3");
return data;
}
async function async2() {
return new Promise((resolve) => {
console.log("4");
resolve("async2的结果");
}).then((data) => {
console.log("5");
return data;
});
}
async1().then((data) => {
console.log("6");
console.log(data);
});
new Promise(function (resolve) {
console.log("7");
// resolve()
}).then(function () {
console.log("8");
});
2、 script 标签的 async 和 defer
共同点:
- 异步加载:不会阻塞 HTML 解析和渲染。
- 仅适用于外部脚本(src 属性有值)。
区别:
| 属性 | 执行顺序 | 执行时机 | 适用场景 |
|---|---|---|---|
| 没有 async 或 defer | 按照在 HTML 中出现的顺序 | 阻塞 HTML 解析和渲染 | 无特殊要求的脚本 |
| async | 无法保证 | 下载完成后立即执行,可能在 DOMContentLoaded 事件之前 | 不依赖其他脚本的独立脚本,例如统计代码、广告代码 |
| defer | 按照在 HTML 中出现的顺序 | DOMContentLoaded 事件之前 | 需要完整 DOM 结构的脚本,例如操作 DOM 的脚本 |
适用场景
- async 适用于不依赖其他脚本、需要尽快执行的脚本,例如统计代码、广告代码等。
- defer 适用于依赖于页面 DOM、需要按照顺序执行的脚本,例如页面初始化代码、库文件等。