JS异步解决方案的发展历程以及优缺点 ⭐️⭐️⭐️
JavaScript 异步编程的解决方案经历了多个阶段的演进,从最初的回调函数到现代的 async/await,每个阶段都试图解决前一代方案的痛点。以下是主要发展历程:
1. 回调函数(Callbacks)
- 时期:早期 JavaScript(ES5 及之前)
- 原理:
通过将函数作为参数传递,在异步操作完成后调用该函数。 - 示例:
setTimeout(() => { console.log("执行完成"); }, 1000); - 优点:简单、直接。
解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。) - 缺点:
- 回调地狱(Callback Hell):多层嵌套导致代码难以维护,不能 return
- 错误处理困难:
需手动传递错误,容易遗漏。不能用 try catch 捕获错误 - 控制流复杂:并行/串行任务需要自行管理。
2. Promise(ES6,2015 年)
- 原理:用对象表示异步操作的最终完成或失败,支持链式调用。
- 示例:
fetchData() .then(data => processData(data)) .then(result => console.log(result)) .catch(error => console.error(error)); - 优点:
链式调用解决回调嵌套。- 统一的错误处理(
.catch())。
- 缺点:
- 仍需要写
.then(),代码结构不够直观。 - 无法取消 Promise。
- 仍需要写
3. Generator + yield(ES6,2015 年)
- 原理:通过
function*和yield暂停函数执行,结合执行器(如co库) 实现异步流程控制。 - 示例:
const co = require('co'); function* asyncTask() { const data = yield fetchData(); const result = yield processData(data); return result; } co(asyncTask).then(result => console.log(result)); - 优点:
- 用同步写法处理异步。
支持暂停和恢复执行。
- 缺点:
- 依赖外部执行器(如
co库)。 - 语法复杂,不够直观。
- 依赖外部执行器(如
4. Async/Await(ES2017,ES8)
- 原理:基于 Promise 和 Generator 的语法糖,用
async和await以同步方式编写异步代码。 - 示例:
async function main() { try { const data = await fetchData(); const result = await processData(data); console.log(result); } catch (error) { console.error(error); } } main(); - 优点:
- 代码简洁,接近同步代码结构。
- 错误处理通过
try/catch实现。 - 兼容 Promise,可混合使用。
- 缺点:
- 需理解 Promise 机制。
- 滥用
await可能导致不必要的串行执行。
其他补充方案
- 事件监听(Event Emitters):如 Node.js 的
EventEmitter,适用于事件驱动的场景。 - RxJS(Observables):响应式编程库,处理复杂异步流(如数据流、取消操作)。
- Web Workers:多线程并行计算,解决 CPU 密集型任务阻塞主线程的问题。
总结
JavaScript 异步编程的发展始终围绕 “简化代码结构” 和 “增强可维护性” 展开:
- 回调函数 → 2. Promise → 3. Generator → 4. Async/Await。
目前,async/await 是主流的异步方案,结合 Promise 的基础能力,能够以最简洁的方式处理大多数异步场景。对于更复杂的异步流(如取消、并发控制),可结合 Promise.all、Promise.race 或第三方库(如 RxJS)实现。
JS执行过程中分为哪些阶段
总结
JavaScript 的执行过程可归纳为:
- 解析代码 → 生成 AST。
- 编译优化 → 生成字节码或机器码。
- 创建执行上下文 → 处理作用域、变量提升。
- 同步代码执行 → 处理调用栈。
- 异步代码处理 → 通过事件循环调度任务队列。
- 垃圾回收 → 释放无用内存。
JavaScript 的执行过程可以分为多个阶段,这些阶段协同工作,确保代码从解析到最终运行结果的生成。以下是 JavaScript 执行的核心阶段:
1. 语法解析(Parsing)
JavaScript 引擎首先将代码解析为可执行的结构:
- 词法分析(Lexical Analysis):将代码拆分为最小的语法单元(Token),如变量名、运算符、括号等。
- 语法分析(Syntax Analysis):根据语法规则(如 ECMAScript 规范),将 Token 转换为抽象语法树(AST,Abstract Syntax Tree)。
- 预解析(Pre-Parsing):现代引擎(如 V8) 会对未立即执行的函数或代码块进行快速预解析,优化后续执行效率。
2. 编译(Compilation)
现代 JavaScript 引擎(如 V8)采用即时编译(JIT, Just-In-Time)技术:
- 解释器(Ignition):将 AST 转换为字节码(Bytecode),快速启动代码执行。
- 优化编译器(TurboFan):对热点代码(反复执行的代码)进行优化,生成机器码(Machine Code)以提高性能。
3. 执行上下文创建(Execution Context Creation)
当代码开始执行时,JavaScript 引擎会创建执行上下文(Execution Context),包括:
- 全局执行上下文:代码首次运行时的默认环境。
- 函数执行上下文:每次调用函数时创建。
- Eval 执行上下文(较少使用)。
执行上下文的生命周期:
- 创建阶段:
- 绑定
this值。 - 创建词法环境(Lexical Environment):
- 变量提升(Hoisting):
var变量和函数声明被初始化(值为undefined或函数体)。 let/const变量进入“暂时性死区”(TDZ)。
- 变量提升(Hoisting):
- 绑定
- 执行阶段:逐行执行代码,赋值变量、调用函数等。
4. 代码执行(Code Execution)
引擎按顺序执行代码:
- 同步代码:逐行执行,遇到函数调用时创建新的函数执行上下文。
- 异步代码:通过事件循环(Event Loop)处理回调(如
setTimeout、Promise)。- 调用栈(Call Stack):记录当前执行的函数位置。
- 任务队列(Task Queue):存储待执行的异步回调(宏任务)。
- 微任务队列(Microtask Queue):存储优先级更高的回调(如
Promise.then)。
执行顺序:
- 同步代码 → 2. 所有微任务 → 3.
一个宏任务→ 重复步骤 2-3。
5. 垃圾回收(Garbage Collection)
JavaScript 引擎自动管理内存,通过标记-清除(Mark-and-Sweep)等算法回收不再使用的对象。
关键机制:事件循环(Event Loop)
事件循环是 JavaScript 处理异步的核心:
- 主线程执行同步代码,遇到异步任务时交给 Web API(如
setTimeout、fetch)处理。 - Web API 完成后,将回调推入任务队列。
- 主线程空闲时,事件循环按优先级依次处理队列:
- 微任务队列(如
Promise.then、MutationObserver)优先于宏任务。 - 宏任务队列(如
setTimeout、DOM 事件、script标签)。
- 微任务队列(如
示例:
console.log("Start"); // 同步代码
setTimeout(() => console.log("Timeout"), 0); // 宏任务
Promise.resolve().then(() => console.log("Promise")); // 微任务
console.log("End"); // 同步代码
// 输出顺序:
// Start → End → Promise → Timeout
定时器的执行顺序
setTimeout 回调的入队时机
-
调用
setTimeout(callback, 1000)时:- 浏览器启动一个计时器,计时开始计时 1000 毫秒
- setTimeout 调用本身是同步执行的,回调不会立即进入任务队列
- 直到计时器到期(1000ms 后),回调函数才被放入宏任务队列
-
当事件循环空闲并且调用栈清空后,事件循环会从宏任务队列取出该回调执行
-
需要注意:
- 1000ms 是最短等待时间,实际执行时间可能更晚,受事件循环状态影响
- 如果主线程一直忙于执行任务,回调会被延迟执行
2.3 总结示例流程
console.log('start');
setTimeout(() => {
console.log('timeout callback');
}, 1000);
console.log('end');
-
输出顺序:
start立即打印- 调用
setTimeout,计时开始,回调未入队列 end立即打印- 1000ms 后,回调进入宏任务队列
- 事件循环取出回调执行,打印
timeout callback
三、常见误区或面试陷阱
- ❌ 误认为
setTimeout调用时回调立即进入任务队列 - ❌ 误解等待时间为精确执行时间,忽略事件循环负载和任务排队延迟
- ❌ 混淆宏任务和微任务执行顺序
- ❌ 忽略事件循环在浏览器与 Node.js 中细节差异
答题要点
- 事件循环保证单线程中异步任务的顺序执行
setTimeout回调在计时结束后才入宏任务队列,不是调用时- 宏任务与微任务分离,微任务优先执行
- 1000ms 是最短等待时间,实际执行受事件循环调度影响
setInterval()
setInterval()函数会按照指定的时间间隔重复执行回调函数。每次间隔时间到达时,就会将回调函数添加到任务队列中。- 与
setTimeout()类似
需要注意的是,定时器的执行顺序可能会受到多种因素的影响,如浏览器的性能、其他脚本的执行情况等。因此,在实际应用中,不要依赖定时器来实现精确的时间控制,尤其是在处理复杂的交互或实时性要求较高的场景时。
同步与异步的执行顺序
在 JavaScript 中,同步代码和异步代码的执行顺序遵循事件循环机制。不管是同步还是异步,js都会按顺序执行
执行顺序
- 同步代码:会按照在代码中出现的顺序依次执行,在当前函数执行完之前,不会执行后续的代码。例如函数调用、变量声明与赋值、算术运算等都是同步执行的。只有当前的同步任务执行完毕,才会去执行下一个同步任务。
- 异步代码:不会立即执行,而是在满足特定条件(如定时器时间到、事件触发、Promise 被解决等)后,将相关的回调函数放入任务队列中。当执行栈中的同步代码全部执行完毕后,事件循环会从任务队列中取出回调函数并放入执行栈中执行。
总的来说,同步代码先执行,异步代码后执行,异步代码内部根据其类型(宏任务或微任务)以及在任务队列中的顺序来执行。同步的任务没有优先级之分,异步执行有优先级 ,先执行微任务(microtask队列),再执行宏任务(macrotask队列),同级别按顺序执行
事件循环是由谁来处理的 2025年阿里社招5年
一、考察点
- 理解事件循环(Event Loop)机制的实现主体
- 掌握事件循环与 JavaScript 引擎及宿主环境的关系
- 理解浏览器或 Node.js 中事件循环的工作职责和分工
二、参考答案
2.1 事件循环的实现主体
- 事件循环不是 JavaScript 引擎本身实现的,而是由宿主环境(Host Environment) 负责管理
- 宿主环境
包括浏览器、Node.js 等运行环境,它们提供了事件循环机制 - JavaScript 引擎(如 V8)负责执行 JS 代码和调用栈管理,但不直接控制事件循环
2.2 浏览器中的事件循环
- 浏览器中的事件循环由浏览器内核(如 Chromium 的 Blink)实现
- 浏览器维护多个任务队列(宏任务队列、微任务队列等)
- 浏览器调度任务,执行 JS 代码,处理事件、渲染页面等
- 浏览器事件循环协调 JS 执行与 UI 渲染,使得异步操作顺畅进行
2.3 Node.js 中的事件循环
- Node.js 事件循环由 libuv 库实现
- libuv 负责管理异步 I/O、定时器、微任务和宏任务队列
- Node.js 事件循环模型与浏览器类似,但有额外阶段处理网络、文件等任务
- Node.js 本身基于 V8 引擎执行 JS 代码,但事件循环是 libuv 的职责
2.4 综述
| 组件 | 角色 | 责任 |
|---|---|---|
| JavaScript 引擎 | 解释执行 JavaScript 代码 | 管理调用栈,执行同步代码 |
| 宿主环境(浏览器/Node.js) | 实现事件循环、管理任务队列 | 调度任务执行,协调异步事件 |
三、常见误区或面试陷阱
- ❌ 误认为事件循环是 JavaScript 引擎的一部分
- ❌ 忽视宿主环境对事件循环的控制和实现差异
- ❌ 混淆 JavaScript 语言本身和运行环境的职责
- ❌ 忽略 Node.js 事件循环与浏览器事件循环的实现差异
答题要点
- 事件循环由 JavaScript 的宿主环境负责实现,不属于 JS 引擎本身
- 浏览器内核负责浏览器中的事件循环机制
- Node.js 事件循环由 libuv 实现,管理异步 I/O 和任务调度
- JavaScript 引擎负责代码执行,事件循环负责异步任务管理和调度
在浏览器环境下,JavaScript执行在哪个进程和线程中 25年阿里5年
一、考察点
- 了解现代浏览器多进程架构
- 掌握 JavaScript 执行所在的具体进程和线程位置
- 理解浏览器各个进程和线程的职责划分
- 理解单线程模型与浏览器多线程架构的关系
二、参考答案
2.1 浏览器多进程架构简介
-
现代浏览器一般采用多进程架构,主要进程包括:
- 浏览器主进程(Browser Process) :负责管理浏览器窗口、标签页、网络请求、UI 等
- 渲染进程(Renderer Process) :负责页面渲染、JavaScript 执行、DOM 处理等
- GPU 进程:负责图形加速和渲染
- 网络进程(部分浏览器) :专门处理网络通信
2.2 JavaScript 执行的进程
- JavaScript 代码运行在 渲染进程(Renderer Process) 中
- 每个标签页通常对应一个或多个渲染进程,保证标签页隔离和稳定性
2.3 JavaScript 执行的线程
- 在渲染进程内部,JavaScript 执行在线程池中的 主线程(Main Thread) 上
- 这个主线程负责执行 JS 代码、处理用户交互事件、操作 DOM 和布局、绘制页面等
- 由于 JS 是单线程的,所有 JS 代码在该主线程上顺序执行
2.4 其他线程
-
浏览器中还有其他线程辅助渲染和网络请求,比如:
- 渲染线程(Compositor Thread) 负责合成页面层
- 事件线程 处理异步事件和回调
- 工作线程(Web Workers) 允许创建独立线程运行 JS,避免阻塞主线程
2.5 总结
| 组件 | 所在进程 | 所在线程 | 作用 |
|---|---|---|---|
| JavaScript 代码 | 渲染进程 | 渲染进程主线程(主线程) | 执行 JS,操作 DOM,处理事件 |
| UI 渲染 | 渲染进程及 GPU 进程 | 多线程并行 | 页面绘制、合成、加速 |
| 网络请求 | 浏览器主进程或网络进程 | 独立线程 | 处理网络 I/O |
| Web Worker | 渲染进程 | 独立工作线程 | JS 多线程执行,计算密集任务 |
三、常见误区或面试陷阱
- ❌ 认为 JS 代码运行在浏览器主进程
- ❌ 误解浏览器单进程模型,忽视现代多进程设计
- ❌ 以为 JS 运行在多个线程中,忽略单线程模型及 Web Worker 区别
- ❌ 混淆渲染线程与 JS 主线程的职责
答题要点
- JavaScript 代码运行在浏览器的 渲染进程 中
- JS 执行在线程池的 主线程(主渲染线程) 上,单线程模型
- 浏览器采用多进程多线程架构,分离渲染、网络、GPU 等任务
- Web Worker 可用于 JS 多线程执行,但与主线程分开
- 了解这些对调试性能和设计前端架构非常重要
如何处理异常捕获
JS中的异常捕获(目的:把抛出的错误捕获到,不让其 阻断浏览器的继续执行)一般写法如下:
1. try...catch 语句
try...catch 语句是最基础的异常捕获方式,它能捕获同步代码块里的异常。
示例
try {
const result = 1 / 0; // 这里可能会引发异常
console.log(result);
} catch (error) {
console.log('捕获到异常:', error.message);
}
2. try...catch...finally 语句
finally 块不管 try 块里的代码是否抛出异常,都会被执行。
示例
try {
const arr = [1, 2, 3];
console.log(arr[10]); // 这里会引发异常
} catch (error) {
console.log('捕获到异常:', error.message);
} finally {
console.log('finally 块被执行');
}
3. 异步代码中的异常捕获
回调函数中的异常捕获
在使用回调函数处理异步操作时,需要在回调函数内部进行异常捕获。
function asyncOperation(callback) {
setTimeout(() => {
try {
const result = 1 / 0; // 模拟异常
callback(null, result);
} catch (error) {
callback(error, null);
}
}, 1000);
}
asyncOperation((error, result) => {
if (error) {
console.log('捕获到异常:', error.message);
} else {
console.log('结果:', result);
}
});
Promise 中的异常捕获
Promise 提供了 catch 方法来捕获异步操作中的异常。
function asyncPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const result = 1 / 0; // 模拟异常
if (isNaN(result)) {
reject(new Error('计算结果不是数字'));
} else {
resolve(result);
}
}, 1000);
});
}
asyncPromise()
.then((result) => {
console.log('结果:', result);
})
.catch((error) => {
console.log('捕获到异常:', error.message);
});
async/await 中的异常捕获
在使用 async/await 处理异步操作时,可以使用 try...catch 来捕获异常。
async function main() {
try {
const result = await asyncPromise();
console.log('结果:', result);
} catch (error) {
console.log('捕获到异常:', error.message);
}
}
main();
4. 全局异常捕获
在浏览器环境中,可以使用 window.onerror 来捕获全局未处理的异常。
window.onerror = function (message, source, lineno, colno, error) {
console.log('全局捕获到异常:', message);
return true; // 返回 true 可以阻止默认的错误处理行为
};
// 模拟全局异常
const undefinedVariable = someUndefinedVariable; // 这里会引发异常
综上所述,根据不同的代码场景,合理运用上述异常捕获方法,能有效处理代码中可能出现的异常,增强代码的健壮性。
定时器为什么是不精确的
因为定时器是异步的,要等到同步任务执行完之后,才会去执行异步的任务,即使setTimeout(0)中时间为0也不是立马执行。再者w3c在HTML标准中规定,要求setTimeout时间低于4ms的都按4ms来算。
解决方法: 使用 web Worker 将定时函数作为独立线程执行
如何解决同步调用代码耗时太高的问题
①异步处理 ②web worker 开辟线程处理
fetch
不过话说回来,fetch虽然有很多优点,但是使用fetch来进行项目开发时,也是有一些常见问题的,下面就来说说fetch使用的常见问题。
1、fetch兼容性
在各个浏览器低版本的情况下都是不被支持的。那么问题来了,如何在所有浏览器中通用fetch呢,当然就要考虑fetch的polyfill了。上面说过,fetch是基于Promise来实现的,所以在低版本浏览器中Promise可能也未被原生支持,所以还需要Promise的polyfill; 大多数情况下,实现fetch的polyfill需要涉及到的:promise的polyfill,例如es6-promise、babel-polyfill提供的promise实现。。
2、fetch默认不携带cookie
若要fetch请求携带cookie信息,只需设置一下credentials选项即可,例如fetch(url, {credentials: 'include'});
3、fetch请求对某些错误http状态不会reject ⭐️
这主要是由fetch返回promise导致的,因为fetch返回的promise在某些错误的http状态下如400、500等不会reject,相反它会被resolve;只有网络错误会导致请求不能完成时,fetch 才会被 reject;所以一般会对fetch请求做一层封装
4、fetch不支持超时timeout处理
用过fetch的都知道,fetch不像大多数ajax库那样对请求设置超时timeout,它没有有关请求超时的feature,这一点比较蛋疼。所以在fetch标准添加超时feature之前,都需要polyfill该特性。
5、fetch不支持JSONP
fetch请求对某些错误http状态不会reject ⭐️
在 JavaScript 中,fetch API 用于发起网络请求,但它有一个特性,就是只有在网络请求本身失败(如网络不通、无法连接到服务器等)时才会 reject,对于 HTTP 状态码表示的错误(如 404、500 等),fetch 默认并不会 reject,而是会 resolve 一个 Response 对象,下面详细介绍其原因和解决办法。
原因分析
fetch 设计成这样是为了让开发者能够更灵活地处理不同的 HTTP 状态码。HTTP 状态码代表了请求的不同结果,有些状态码虽然表示错误,但可能是预期的业务逻辑一部分,例如 401 未授权、403 禁止访问等,开发者可能希望根据这些状态码执行不同的操作,而不是简单地让请求失败。
示例代码
以下代码展示了 fetch 对错误 HTTP 状态码的处理:
fetch('https://example.com/nonexistent')
.then(response => {
console.log('请求已完成,状态码:', response.status);
return response;
})
.catch(error => {
console.log('请求失败:', error);
});
在上述代码中,如果请求的 URL 不存在,服务器会返回 404 状态码,但 fetch 不会进入 catch 块,而是会正常执行 then 块,因为 fetch 认为请求已经成功发出并收到了响应,只是响应的状态码表示错误。
解决办法
如果希望 fetch 在遇到某些错误 HTTP 状态码时 reject,可以在 then 块中手动检查状态码,并在需要时抛出错误。
fetch('https://example.com/nonexistent')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
})
.then(data => {
// 处理响应数据
console.log(data);
})
.catch(error => {
console.log('请求失败:', error);
});
在这个改进后的代码中,首先检查 response.ok 属性,该属性是一个布尔值,表示响应的状态码是否在 200 - 299 之间。如果状态码不在这个范围内,就抛出一个错误,这样请求就会进入 catch 块。通过这种方式,你可以根据需要自定义 fetch 对不同 HTTP 状态码的处理逻辑。
异步整个执行周期
JavaScript 的异步执行周期围绕 事件循环(Event Loop) 展开,整个过程涉及主线程、调用栈、任务队列(宏任务和微任务)以及宿主环境(如浏览器或 Node.js)的协作。以下是异步代码的完整执行周期:
4. 事件循环调度
事件循环按以下顺序处理任务队列:
- 执行完所有同步代码(调用栈为空)。
- 清空微任务队列:依次执行所有微任务(包括微任务中产生的新的微任务)。
- 执行一个宏任务:从宏任务队列中取出一个任务执行。
- 重复步骤 2-3,直到所有队列为空。
异步执行周期的关键细节
- 微任务优先级高于宏任务:
- 每次调用栈清空后,必须先清空所有微任务,再执行一个宏任务。
- 微任务会“插队”:
- 如果在宏任务执行过程中产生新的微任务,这些微任务会在当前宏任务完成后立即执行。
- 任务队列的类型:
- 宏任务队列可能有多个(如浏览器中的定时器队列、IO 队列、渲染队列等),但每次事件循环只处理一个宏任务。
- 微任务队列只有一个,必须完全清空。
- 渲染时机:
- 在浏览器中,页面渲染(UI 更新)发生在宏任务之间
requestAnimateFrame是宏任务。
- 在浏览器中,页面渲染(UI 更新)发生在宏任务之间
Node.js 的特殊情况
Node.js 的事件循环分为多个阶段(如 timers、poll、check),每个阶段处理不同类型的宏任务:
timers阶段:处理setTimeout、setInterval。poll阶段:处理 I/O 回调。check阶段:处理setImmediate回调。- 微任务:在阶段切换之间执行。
总结:异步执行周期
- 同步代码 → 2. 微任务队列 → 3. 一个宏任务 → 4. 重复步骤 2-3。
理解这一机制可以避免常见的异步陷阱(如执行顺序不符合预期),并优化代码性能(如合理使用微任务减少渲染延迟)。
JS为什么要区分微任务和宏任务
JavaScript 区分微任务和宏任务主要有以下原因:
确保异步操作的正确性
- 避免竞态条件:微任务会在当前任务执行结束后立即执行,这保证了其优先级。例如,多个 Promise 链式调用时,微任务机制能保证每个.then () 或.catch () 回调按添加顺序依次执行。
- DOM 操作的一致性:像 MutationObserver 这种用于监听 DOM 变化的微任务,可在 DOM 变化后及时执行相关回调,保证对 DOM 操作和状态变化处理的及时性与一致性。若不区分微任务和宏任务,在复杂 DOM 交互场景下,可能出现 DOM 状态更新与相关操作处理不同步的问题。
优化性能和资源利用
- 合理安排任务执行时机:宏任务一般是需要排队等待 JavaScript 引擎空闲时才能执行的任务,如 setTimeout、setInterval、I/O 操作等。
将一些耗时或不紧急的操作(如网络请求、定时任务)放在宏任务队列中,可避免阻塞当前任务执行。例如,在页面渲染时,若将 I/O 操作这类宏任务与页面渲染代码同步执行,可能导致页面卡顿;将其作为宏任务异步执行,就不会影响页面渲染的流畅性。 - 提高执行效率:
微任务的存在让一些紧急、需立即处理的任务能优先执行。比如 Promise 的回调,在异步操作完成后,可迅速通过微任务进行后续处理,无需等待宏任务队列执行。而且,在同一事件循环中,微任务在宏任务执行完毕后立即执行,能利用宏任务执行后的空闲时间,提升代码整体执行效率。
配合事件循环机制
JavaScript 是单线程语言,通过事件循环机制处理异步任务。区分微任务和宏任务为事件循环提供了任务优先级机制,明确了任务执行顺序。这避免任务执行混乱,保证程序逻辑的正确性和稳定性。
介绍下Promise x3 ⭐️⭐️
是ES6标准的异步编程的一种解决方案,解决回调地狱
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise 是 JavaScript 中处理异步操作的核心机制,它通过更清晰、更结构化的方式替代传统的回调函数模式。以下是 Promise 的详细解析:
一、Promise 的核心概念
1. 什么是 Promise?
- 定义:Promise 是一个表示异步操作最终完成或失败的对象。
- 特点:
- 状态明确:Promise 有明确的三种状态(
pending、fulfilled、rejected)。 - 不可逆性:状态一旦从
pending变为fulfilled或rejected,不可逆转。 - 链式调用:通过
.then()和.catch()实现链式操作,避免回调地狱。
- 状态明确:Promise 有明确的三种状态(
2. Promise 的状态
| 状态 | 描述 | 触发条件 |
|---|---|---|
pending | 初始状态,异步操作尚未完成 | 创建 Promise 时默认状态 |
fulfilled | 异步操作成功完成 | 调用 resolve(value) |
rejected | 异步操作失败 | 调用 reject(reason) |
二、Promise 的基本用法
1. 创建 Promise
如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
const promise = new Promise((resolve, reject) => {
// 异步操作(如网络请求、定时器)
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('成功的结果');
} else {
reject('失败的原因');
}
}, 1000);
});
promise本身是同步的,但是他的成功的回调.then方法里面是异步的
console.log(1); // 同步
new Promise(resolve => {
console.log(2); // 同步(Promise 构造函数内)
resolve();
}).then(() => console.log(3)); // 微任务
setTimeout(() => console.log(4)); // 宏任务
console.log(5); // 同步
// 执行顺序:1 → 2 → 5 → 3(微任务)→ 4(宏任务)
Promise 相关的 “异步逻辑” 只在 then/catch/finally 回调,且属于微任务;宏任务是其他类型(如 setTimeout),和 Promise 本身无关。
2. 处理 Promise 结果
promise
.then((result) => {
console.log('成功:', result);
})
.catch((error) => {
console.error('失败:', error);
})
.finally(() => {
console.log('无论成功失败,最终执行');
});
三、Promise 的链式调用
Promise 通过返回新 Promise 实现链式调用:
fetchData()
.then(processData) // 处理第一步结果
.then(uploadData) // 处理第二步结果
.then(() => {
console.log('所有操作完成');
})
.catch(handleError); // 统一处理所有错误
链式调用的规则:
- 每个
.then()返回一个新的 Promise。 - 如果回调函数返回一个值,新 Promise 会用该值
resolve。 - 如果回调函数抛出错误,新 Promise 会
reject。 - 如果回调函数返回一个 Promise,则后续链式调用会等待该 Promise 完成。
四、Promise 的静态方法
1. Promise.resolve(value)
快速创建一个已解决的 Promise:
Promise.resolve(42).then(console.log); // 输出 42
2. Promise.reject(reason)
快速创建一个已拒绝的 Promise:
Promise.reject('Error').catch(console.error); // 输出 Error
3. Promise.all(iterable)
并行执行多个 Promise,全部成功时返回结果数组,任一失败则立即终止:
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
Promise.all([p1, p2])
.then(([res1, res2]) => {
console.log(res1, res2); // 输出 1 2
});
4. Promise.race(iterable)
返回最先完成的 Promise(无论成功或失败):
const p1 = new Promise(resolve => setTimeout(() => resolve('A'), 500));
const p2 = new Promise(resolve => setTimeout(() => resolve('B'), 200));
Promise.race([p1, p2]).then(console.log); // 输出 B
5. Promise.allSettled(iterable)(ES2020)
等待所有 Promise 完成(无论成功或失败):
Promise.allSettled([p1, p2])
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.error(result.reason);
}
});
});
五、Promise 的常见问题与解决方案
1. 回调地狱的解决
问题:传统嵌套回调难以维护。 解决:Promise 链式调用扁平化代码结构:
// 回调地狱
getData(data => {
process(data, processedData => {
upload(processedData, () => {
console.log('完成');
});
});
});
// Promise 链式调用
getData()
.then(process)
.then(upload)
.then(() => console.log('完成'));
2. 错误处理
问题:回调函数需手动传递错误。
解决:通过 .catch() 统一捕获链中所有错误:
fetchData()
.then(process)
.catch(err => {
console.error('错误捕获:', err);
});
3. 并行与串行
- 并行:使用
Promise.all同时执行多个异步操作。 - 串行:通过链式调用逐个执行:
// 串行示例
function serialExecution() {
return task1()
.then(task2)
.then(task3);
}
六、Promise 的底层原理
- 状态管理:维护
pending、fulfilled、rejected状态。 - 回调队列:在
pending状态时收集回调函数,状态变更后触发。 - 链式调用:每个
.then()返回新 Promise,处理返回值可能是 Promise 的情况。 - then return出去的值,会被后面的then接收,如果后面还有跟then的话,catch同理
- promise不管返回什么值,都会被包装成一个promise对象,即使这个返回值是error
- then接收到的值,如果不是一个函数,会穿透到后面的then
- Promise实现了链式调用,也就是说每次调用then之后返回的都是一个Promise,并且是一个全新的Promise,原因也是因为状态不可变。如果你在then中 使用了return,那么return的值会被Promise.resolve()包装
Promise.resolve(1)
.then(res => {
console.log(res) // => 1
return 2 // 包装成 Promise.resolve(2)
})
.then(res => {
console.log(res) // => 2
})
七、Promise 的最佳实践
- 避免嵌套:始终返回 Promise 以保持链式调用。
- 错误处理:每个链式调用末尾添加
.catch()。 - 合理使用
async/await:结合async/await编写更同步化的代码。 - 避免冗余 Promise:已有 Promise 时无需再包装。
八、Promise 的局限性
- 无法取消:一旦创建 Promise,无法中途取消。
- 单一结果:一个 Promise
只能表示一个异步操作的最终结果。 - 错误吞噬:未处理的 Promise 错误可能导致静默失败(可通过
unhandledrejection事件监听)。
-
混淆
Promise.race()和Promise.any()。- 纠正:
Promise.race()只要有任何一个 Promise 完成(fulfilled或rejected)就会立即结束。Promise.any()(ES2021) 则是只要有一个 Promisefulfilled就会结束,如果所有 Promise 都rejected才会rejected。
- 纠正:
-
性能问题: 并行执行虽然快,但也要考虑服务器并发处理能力,避免过度请求导致服务过载。对于大量并发请求,可以考虑分批处理 (batching)。
总结
Promise 是 JavaScript 异步编程的基石,它通过状态管理、链式调用和统一的错误处理,显著提升了代码的可读性和可维护性。理解其核心机制(如微任务、事件循环)和常见用法(如 Promise.all、错误捕获),是掌握现代 JavaScript 异步编程的关键。结合 async/await 语法,可以进一步简化异步代码的编写。
Promise 使用场景
- 异步操作:处理需要时间的异步操作,如网络请求等。
- 并发控制:使用
Promise.all()来并行处理多个异步操作,并等待它们全部完成。 - 串行任务:按顺序执行一系列依赖前一个结果的异步操作。
- 错误处理:集中处理异步操作中的错误。
- 定时器:使用
setTimeout()或setInterval()与Promise结合,实现定时执行。
如何让Promise.all在抛出异常后依然有效
方案一:使用 map 方法处理每个 Promise
在 Promise.all() 队列中,我们使用 map 方法对每个 Promise 进行处理。如果任何一个 Promise 失败,我们返回一个特定的值,以确保整个 Promise.all() 能够正常执行并走到 .then() 中。
方案二:使用 Promise.allSettled 替代 Promise.all()
另一个解决方案是使用 Promise.allSettled() 方法。这个方法会返回一个新的 Promise,它在所有给定的 Promise 已经被解析或被拒绝后解析。每个对象都描述了每个 Promise 的结果。
promise.catch后面的.then还会执行吗?
会执行!核心规则:catch 捕获错误后,后续的 .then 会正常执行(相当于错误已被 “处理”,Promise 链恢复正常流程) ;只有未被捕获的错误,才会跳过后续 .then 直接触发下一个 catch。
简单拆解两种关键场景:
1. 正常情况:catch 捕获错误 → 后续 .then 执行
catch 的作用是 “捕获前面链条中的错误”,一旦捕获,Promise 链的状态会恢复为 fulfilled(成功),后续的 .then 会按正常流程执行(接收 catch 的返回值,或透传)。
示例:
Promise.reject('出错了') // 初始状态:rejected
.catch(err => {
console.log('捕获错误:', err); // 输出 "捕获错误:出错了"
return '错误已处理'; // catch 返回值,成为下一个 then 的输入
})
.then(res => {
console.log('后续 then 执行:', res); // 正常执行!输出 "后续 then 执行:错误已处理"
});
2. 特殊情况:catch 内部抛错 → 后续 .then 跳过,触发下一个 catch
如果 catch 内部本身抛出错误(或返回 Promise.reject),则错误会继续向下传递,跳过后续 .then,直到下一个 catch 捕获。
示例:
Promise.reject('出错了')
.catch(err => {
console.log('捕获错误:', err); // 输出 "捕获错误:出错了"
throw new Error('catch 内部又出错了'); // 抛出新错误
})
.then(res => {
console.log('这里不会执行'); // 被跳过
})
.catch(newErr => {
console.log('捕获到 catch 内部的错误:', newErr.message); // 输出 "捕获到 catch 内部的错误:catch 内部又出错了"
});
总结
catch捕获错误后(无新错误抛出):后续.then正常执行;catch内部抛错:后续.then跳过,错误传递给下一个catch;- 核心逻辑:
catch是 “错误处理节点”,处理完(无新错)则链条恢复正常,没处理完(抛新错)则错误继续传递。
Promise then第二个参数和catch的区别是什么?
Promise 的 then 方法和 catch 方法都是用来处理 Promise 在变为 rejected 状态时的错误情况。以下是它们之间的区别和使用方式:
-
then 方法的第二个参数:
- 当 Promise 被拒绝(rejected)时,
then方法的第二个参数,即错误处理函数,会被调用。 - 这个错误处理函数接收一个参数,即 Promise 被拒绝时返回的错误信息。
- 需要在每次调用
then方法时提供第二个参数,以便处理可能出现的错误。
- 当 Promise 被拒绝(rejected)时,
-
catch 方法:
catch方法用于捕获 Promise 被拒绝时的错误,与then方法的第二个参数功能相同。catch方法可以链式调用,不需要在每次调用then方法时都传递错误处理函数。
手写Promise实现x3
以下是手写一个简化版 Promise 的实现,涵盖核心功能(状态管理、链式调用、异步处理),适合理解 Promise 的底层原理:
// Promise 的三种状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(executor) {
this.status = PENDING; // 初始状态
this.value = undefined; // 成功的结果
this.reason = undefined; // 失败的原因
this.onFulfilledCallbacks = []; // 成功的回调队列
this.onRejectedCallbacks = []; // 失败的回调队列
// 定义 resolve 函数
const resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// 执行所有成功回调
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
// 定义 reject 函数
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// 执行所有失败回调
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
// 立即执行执行器函数
executor(resolve, reject);
} catch (err) {
// 捕获执行器中的错误
reject(err);
}
}
// then 方法实现链式调用
then(onFulfilled, onRejected) {
// 处理透传(值穿透)
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };
// 返回新的 Promise
const promise2 = new MyPromise((resolve, reject) => {
// 统一处理回调
const handleCallback = (callback, valueOrReason, resolve, reject) => {
// 异步执行(模拟微任务,此处用 setTimeout 简化)
setTimeout(() => {
try {
const x = callback(valueOrReason);
// 处理返回值可能是 Promise 的情况
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
}, 0);
};
// 当前状态为 fulfilled
if (this.status === FULFILLED) {
handleCallback(onFulfilled, this.value, resolve, reject);
}
// 当前状态为 rejected
if (this.status === REJECTED) {
handleCallback(onRejected, this.reason, resolve, reject);
}
// 当前状态为 pending,将回调存入队列
if (this.status === PENDING) {
this.onFulfilledCallbacks.push(() => {
handleCallback(onFulfilled, this.value, resolve, reject);
});
this.onRejectedCallbacks.push(() => {
handleCallback(onRejected, this.reason, resolve, reject);
});
}
});
return promise2;
}
// catch 方法(语法糖)
catch(onRejected) {
return this.then(null, onRejected);
}
// 静态 resolve 方法
static resolve(value) {
// 如果参数是 Promise 实例,直接返回
if (value instanceof MyPromise) return value;
// 否则包装成 Promise
return new MyPromise(resolve => resolve(value));
}
// 静态 reject 方法
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
// 其他静态方法(如 all、race 等)可在此扩展...
}
// 处理 then 返回值 x 的通用逻辑
function resolvePromise(promise2, x, resolve, reject) {
// 避免循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
// 防止重复调用
let called = false;
// 如果 x 是对象或函数
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
try {
const then = x.then;
// 如果 x 是 Promise
if (typeof then === 'function') {
then.call(
x,
y => {
if (called) return;
called = true;
// 递归解析,直到返回值非 Promise
resolvePromise(promise2, y, resolve, reject);
},
r => {
if (called) return;
called = true;
reject(r);
}
);
} else {
// 普通对象
resolve(x);
}
} catch (err) {
if (called) return;
called = true;
reject(err);
}
} else {
// 普通值
resolve(x);
}
}
核心功能验证
// 测试基本功能
const p = new MyPromise((resolve, reject) => {
setTimeout(() => resolve('success'), 1000);
});
p.then(res => {
console.log(res); // 1秒后输出 "success"
return 'chain1';
})
.then(res => {
console.log(res); // 输出 "chain1"
return new MyPromise(resolve => resolve('chain2'));
})
.then(res => {
console.log(res); // 输出 "chain2"
throw new Error('error');
})
.catch(err => {
console.log(err.message); // 输出 "error"
});
// 测试静态方法
MyPromise.resolve('static resolve').then(console.log); // 输出 "static resolve"
MyPromise.reject('static reject').catch(console.log); // 输出 "static reject"
实现要点
- 状态管理:
pending→fulfilled或pending→rejected,状态不可逆。
- 异步处理:
- 使用
setTimeout模拟微任务队列(实际 Promise 使用微任务队列)。
- 使用
- 链式调用:
then返回新 Promise,支持链式调用。- 处理返回值可能是 Promise 的情况(递归解析)。
- 错误处理:
- 捕获执行器函数和回调中的错误。
catch是then(null, onRejected)的语法糖。
- 值穿透:
- 当
then的参数不是函数时,直接将值传递到下一层。解释:.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。当then中传入的不是函数,则这个then返回的promise的data,将会保存上一个的promise.data。这就是发生值穿透的原因。而且每一个无效的then所返回的promise的状态都为resolved。
- 当
Promise.resolve(1)
.then(2) // 注意这里
.then(Promise.resolve(3))
.then(console.log)
输出:1
与原生的差异
- 微任务队列:
- 原生 Promise 使用微任务队列(如
queueMicrotask或MutationObserver),此处用setTimeout(宏任务)简化。
- 原生 Promise 使用微任务队列(如
- 边缘情况:
- 未完全覆盖所有 Promise/A+ 规范细节(如
thenable对象的深度处理)。
- 未完全覆盖所有 Promise/A+ 规范细节(如
- 性能优化:
- 未实现真正的微任务调度和引擎级优化。
扩展建议
- 实现
Promise.all:static all(promises) { return new MyPromise((resolve, reject) => { const results = []; let count = 0; promises.forEach((p, i) => { MyPromise.resolve(p).then( res => { results[i] = res; if (++count === promises.length) resolve(results); }, err => reject(err) ); }); }); } - 实现
Promise.race:static race(promises) { return new MyPromise((resolve, reject) => { promises.forEach(p => { MyPromise.resolve(p).then(resolve, reject); }); }); }
此实现可用于理解 Promise 的核心机制,实际开发中应直接使用原生 Promise。
介绍Promise异常捕获
如果 Promise 状态已经变成resolved,再抛出错误是无效的。 ⭐️
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
上面代码中,Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise产生的错误
});
如何实现 Promise.finally ?
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。finally本质上是then方法的特例。
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
实现
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
实现promise.all和原理x2
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
下面是一个具体的例子。
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
上面代码中,promises是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。
实现
function isPromise(obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}
const myPromiseAll = (arr)=>{
let result = [];
return new Promise((resolve,reject)=>{
for(let i = 0;i < arr.length;i++){
if(isPromise(arr[i])){
arr[i].then((data)=>{
result[i] = data;
if(result.length === arr.length){
resolve(result)
}
},reject)
}else{
result[i] = arr[i];
}
}
})
}
实现promise.retry
Promise.retry = function(getData, times, delay) {
return new Promise((resolve, reject) => {
function attemp() {
getData().then((data) => {
resolve(data)
}).catch((err) => {
if (times === 0) {
reject(err)
} else {
times--
setTimeout(attemp, delay)
}
})
}
attemp()
})
}
Promise 和 async/await 和 callback 、Generator的区别x3 ⭐️⭐️⭐️⭐
对比总结
| 特性 | Callback | Promise | Generator | async/await |
|---|---|---|---|---|
| 代码结构 | 嵌套回调 | 链式调用 | 生成器函数 + yield | 同步风格 + await |
| 错误处理 | 手动传递错误 | .catch() | try/catch + 执行器 | try/catch |
| 控制流 | 自行管理 | 链式控制 | 可手动控制 | 自动控制 |
| 可读性 | 低(回调地狱) | 中 | 中 | 高 |
| 异步任务管理 | 复杂(需外部库) | 支持 all/race | 依赖执行器 | 结合 Promise 方法 |
| 适用场景 | 简单异步操作 | 复杂链式异步 | 特殊流程控制 | 现代异步编程 |
演进关系
- Callback → Promise:解决回调地狱,统一错误处理。
- Promise → Generator:尝试用同步风格写异步代码,但依赖执行器。
- Generator → async/await:
语法糖封装,成为终极异步方案。
如何选择?
- 简单场景:Callback(如事件监听)。
- 链式异步:Promise(如多个接口顺序调用)。
- 复杂异步逻辑:async/await(如循环 + 条件判断)。
- 特殊控制需求:Generator(如自定义流程调度)。
现代开发中,async/await 是主流方案,但需结合 Promise 的基础能力。
promise 有几种状态, Promise 有什么特色和优缺点 x4 ⭐️⭐️
等待中(pending) 完成了 (resolved) 拒绝了(rejected)
一、核心特色
- 链式调用:替代回调嵌套(回调地狱),用
then/catch/finally链式调用,逻辑更线性清晰; - 状态不可逆:只有 3 种状态(pending/fulfilled/rejected),一旦从 pending 转为成功 / 失败,状态永久固定,避免重复触发;
- 统一错误处理:
then可省略回调,直接透传结果;错误会沿链式自动冒泡到最近的catch,统一捕获异常;
二、优点
- 解决 “回调地狱”:嵌套逻辑扁平化,可读性、可维护性大幅提升;
- 错误处理统一:相比回调的 “错误优先回调”(err, res),
catch可集中捕获链式中所有错误,更简洁; - 支持并行 / 串行控制:配合
Promise.all/Promise.race/Promise.allSettled等静态方法,轻松处理多异步任务协作; - 与现代语法兼容:是
async/await的基础,可无缝衔接,写出 “类同步” 的异步代码。
三、缺点
- 无法取消:一旦创建 Promise 并执行,无法中途取消(需手动封装取消逻辑);
- 状态不透明:外部无法直接查看 Promise 当前是 pending/fulfilled/rejected,只能通过回调感知;
promise如何实现then处理 ? x3
Promise和setTimeout的区别(Event Loop)x3
Promise构造函数中是立即执行(同步任务),then函数分发到微任务Event Queue(异步任务),setTimeout是分发到宏任务中
设计并实现 Promise.race()
Promise._race = promises => new Promise((resolve, reject) => {
promises.forEach(promise => {
promise.then(resolve, reject)
})
})
使用async会注意哪些东西 ⭐️⭐️
特点
- await只能放到async函数中
- 相比genrator语义化更强
- await后面可以是promise对象,也可以数字、字符串、布尔
- async函数总是返回是一个promise对象
- 只要await语句后面Promise状态变成 reject, 那么整个async函数会中断执行
- async函数和普通函数一样按顺序执行,同时,在执行到await语句时,返回一个Promise对象
注意
1)await 命令后面的Promise对象,运行结果可能是 rejected,此时等同于 async 函数返回的 Promise 对象被reject。因此需要加上错误处理,可以给每个 await 后的 Promise 增加 catch 方法;也可以将 await 的代码放在 try…catch 中。
2)多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发,代码如下
//下面两种写法都可以同时触发
//法一
async function f1() {
await Promise.all([
new Promise((resolve) => {
setTimeout(resolve, 600);
}),
new Promise((resolve) => {
setTimeout(resolve, 600);
})
])
}
//法二
async function f2() {
let fn1 = new Promise((resolve) => {
setTimeout(resolve, 800);
});
let fn2 = new Promise((resolve) => {
setTimeout(resolve, 800);
})
await fn1;
await fn2;
}
3)await命令只能用在async函数之中,如果用在普通函数,会报错
1.3 常见误区或面试陷阱
-
误区: 认为
async/await替代了Promise。- 纠正:
async/await是建立在Promise之上的更高层抽象,是Promise的语法糖,它使得Promise的使用更加方便。没有Promise,就没有async/await。
- 纠正:
-
误区: 滥用
await,导致串行执行。- 纠正: 如果多个异步操作之间没有依赖关系,应该考虑并行执行它们(例如使用
Promise.all()),而不是一个接一个地await,这会降低效率。
- 纠正: 如果多个异步操作之间没有依赖关系,应该考虑并行执行它们(例如使用
-
面试陷阱: 不清楚
async函数内部抛出的错误如何被捕获。- 纠正:
async函数内部抛出的错误会被自动捕获并作为一个被拒绝的 Promise 返回,因此可以使用try...catch语句来捕获,或者在调用async函数时使用.catch()。
- 纠正:
-
误区: 认为
await可以直接用在全局作用域。- 纠正:
await关键字只能在async函数内部使用。在顶级模块中使用await需要特定的环境支持(如 ES Modules 中的 Top-level await)。
- 纠正:
-
只字不提 Generator 函数和 Promise 的关系。
- 纠正:
async/await实际上是 Generator 函数和 Promise 的语法糖。Generator 函数提供了暂停和恢复执行的能力,Promise 提供了异步操作的解决方案。两者结合形成了async/await的基础。
- 纠正:
答题要点
- 理解
async/await是基于 Promise 的语法糖,而不是替代品。 - 掌握
async/await的基本语法和优势:同步化的异步代码、错误处理、可读性。 - 能够将 Promise 链式调用改写为
async/await形式。 - 了解
async/await的局限性。
Async里面有多个await请求,可以怎么优化(请求是否有依赖)
- 有依赖:串行 await(按顺序写,逻辑优先);
- 无依赖:并行执行(用
Promise.all/allSettled,性能优先); - 核心原则:不做无意义的等待,让能同时跑的请求尽量同时跑。
对async、await的理解,内部原理以及实现 ⭐️
async/await 是 ES2017 推出的语法糖,核心作用是 让异步代码写起来像同步代码—— 本质是基于 Promise 封装,没有脱离 Promise 的异步模型,却大幅降低了异步逻辑的可读性和维护成本。
一、核心理解(是什么 + 怎么用)
1. 核心定位
async:修饰函数,表明该函数是 “异步函数”,返回值必然是 Promise(哪怕函数内 return 原始值,也会被自动包装成Promise.resolve(原始值);若抛出错误,会包装成Promise.reject(错误))。await:只能用在async函数内部,作用是 “等待 Promise 决议” —— 会暂停当前 async 函数的执行,直到后面的 Promise 变为 fulfilled(成功)或 rejected(失败),再恢复执行并返回结果(或抛出错误)。
2. 极简使用示例
// async 函数返回 Promise
async function fetchData() {
try {
// await 等待 Promise 完成,直接拿到结果(无需 then)
const res = await fetch('https://api.example.com/data'); // 等待请求完成
const data = await res.json(); // 等待解析 JSON
return data; // 自动包装为 Promise.resolve(data)
} catch (err) {
// 统一捕获所有 await 对应的 Promise 错误(替代 catch 回调)
console.error('请求失败:', err);
throw err; // 可选:向外抛出错误,让调用者处理
}
}
// 调用 async 函数(本质还是 Promise)
fetchData().then(data => console.log(data)).catch(err => ...);
二、内部原理:语法糖的本质
async/await 不是全新的异步机制,而是 Generator 函数 + Promise 的语法封装—— 浏览器 / Node.js 内部会把 async 函数编译成 Generator 函数,再通过一个 “自动执行器” 驱动其完成异步流程。
先铺垫两个关键前提:
- Generator 函数:ES6 特性,用
function*定义,内部用yield暂停执行,返回迭代器(需手动调用next()恢复执行); - 异步流程需要 “自动执行”:Generator 本身不会自动跑,而 async/await 不需要手动触发,核心是内部有一个 “自动执行器”(类似第三方库
co的逻辑)。
原理拆解:3 步实现 async/await
我们可以通过 “手动模拟”,看透其内部转化逻辑:
1. 第一步:async 函数 → Generator 函数(编译层面)
async 函数会被编译成一个 Generator 函数,await 关键字对应 Generator 中的 yield。比如下面的 async 函数:
async function fetchData() {
const res = await fetch('url1');
const data = await res.json();
return data;
}
会被编译成类似这样的 Generator 函数:
// 编译后的 Generator 函数(浏览器内部行为)
function* fetchDataGenerator() {
const res = yield fetch('url1'); // await → yield
const data = yield res.json(); // await → yield
return data;
}
2. 第二步:自动执行器(核心驱动逻辑)
Generator 函数需要手动调用 iterator.next() 才能推进执行,而 async/await 的 “自动等待”,本质是内部有一个执行器,自动帮我们调用 next(),且会把 yield 后面的 Promise 结果,作为 next() 的参数传入(让 Generator 恢复执行时能拿到异步结果)。
手动实现一个极简自动执行器(模拟内部逻辑):
// 自动执行 Generator 函数,返回 Promise(对应 async 函数的返回值)
function autoRun(generatorFunc) {
return new Promise((resolve, reject) => {
const iterator = generatorFunc(); // 生成迭代器
// 递归执行 next()
function run(nextValue) {
try {
const { value: promise, done } = iterator.next(nextValue); // 执行到下一个 yield
if (done) {
// Generator 执行完毕,resolve 最终返回值
resolve(promise);
return;
}
// 等待 yield 后面的 Promise 决议,再递归调用 run(传入结果)
Promise.resolve(promise).then(
(result) => run(result), // 成功:把结果传给 next(),继续执行
(err) => reject(err) // 失败:直接 reject,终止执行
);
} catch (err) {
reject(err); // 捕获 Generator 内部同步错误
}
}
// 启动执行器
run();
});
}
3. 第三步:调用自动执行器(完成 async/await 逻辑)
通过自动执行器调用 Generator 函数,就实现了 async/await 的效果:
// 调用自动执行器,等价于调用原始的 async 函数
autoRun(fetchDataGenerator).then(data => console.log(data)).catch(err => ...);
原理总结
async/await 的内部流程 = async 函数编译为 Generator 函数 + 自动执行器驱动 Generator 执行:
await xxx→ 编译为yield Promise.resolve(xxx)(哪怕 xxx 不是 Promise,也会被包装);- 自动执行器等待
yield后的 Promise 完成,拿到结果后调用iterator.next(结果),让 Generator 恢复执行; - 重复步骤 2,直到 Generator 执行完毕(
done: true),自动执行器返回的 Promise 决议为最终结果; - 若过程中 Promise 失败或抛出错误,自动执行器会 reject 错误,可通过
try/catch捕获(对应 async 函数内的 try/catch)。
三、关键特性(对应原理的延伸)
- await 只能在 async 函数内使用:因为 await 依赖 Generator 的
yield暂停逻辑,而普通函数没有迭代器机制,无法实现 “暂停 - 恢复”; - async 函数返回值必然是 Promise:这是自动执行器的设计决定的(autoRun 直接返回 Promise);
- 错误捕获:
await对应的 Promise 失败时,会触发自动执行器的 reject,可通过try/catch捕获(替代 Promise 的catch回调); - 执行顺序:
await会暂停当前 async 函数,但不会阻塞整个事件循环(本质是 Promise 微任务的延迟执行)。
四、核心总结
- 本质:async/await 是 Generator + Promise + 自动执行器 的语法糖,没有新增异步模型,只是简化了 Promise 的调用方式;
- 优势:相比 raw Promise 的
then链式调用,代码更线性、可读性更强,错误处理更直观(try/catch 统一捕获); - 底层依赖:必须基于 Promise 实现(若 await 后面不是 Promise,会先包装成 Promise),且依赖 Generator 的迭代器机制实现 “暂停 - 恢复”。
为何try里面放return,finally还会执行,理解其内部机制
核心结论:finally 块的设计语义是 “无论 try/catch 执行结果如何(正常返回、抛错、return/break/continue),都必须执行” ,内部机制本质是 JS 引擎对代码的 “执行顺序重排”—— 先暂存 try/catch 的返回值 / 错误状态,执行完 finally 后,再返回暂存结果或抛出暂存错误。
简单拆解内部机制:
-
执行流程重排(关键) 当 try 里有 return 时,JS 不会直接返回,而是分 3 步走:
- 第一步:执行 try 块内代码,遇到 return 时,先计算 return 后面的表达式值(比如 return 1+2 会先算出 3),并把这个结果 “暂存” 起来;
- 第二步:暂停 try 的返回逻辑,转去执行 finally 块内的所有代码(无论 finally 里有没有逻辑,都会执行);
- 第三步:finally 执行完后,再把第一步暂存的结果返回出去(完成 try 的 return 逻辑)。
-
finally 能 “覆盖” 返回值吗?(补充注意) 如果 finally 里也有 return,会直接覆盖 try 里的暂存结果 —— 因为 finally 的 return 会 “打断流程”,执行到 finally 的 return 时,会直接返回,不再管 try 里暂存的结果。示例:
function fn() {
try {
return 1; // 暂存结果 1
} finally {
return 2; // 直接返回 2,覆盖 try 的 1
}
}
console.log(fn()); // 输出 2
- 本质:finally 的 “强制执行” 语义JS 引擎在编译时,会把 finally 块的代码 “插入” 到 try/catch 的所有退出路径(return、throw、break 等)之前,确保无论如何都能执行。它的优先级高于 try/catch 的退出逻辑,这是语言层面的设计规则,目的是让 finally 承担 “清理资源”(比如关闭文件、取消请求)的职责 —— 这些操作无论代码是否正常执行,都必须完成。
总结:try 里的 return 会 “先算结果、暂存起来”,等 finally 执行完再返回;finally 的执行是 JS 引擎强制保证的,不受 try 里的 return 影响,核心是为了满足 “必须执行清理逻辑” 的语义。
Node 与浏览器 EventLoop 的差异
核心差异:宏任务队列的分类与执行顺序不同(微任务队列逻辑一致),Node 有多个宏任务队列且按优先级执行,浏览器只有一个宏任务队列按 “先进先出” 执行,具体简单总结:
一、相同点(基础逻辑一致)
- 执行顺序核心:同步代码 → 所有微任务(Promise.then/catch/finally、queueMicrotask 等)→ 一轮宏任务 → 所有微任务 → ...(循环);
- 微任务队列:两者都只有一个,优先级高于所有宏任务,同一轮微任务会全部执行完。
二、核心差异(宏任务的处理)
| 维度 | 浏览器 EventLoop | Node.js EventLoop(v11+ 后,接近浏览器但仍有差异) |
|---|---|---|
| 宏任务队列 | 只有 1 个 “全局宏任务队列”(先进先出) | 分 6 个优先级不同的宏任务队列(如 timers、poll、check 等) |
| 宏任务执行方式 | 每次取 1 个宏任务执行,完后清微任务 | 每次按优先级遍历队列,取当前队列所有任务执行,完后清微任务 |
| 典型宏任务优先级 | 无优先级,按添加顺序执行 | timers(setTimeout/setInterval)> poll(I/O 回调)> check(setImmediate)等 |
三、关键场景体现差异
-
setTimeout(0) vs setImmediate(最直观):
- 浏览器:两者同属一个宏任务队列,按添加顺序执行(谁先写谁先跑);
- Node:timers 队列优先级高于 check 队列,但因 Node 启动有微小延迟,
setTimeout(0)实际可能比setImmediate后执行(多数情况 timers 先跑,偶尔因延迟反转)。
-
I/O 回调的位置:
- 浏览器:I/O 回调(如 AJAX、DOM 事件)和 setTimeout 同属一个宏任务队列,按顺序执行;
- Node:I/O 回调属于 poll 队列,优先级介于 timers 和 check 之间,会在 timers 执行完后、check 执行前处理。
四、总结(极简版)
- 微任务:两者完全一致,优先执行;
- 宏任务:浏览器 “一锅炖”(先进先出),Node“分队列、按优先级” 执行;
- 核心记住:Node 有宏任务优先级,浏览器没有,这是最主要差异。
ECMAScript和JavaScript的关系
核心关系:ECMAScript 是 “标准 / 规范”,JavaScript 是 “标准的实现” —— 简单说就是「标准 vs 具体产品」的关系,且 JavaScript 是 ECMAScript 最主流的实现。