标签

Promise

Promise 是一种用于延迟计算的策略,适用于多种并发风格:用于本地计算的线程和事件循环并发,以及同步和异步远程消息传递。Promise 代表一个异步操作的最终结果。使用 Promises 的主要方式是通过一个方法,该方法注册从 promise 的最终值或失败原因到新 promise 的转换。

Promise
前端5月30日 02:24
Promise 并发控制如何实现?Promise 并发控制就是限制同一时间运行的异步任务数量。面试可以先说结论:维护一个任务池,启动任务后放入 executing,数量达到上限就 await Promise.race(executing),等任意任务结束再继续放新任务。最后用 Promise.all 收集全部结果。真实项目里常用于批量请求、上传、爬取、发邮件,目的是保护浏览器连接数、服务端限流和内存。 ## 追问 ### 并发控制和 Promise.all 有什么区别? Promise.all 会一次性启动所有任务;并发控制只同时跑固定数量。前者适合少量独立任务,后者适合几百、几千个任务。 ### 为什么用 Promise.race? 它能等“最先完成的一个任务”。有任务完成后,池子空出位置,就可以继续补下一个任务。 ### 失败任务怎么处理? 看业务。要快速失败就让错误抛出;要尽量完成全部任务,就给每个任务包一层 catch,返回 `{status, value, reason}`。 ### 并发数怎么定? 没有固定答案。浏览器请求可从 4-8 开始;Node 服务要看下游 QPS、CPU、连接池和超时率,再动态调。 ## 写段代码 ```js async function pool(limit, list, worker) { const ret = []; const running = []; for (const item of list) { const p = Promise.resolve().then(() => worker(item)); ret.push(p); const e = p.finally(() => running.splice(running.indexOf(e), 1)); running.push(e); if (running.length >= limit) await Promise.race(running); } return Promise.all(ret); } ```
前端5月30日 02:24
Promise 性能优化面试怎么答?Promise 性能优化的核心不是“少用 Promise”,而是少制造没必要的异步层级,让能并行的任务并行,让高频请求有缓存、去重和并发限制。面试里先答三点:避免 new Promise 包一层已有 Promise;独立任务用 Promise.all 并行;批量任务别一次性打满,用队列或 p-limit 控制数量。再补一句:性能问题通常来自请求瀑布、重复请求、长链微任务和未释放的引用。 ## 追问 ### Promise.all 一定更快吗? 不一定。只有任务互不依赖时才更快;如果后一个请求依赖前一个结果,强行并行会写错逻辑。Promise.all 还会遇到一个失败就整体失败的问题。 ### 为什么不建议包一层 new Promise? 已有 Promise 直接返回即可。多包一层会增加对象创建、微任务调度和错误传播复杂度,还容易漏掉 reject。 ### 请求去重怎么做? 用 Map 保存进行中的 Promise,相同 key 直接复用;结束后在 finally 里删除,避免内存泄漏。 ### 长 Promise 链会慢吗? 真正慢的通常不是链本身,而是链里塞了大量同步计算或无意义 then。可读性差时改 async/await,但不要把独立任务写成串行 await。 ## 写段代码 ```js const pending = new Map(); function once(key, fn) { if (pending.has(key)) return pending.get(key); const p = fn().finally(() => pending.delete(key)); pending.set(key, p); return p; } async function load() { const [user, posts] = await Promise.all([ once('user', fetchUser), once('posts', fetchPosts) ]); return { user, posts }; } ```
服务端5月30日 02:24
Promise.any() 有什么作用?和 Promise.race 有什么区别?`Promise.any()` 的作用是:一组 Promise 里只要有一个成功,就立刻返回这个成功结果;只有全部失败时,才会 reject,并抛出 `AggregateError`。它适合“多个候选源,谁先成功用谁”的场景,比如多 CDN 拉资源、多个镜像接口兜底。面试里要强调:它忽略失败,只关心第一个成功;这和 `Promise.race()` 谁先 settled 就返回完全不同。 ## 追问 ### Promise.any 和 Promise.race 最大区别是什么? `race` 看第一个完成,不管成功还是失败;`any` 看第一个成功,失败会被暂时忽略,除非全部失败。 ### 全部失败时会发生什么? 会 reject 一个 `AggregateError`,里面的 `errors` 保存所有失败原因。不要只 catch 后打印 message,最好把 errors 也记录下来。 ### 它和 Promise.all 有什么区别? `all` 要全部成功才成功,一个失败就失败;`any` 只要一个成功就成功。一个适合“都要”,一个适合“有一个可用就行”。 ### 实际项目里怎么用? 适合容灾兜底,不适合支付、写入、下单这类不能重复尝试的操作。并行请求多个源时也要考虑取消慢请求或控制成本。 ## 写段代码 ```javascript try { const data = await Promise.any([ fetch('/cdn-a/config.json'), fetch('/cdn-b/config.json') ]); console.log(await data.json()); } catch (e) { console.error(e.errors); } ```
前端5月27日 23:41
async/await 的执行原理是什么?与 Promise 和事件循环有什么关系?async/await 是 ES2017 引入的异步编程语法,本质上基于 Promise 和 Generator 实现。理解它的工作原理,关键在于弄清 await 做了什么、代码到底在哪一步暂停、以及它与事件循环中微任务队列的关系。 ## async 函数的返回值 async 函数无论内部返回什么,调用它拿到的永远是一个 Promise。返回普通值会被 Promise.resolve() 包装,抛出异常则对应一个 rejected 的 Promise。 ```javascript async function foo() { return 42; } foo(); // Promise { fulfilled: 42 } async function bar() { throw new Error("fail"); } bar(); // Promise { rejected: Error: fail } ``` 这一点是后续理解执行流程的前提:async 函数本身并不异步执行,函数体内 await 之前的代码是同步运行的,只有遇到 await 才会产生暂停效果。 ## await 到底做了什么 await 的执行分两步: 1. 立即求值 await 右侧的表达式。如果右侧不是 Promise,则用 Promise.resolve() 包装。 2. 暂停当前 async 函数的执行,将 await 之后的代码注册为该 Promise 的 then 回调——即放入微任务队列。 注意:await 不会阻塞整个 JavaScript 主线程,它只暂停自己所在的 async 函数。外部调用栈会继续往下执行。 ```javascript async function demo() { console.log(1); await Promise.resolve(); console.log(2); } console.log("a"); demo(); console.log("b"); // 输出顺序: a → 1 → b → 2 ``` 为什么是 a → 1 → b → 2?console.log("a") 同步执行;调用 demo() 进入函数体,console.log(1) 同步执行;遇到 await,后面的 console.log(2) 被放入微任务队列,函数返回一个 pending 的 Promise;回到调用栈继续执行 console.log("b");同步代码跑完后,事件循环检查微任务队列,执行 console.log(2)。 ## 事件循环与微任务队列 JavaScript 的事件循环模型决定了 async/await 的执行时序: - 宏任务:script 整体代码、setTimeout、setInterval、I/O 回调等 - 微任务:Promise.then/catch/finally、await 之后的代码、queueMicrotask 等 执行规则:每执行完一个宏任务,就会清空整个微任务队列,然后再执行下一个宏任务。 ```javascript console.log("script start"); setTimeout(() => console.log("setTimeout"), 0); Promise.resolve() .then(() => console.log("promise1")) .then(() => console.log("promise2")); async function async1() { console.log("async1 start"); await async2(); console.log("async1 end"); } async function async2() { console.log("async2"); } async1(); console.log("script end"); // 输出顺序: // script start → async1 start → async2 → script end → // promise1 → async1 end → promise2 → setTimeout ``` 解析:同步代码先执行完毕;微任务队列中 promise1 先入队,async1 end 随后入队(因为 await async2() 右侧同步执行完后,await 后的代码才入微任务),所以 promise1 先输出,接着 async1 end,然后 promise1.then 产生 promise2 再执行;最后才轮到宏任务 setTimeout。 ## async/await 与 Promise 的等价转换 async/await 是 Promise 的语法糖,每一段 async/await 代码都可以机械地改写为 Promise 链式调用: ```javascript // async/await 写法 async function fetchUser() { try { const res = await fetch("/api/user"); const data = await res.json(); return data; } catch (e) { console.error(e); throw e; } } // 等价 Promise 写法 function fetchUser() { return fetch("/api/user") .then(res => res.json()) .then(data => data) .catch(e => { console.error(e); throw e; }); } ``` V8 引擎在早期版本中将 async/await 编译为基于 Generator 的状态机(配合 __awaiter 辅助函数),现代 V8 已优化为直接生成 Promise 链,减少了 Generator 中间层带来的性能开销。 ## 错误处理 ### try/catch 捕获 ```javascript async function fetchData() { try { const res = await fetch("/api/data"); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (e) { console.error("请求失败:", e.message); throw e; } } ``` ### try/catch 能否捕获未 await 的 Promise 异常? 不能。如果忘记 await,异常是 Promise 的 rejection,不会冒泡到外层 try/catch: ```javascript async function foo() { try { asyncFnThatThrows(); // 没有 await,异常丢失 } catch (e) { // 捕获不到 } } ``` 必须加 await 或手动 .catch() 才能捕获。 ## 并发控制 ### 顺序 vs 并行 多个独立的异步操作不要用 await 逐个等待,应使用 Promise.all 并行执行: ```javascript // 顺序执行 — 慢 async function sequential() { const a = await fetchA(); // 等 1s const b = await fetchB(); // 再等 1s,总计 ~2s } // 并行执行 — 快 async function parallel() { const [a, b] = await Promise.all([fetchA(), fetchB()]); // 总计 ~1s } ``` ### 容错并行 Promise.allSettled 不会因某个请求失败而中断,适合需要全部结果(含失败)的场景: ```javascript async function fetchAll() { const results = await Promise.allSettled([ fetchUser(), fetchPosts(), fetchComments() ]); results.forEach(r => { if (r.status === "fulfilled") console.log(r.value); else console.error(r.reason); }); } ``` ## 常见陷阱 ### 在循环中顺序 await ```javascript // 慢 — 逐个等待 for (const url of urls) { const data = await fetch(url); process(data); } // 快 — 并发请求 const results = await Promise.all(urls.map(u => fetch(u))); results.forEach(process); ``` ### 在顶层直接使用 await ES2022 引入了 Top-level await,在 ES Module 的顶层可以直接使用 await,但 CommonJS 模块中仍需包裹在 async 函数内。 ### await 只能用在 async 函数内 ```javascript function foo() { await bar(); // SyntaxError } ``` ## 面试追问 Q: async 函数中 await 一个非 Promise 值会怎样? 会自动用 Promise.resolve() 包装,等价于 await 一个立即 resolve 的 Promise。await 之后的代码仍会进入微任务队列,在当前同步代码执行完后才运行。 Q: 为什么 await 后面的代码是微任务而不是宏任务? 因为 await 的语义是等待 Promise 完成后继续执行,这个继续执行本质上就是 Promise 的 then 回调,而 Promise.then 属于微任务。如果放在宏任务队列中,每轮事件循环只会执行一个宏任务,延迟过高且不符合语义。 Q: async/await 相比 Promise.then 链式调用有什么不足? 两个主要局限:一是无法方便地实现 Promise.race/all 等组合逻辑,仍需回到 Promise API;二是 try/catch 无法区分错误来源,而 .catch() 可以在特定 .then 后精准捕获。
前端5月27日 23:35
如何实现 Promise 的取消?Promise 一旦创建就无法从外部中断它的执行——这是面试中频繁出现的考点,也是实际开发中经常遇到的痛点。下面直接给出答案,再逐步分析每种方案的原理和取舍。 ## 核心答案 Promise 本身不支持取消。状态一旦从 pending 变为 fulfilled 或 rejected 就不可逆,这是规范设计决定的。但我们可以通过以下方式间接实现取消效果: | 方案 | 原理 | 是否真正取消 | 适用场景 | |------|------|------------|---------| | AbortController | 浏览器标准 API,通过 signal 通知异步操作中止 | 是(对支持的 API) | fetch、Node.js 流操作等 | | 包装函数 | 用标志位忽略 resolve/reject 的结果 | 否,仅忽略结果 | 简单场景、旧代码兼容 | | CancellationToken | 手动传递令牌,在关键节点检查 | 半取消(需主动配合) | 复杂业务逻辑、多步骤任务 | | Promise.race | 用超时 Promise 竞争 | 否,仅忽略结果 | 超时控制 | **面试追问答法**:为什么说包装函数不是真正取消?——因为原始 Promise 内部的异步操作仍在执行,只是我们不再处理它的结果。真正的取消需要异步操作本身支持中止,比如 fetch 接收到 abort 信号后会终止 TCP 连接。 ## 为什么 Promise 规范不内置取消? ES6 Promise 遵循 Promises/A+ 规范,核心设计原则是**不可变性**:状态一旦确定就不再改变。这个设计换来了两个关键保证: - **可靠性**:then 注册的回调一定会在状态确定后执行,不存在"取消导致回调不执行"的歧义 - **可组合性**:Promise 链可以自由组合,不必担心中间环节被意外取消 取消操作引入的副作用(资源未释放、回调丢失、竞态条件)远大于收益,所以规范层面选择了不支持。Domenic Denicola 曾在 TC39 提案中解释过这个设计决策:取消是**操作**的属性,不是**值**的属性,而 Promise 代表的是值。 ## AbortController:标准方案详解 AbortController 是 Web API(不是 ECMAScript 规范),但已成为事实上的取消标准。Node.js 从 v15 起完整支持。 ### 基本用法 ```javascript const controller = new AbortController(); const signal = controller.signal; fetch('/api/data', { signal }) .then(res => res.json()) .then(data => console.log(data)) .catch(err => { if (err.name === 'AbortError') { console.log('请求已取消'); } }); // 取消 controller.abort(); ``` 调用 `abort()` 后,signal 上的 `aborted` 属性变为 true,同时触发 abort 事件。fetch 内部监听了这个信号,会主动断开请求。 ### 封装超时请求 ```javascript function fetchWithTimeout(url, options = {}, timeout = 5000) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); return fetch(url, { ...options, signal: controller.signal }) .then(res => { clearTimeout(id); return res.json(); }) .catch(err => { clearTimeout(id); if (err.name === 'AbortError') { throw new Error(`请求超时(${timeout}ms)`); } throw err; }); } ``` ### AbortSignal.timeout()——更简洁的超时方案 现代浏览器和 Node.js 18+ 支持 `AbortSignal.timeout()`,无需手动管理 setTimeout: ```javascript // 5 秒超时自动取消 fetch('/api/data', { signal: AbortSignal.timeout(5000) }) .then(res => res.json()) .catch(err => { if (err.name === 'AbortError') { console.log('超时或手动取消'); } }); ``` ### 给自定义异步函数添加取消支持 关键是监听 signal 的 abort 事件并在回调中执行清理: ```javascript async function delay(ms, { signal } = {}) { return new Promise((resolve, reject) => { if (signal?.aborted) { return reject(signal.reason ?? new DOMException('Aborted', 'AbortError')); } const timer = setTimeout(resolve, ms); signal?.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason ?? new DOMException('Aborted', 'AbortError')); }, { once: true }); }); } ``` > 注意:必须用 `{ once: true }` 防止重复触发,且要在 Promise resolve 后清除定时器避免资源泄漏。 ## 包装函数方案 不依赖任何 API,兼容性最好,但只能"忽略结果",不能"停止执行": ```javascript function makeCancellable(promise) { let cancelled = false; const wrapped = new Promise((resolve, reject) => { promise.then( val => cancelled || resolve(val), err => cancelled || reject(err) ); }); return { promise: wrapped, cancel() { cancelled = true; } }; } // 使用 const { promise, cancel } = makeCancellable( fetch('/api/data').then(r => r.json()) ); promise.then(data => console.log(data)); cancel(); // 后续 then 不会执行,但 fetch 请求仍在进行 ``` **面试追问**:这种方案有什么隐患?——即使调用了 cancel,原始请求仍在运行,如果它最终 resolve,回调虽不执行,但占用的网络和内存资源不会释放。对于大量并发请求的场景,这会造成资源浪费。 ## CancellationToken 模式 在多步骤任务中,需要在每个关键节点主动检查取消状态: ```javascript class CancellationToken { #cancelled = false; #reason = null; get isCancelled() { return this.#cancelled; } get reason() { return this.#reason; } cancel(reason) { this.#cancelled = true; this.#reason = reason ?? 'Operation cancelled'; } throwIfCancelled() { if (this.#cancelled) throw new Error(this.#reason); } } // 多步骤任务中使用 async function processOrder(orderId, token) { token.throwIfCancelled(); const order = await fetchOrder(orderId); token.throwIfCancelled(); const payment = await processPayment(order); token.throwIfCancelled(); await confirmOrder(order, payment); } ``` 这种模式需要开发者在代码中主动插入检查点,适合步骤清晰的长任务。缺点是如果某个步骤的 Promise 已提交但还没到下一个检查点,中间这段时间无法响应取消。 ## Promise.race 实现超时 ```javascript function withTimeout(promise, ms) { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms) ); return Promise.race([promise, timeout]); } ``` 这种方式简洁但有两个问题:一是超时后原始 Promise 仍在执行;二是如果原始 Promise 先 reject,超时定时器不会清理,造成轻微的内存泄漏。实际生产中优先用 AbortController。 ## 实战:搜索框防抖取消 这是最常见的业务场景——用户快速输入时,只保留最后一次请求: ```javascript function createSearchService() { let controller = null; return async function search(query) { // 取消上一次请求 controller?.abort(); controller = new AbortController(); try { const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal }); return await res.json(); } catch (err) { if (err.name === 'AbortError') return null; // 被取消,静默处理 throw err; } }; } ``` ## 实战:组件卸载时取消请求 以 React 为例,useEffect 返回的清理函数中取消请求: ```javascript useEffect(() => { const controller = new AbortController(); fetch('/api/data', { signal: controller.signal }) .then(res => res.json()) .then(data => setData(data)) .catch(err => { if (err.name !== 'AbortError') console.error(err); }); return () => controller.abort(); }, []); ``` ## 面试追问与边界问题 **Q:Promise.all 中某一个被取消,其他的会怎样?** 不会怎样。Promise.all 只关心结果,取消是通过外部机制(如 AbortController)实现的。如果想让所有请求共享同一个取消信号,传同一个 signal 即可。 **Q:async/await 中如何取消?** await 只是语法糖,取消方式完全一样——传 signal 给底层 API,用 try/catch 捕获 AbortError。 **Q:取消后的 Promise 内存怎么回收?** 取消本身不会自动回收。需要确保:清理定时器、移除事件监听、断开网络连接。AbortController 的 signal 用 `{ once: true }` 绑定监听器,触发后自动移除,这是最佳实践。
前端5月27日 23:32
Promise 有哪几种状态?状态如何转换?## 状态与转换规则 Promise 有三种核心状态,理解状态转换是掌握 Promise 的基础: - **pending**:初始状态,异步操作尚未完成 - **fulfilled**:操作成功,触发 `.then()` 回调 - **rejected**:操作失败,触发 `.catch()` 回调 状态转换只能发生一次:pending → fulfilled 或 pending → rejected,一旦改变不可逆。多次调用 `resolve` 或 `reject`,只有第一次生效。 此外还有一个派生状态 **settled**(已定型),表示 Promise 已完成(无论成功或失败),此时会触发 `.finally()` 回调。settled 不是独立状态,而是 fulfilled 和 rejected 的统称。 ```javascript const p = new Promise((resolve, reject) => { resolve('第一次'); // 生效,状态变为 fulfilled resolve('第二次'); // 忽略,状态已不可变 reject('失败'); // 忽略 }); p.then(val => console.log(val)); // "第一次" ``` ## then 返回值与新 Promise 状态 `.then()` 返回的是一个新的 Promise,它的状态由回调函数的返回值决定: - 返回普通值 → 新 Promise 变为 fulfilled,值为该返回值 - 抛出错误 → 新 Promise 变为 rejected - 返回另一个 Promise → 新 Promise 的状态跟随该 Promise ```javascript Promise.resolve(1) .then(val => val + 1) // 返回 2,新 Promise fulfilled .then(val => { // val 为 2 throw new Error('出错了'); // 新 Promise rejected }) .catch(err => 100) // 捕获错误,返回 100 .then(val => console.log(val)); // 100 ``` ## 错误处理与穿透机制 Promise 的错误会沿链向下传递,直到遇到 `.catch()`。如果 `.then()` 没有提供第二个参数(错误回调),错误会自动穿透到下一个 `.catch()`: ```javascript fetch('/api/data') .then(res => res.json()) // 如果 fetch 失败,错误穿透到 catch .then(data => processData(data)) // 如果上一步失败,继续穿透 .catch(err => console.error('请求失败:', err)); ``` 注意:`.then(onFulfilled, onRejected)` 中,`onRejected` 只捕获前一步的错误,不能捕获 `onFulfilled` 自身的错误。推荐统一使用 `.catch()`。 ## 静态方法对比 | 方法 | 全部成功 | 有失败 | 返回值 | |------|---------|--------|--------| | `Promise.all()` | 返回所有结果 | 第一个失败的原因 | 数组 | | `Promise.allSettled()` | 返回所有状态和结果 | 不会失败 | `{status, value/reason}[]` | | `Promise.race()` | 第一个完成的结果 | 第一个失败的原因 | 单个值 | | `Promise.any()` | 第一个成功的结果 | 全部失败时返回 AggregateError | 单个值 | `Promise.all()` 适合"全部成功才继续"的场景(如并行请求多个接口)。`Promise.allSettled()` 适合"不管成败都要结果"(如批量操作后统计)。`Promise.any()` 适合"取最快成功的"(如多源竞速)。 ## 微任务与执行顺序 Promise 的 `.then()`/`.catch()`/`.finally()` 回调属于微任务,在当前宏任务结束后、下一个宏任务开始前执行。微任务优先级高于宏任务: ```javascript console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => console.log('3')); console.log('4'); // 输出顺序:1 → 4 → 3 → 2 ``` 面试中常考的变体:在 `then` 回调中嵌套 `setTimeout`,或 `async/await` 与 `Promise.then` 的混合执行顺序。 ## 常见陷阱 **1. then 中的 throw 被 catch 捕获,但同步代码中的 throw 不会:** ```javascript Promise.resolve() .then(() => { throw new Error('then中抛出'); }) .catch(e => console.log('捕获:', e.message)); // 捕获: then中抛出 ``` **2. catch 之后还能继续 then:** `.catch()` 本身也返回 Promise,后续可以接 `.then()` 继续执行。 **3. Promise 构造函数中的同步错误:** ```javascript const p = new Promise(() => { throw new Error('构造函数中抛出'); // 会被 Promise 内部捕获,p 变为 rejected }); ``` **4. 返回值是 thenable 对象(有 then 方法的对象)会被当作 Promise 处理:** ```javascript Promise.resolve().then(() => { return { then(resolve) { resolve('thenable'); } }; }).then(val => console.log(val)); // "thenable" ``` ## async/await 本质 async/await 是 Promise 的语法糖。async 函数始终返回 Promise,await 暂停执行直到 Promise 完成。错误处理使用 try/catch,比 `.catch()` 更符合同步代码的直觉写法: ```javascript async function fetchUser() { try { const res = await fetch('/api/user'); const user = await res.json(); return user; } catch (err) { console.error('获取用户失败:', err); } } ``` 注意:`await` 只能在 async 函数内使用(顶层 await 需 ES2022 模块环境)。多个独立异步操作不要串行 await,应使用 `Promise.all()` 并行处理。
前端5月27日 23:31
Promise 的常见陷阱和最佳实践有哪些?## 常见陷阱 ### 忘记返回 Promise 这是 Promise 链中最容易犯的错误。`then` 回调中的返回值会作为下一个 `then` 的输入,忘记 `return` 会导致链断裂: ```javascript // 错误:then 中忘记 return function fetchUser() { getUser().then(user => { return getPosts(user.id); // 没有 return,外层拿不到结果 }); } fetchUser().then(posts => console.log(posts)); // undefined // 正确:return 让 Promise 链延续 function fetchUser() { return getUser().then(user => { return getPosts(user.id); }); } fetchUser().then(posts => console.log(posts)); // posts 数据 ``` ### 在 then 中嵌套 Promise 嵌套写法失去了 Promise 链的核心优势——扁平化异步流程: ```javascript // 错误:回调地狱的 Promise 版 getUser().then(user => { getPosts(user.id).then(posts => { // 嵌套了 getComments(posts[0].id).then(comments => { // 越嵌越深 }); }); }); // 正确:链式扁平调用 getUser() .then(user => getPosts(user.id)) .then(posts => getComments(posts[0].id)) .then(comments => console.log(comments)); ``` ### 忘记 catch 未捕获的 Promise rejection 在 Node.js 中会导致进程退出(`unhandledRejection`),在浏览器中则静默失败,排查困难: ```javascript // 错误:请求失败无任何提示 fetch('/api/data').then(r => r.json()).then(data => render(data)); // 正确:至少捕获错误 fetch('/api/data') .then(r => r.json()) .then(data => render(data)) .catch(err => console.error('请求失败:', err)); ``` **追问:** `.then(func).catch(handler)` 和 `.then(func, handler)` 有什么区别? `catch` 能捕获 `func` 内部的异常,而 `.then(null, handler)` 的第二个参数只处理上一个 Promise 的 rejection,捕获不到 `func` 抛出的错误。 ### 循环中顺序 await 当多个异步操作互不依赖时,逐个 `await` 会白白浪费时间: ```javascript // 错误:串行等待,3 个请求耗时 = 3 × 单次耗时 async function loadDashboard() { const user = await fetchUser(); // 等 1s const posts = await fetchPosts(); // 再等 1s const stats = await fetchStats(); // 再等 1s return { user, posts, stats }; } // 正确:并行发起,3 个请求耗时 ≈ 单次耗时 async function loadDashboard() { const [user, posts, stats] = await Promise.all([ fetchUser(), fetchPosts(), fetchStats() ]); return { user, posts, stats }; } ``` ### 混用 async/await 和 .then() 两种风格混用会让代码风格不一致,增加阅读负担: ```javascript // 错误:同一个函数里混用两种写法 async function getData() { const res = await fetch('/api'); return res.json().then(data => { // 突然切到 .then return transform(data); }); } // 正确:统一用 async/await async function getData() { const res = await fetch('/api'); const data = await res.json(); return transform(data); } ``` ### 不必要的 Promise 包装 已经返回 Promise 的函数不需要再用 `new Promise` 包一层: ```javascript // 错误:反模式 —— Promise 构造函数包装 function getData() { return new Promise((resolve, reject) => { fetch('/api') // fetch 本身就返回 Promise .then(r => r.json()) .then(resolve) .catch(reject); }); } // 正确:直接返回 function getData() { return fetch('/api').then(r => r.json()); } ``` 这种写法被称为 **deferred anti-pattern**,不仅多余,还会吞掉 `resolve`/`reject` 回调中的异常。 ### 构造函数中执行异步操作 构造函数必须同步返回实例,无法 `await`,导致实例属性可能处于未就绪状态: ```javascript // 错误:data 可能为 null class UserService { constructor(id) { this.data = null; fetch(`/api/users/${id}`) .then(r => r.json()) .then(data => { this.data = data; }); } } const svc = new UserService(1); console.log(svc.data); // null —— 请求还没完成 // 正确:静态工厂方法 class UserService { constructor(data) { this.data = data; } static async create(id) { const data = await fetch(`/api/users/${id}`).then(r => r.json()); return new UserService(data); } } const svc = await UserService.create(1); console.log(svc.data); // 有数据 ``` ### 误用 Promise.all 替代条件请求 `Promise.all` 会等所有请求完成,如果部分请求的结果并不需要,就是浪费: ```javascript // 错误:无条件并发所有请求 async function getPage(cond) { const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]); return cond ? a : b; // c 永远用不到,但请求已经发了 } // 正确:按需请求 async function getPage(cond) { if (cond) return { a: await fetchA() }; return { b: await fetchB() }; } ``` ## 最佳实践 ### 用 Promise.allSettled 处理部分失败 `Promise.all` 只要有一个失败就整体失败,而 `allSettled` 会等全部完成,适合"能拿多少拿多少"的场景: ```javascript async function fetchAll(urls) { const results = await Promise.allSettled(urls.map(u => fetch(u).then(r => r.json()))); const ok = results.filter(r => r.status === 'fulfilled').map(r => r.value); const failed = results.filter(r => r.status === 'rejected').map(r => r.reason); return { ok, failed }; } ``` ### 用 Promise.race 实现超时控制 ```javascript function withTimeout(promise, ms) { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`超时 ${ms}ms`)), ms) ); return Promise.race([promise, timeout]); } // 用法 const data = await withTimeout(fetch('/api'), 5000); ``` ### 用 Promise.any 获取最快成功结果 多个数据源竞争时,`Promise.any` 返回第一个 fulfilled 的结果,只有全部失败才抛 `AggregateError`: ```javascript async function getFastest(urls) { try { const res = await Promise.any(urls.map(u => fetch(u))); return await res.json(); } catch (e) { // e 是 AggregateError,包含所有失败原因 throw new Error('所有数据源均不可用'); } } ``` ### 用 AbortController 取消请求 ```javascript const controller = new AbortController(); async function search(query) { const res = await fetch(`/api/search?q=${query}`, { signal: controller.signal }); return res.json(); } // 用户输入新关键词时取消上一次请求 input.addEventListener('input', () => { controller.abort(); search(input.value); }); ``` ### 实现带退避的重试机制 ```javascript async function retry(fn, max = 3) { for (let i = 0; i < max; i++) { try { return await fn(); } catch (err) { if (i === max - 1) throw err; await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i))); // 指数退避 } } } ``` ### 用 finally 做资源清理 `finally` 无论成功失败都会执行,适合关闭连接、隐藏 loading 等场景: ```javascript async function query() { let conn; try { conn = await pool.getConnection(); return await conn.query('SELECT * FROM users'); } finally { conn?.release(); // 无论是否抛异常都会释放连接 } } ``` ### 请求去重 同一时刻对同一资源发起多次请求是浪费,可以用 Map 缓存正在进行的 Promise: ```javascript const pending = new Map(); function fetchOnce(url) { if (pending.has(url)) return pending.get(url); const p = fetch(url) .then(r => r.json()) .finally(() => pending.delete(url)); pending.set(url, p); return p; } ``` ### 并发控制 `Promise.all` 一次全部发出,当数量大时可能打爆服务端。用一个简单的并发池控制: ```javascript async function concurrent(tasks, limit) { const results = []; const executing = new Set(); for (const task of tasks) { const p = task().then(r => { executing.delete(p); return r; }); executing.add(p); results.push(p); if (executing.size >= limit) { await Promise.race(executing); } } return Promise.all(results); } // 最多同时 3 个请求 await concurrent(urls.map(u => () => fetch(u).then(r => r.json())), 3); ``` ## 易错辨析 **Promise.then() 返回的是同一个 Promise 吗?** 不是。每次 `.then()` 都会返回一个新的 Promise,这也是链式调用的基础: ```javascript const p1 = fetch('/'); const p2 = p1.then(r => r.json()); const p3 = p2.then(data => data.id); console.log(p1 === p2); // false console.log(p2 === p3); // false ``` **await 一个非 Promise 值会怎样?** 会立即 resolve。`await 42` 等价于 `await Promise.resolve(42)`,不会阻塞后续微任务。但在 `for...of` 中加 `await` 会拖慢循环,即使值不是 Promise。 **为什么 catch 之后还能继续 then?** `catch` 返回的也是新 Promise,且状态为 fulfilled(除非 catch 回调内又抛异常),所以后面可以继续 `.then()`: ```javascript Promise.reject('err') .catch(e => 'recovered') // 返回 fulfilled('recovered') .then(val => console.log(val)); // 'recovered' ```
前端5月27日 23:25
Promise 微任务什么时候执行?事件循环怎么跑的?面试常问这道题,本质是在考察你对 JS 异步执行顺序的理解。核心答案:微任务在当前宏任务结束后、下一个宏任务开始前全部执行完毕;Promise 的 then/catch/finally 回调属于微任务,会在所有同步代码之后、setTimeout 之前执行。 ## 事件循环的执行顺序 记住这个流程就够了: 1. 执行同步代码(调用栈) 2. 清空微任务队列(全部执行) 3. 取一个宏任务执行 4. 回到步骤 2,循环往复 所以微任务不是"尽快执行",而是"在当前宏任务结束后立即执行"。这是理解所有输出顺序题的根基。 ## 微任务和宏任务有哪些 微任务:Promise.then/catch/finally、queueMicrotask()、MutationObserver、async/await 中 await 后面的代码。 宏任务:setTimeout、setInterval、setImmediate(Node)、I/O、UI 渲染。 ## 经典输出顺序题 ```javascript console.log("1"); setTimeout(() => console.log("2"), 0); Promise.resolve().then(() => console.log("3")); console.log("4"); // 输出:1 → 4 → 3 → 2 ``` 同步代码先跑(1、4),然后清空微任务(3),最后执行宏任务(2)。 ## 链式 then 的执行顺序 ```javascript Promise.resolve() .then(() => console.log("1")) .then(() => console.log("2")) .then(() => console.log("3")); Promise.resolve() .then(() => console.log("4")) .then(() => console.log("5")); // 输出:1 → 4 → 2 → 5 → 3 ``` 每个 then 返回新 Promise,下一个 then 注册为该 Promise 的微任务。两根链条交替推进,按注册顺序轮流执行。 ## 嵌套 Promise 怎么跑 ```javascript Promise.resolve() .then(() => { console.log("1"); Promise.resolve().then(() => console.log("2")); }) .then(() => console.log("3")); // 输出:1 → 2 → 3 ``` 第一个 then 执行时注册了内层微任务,外层第二个 then 也在此时被注册。当前微任务轮次里,两个微任务都已入队,按先进先出执行:先 2 后 3。 ## async/await 和微任务的关系 ```javascript async function foo() { console.log("1"); await bar(); console.log("2"); // 这行是微任务 } function bar() { console.log("3"); } foo(); console.log("4"); // 输出:1 → 3 → 4 → 2 ``` await 后面的代码等价于放到 then 回调里,是微任务。这是 async/await 的本质——语法糖。 ## Node.js 的差异 Node.js 中 process.nextTick 优先级比 Promise.then 更高,会先于微任务队列执行。另外 Node 11 之后,每个宏任务结束后也会清空微任务,行为已与浏览器一致。 ## 追问:微任务会阻塞渲染吗 会。微任务在渲染前执行,如果微任务队列过长,页面就会卡住。所以不要在微任务里做密集计算,该用 setTimeout 让出主线程时就用。
前端5月27日 01:17
Promise 和 async/await 和 Callback 有什么区别?三个阶段的异步方案,层层递进: **Callback**:把后续操作作为回调函数传给异步操作。问题是回调地狱——多层嵌套横向增长,错误处理每个回调都得单独处理。 **Promise**:把回调包装成对象,链式 `.then()` 解决横向嵌套,`.catch()` 统一处理错误。但长链仍不够直观,且 `.then()` 里不能直接用 `try-catch`。 **async/await**:Promise 的语法糖。`async` 函数返回 Promise,`await` 暂停执行等结果。写法就是同步代码的样子,错误用 `try-catch`。本质还是 Promise——`await` 的值就是 `.then()` 回调的参数。 ```javascript // 三个方案的同一操作 // Callback getData((err, data) => { if (err) return; process(data); }); // Promise getData().then(process).catch(handleError); // async/await try { const data = await getData(); process(data); } catch { handleError(); } ``` ## 追问 ### async/await 怎么处理并发请求? `Promise.all([fetch1, fetch2])` 配合 `await`。不要写成 `await fetch1(); await fetch2()`——这样是串行的,第二个请求等第一个完成才发。 ### async 函数返回的 Promise 和普通 Promise 有区别吗? 没有本质区别。async 函数内部抛错等于 reject,return 值等于 resolve。唯一注意的是:async 函数返回的 Promise 是原生 Promise,即使你 return 的是一个 thenable 对象,也会自动包裹成 Promise。
前端2024年6月24日 16:43
Promise 是如何实现链式调用的?Promise 实现链式调用主要依赖于其返回一个新的 Promise 对象的特性。 在 JavaScript 中,Promise 是一个处理异步操作的对象,可以在原调用位置以同步方式处理异步操作结果。 下面是 Promise 的链式调用的基本实现: 1. Promise 构造函数接收一个执行函数,执行函数接收两个参数:resolve 和 reject,分别用于异步操作成功与失败的情况。 2. 调用 Promise 对象的 `.then` 方法提供链式调用。`.then` 方法接收两个参数(都是可选的):`onFulfilled` 和 `onRejected`,分别在 Promise 成功或失败时调用。`.then` 方法也返回一个 Promise 对象,以便进行链式调用。 3. 如果 `onFulfilled` 或 `onRejected` 返回一个值 x,运行 Promise 解决过程:[[Promise Resolution Procedure]](https://promisesaplus.com/#the-promise-resolution-procedure)。 4. 如果 `onFulfilled` 或 `onRejected` 抛出一个异常 e,`Promise.then` 的返回的 Promise 对象会被 reject 掉。 5. 如果 `onFulfilled` 不是函数且 promise1(前一个 promise) 成功执行,promise2(下一个 promise)成功处理 promise1 的 final state。 6. 如果 `onRejected` 不是函数且 promise1 失败,promise2 会拒绝 promise1 的原因。 以下是一个示例: ```javascript new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); // 第一步:创建一个 Promise 并执行一个异步操作 }).then(function(result) { // 第二步:注册一个 onFulfilled 回调 console.log(result); // 打印:1 return result + 2; }).then(function(result) { // 第三步:链式调用 console.log(result); // 打印:3 return result + 2; }).then(function(result) { console.log(result); // 打印:5 return result + 2; }); ``` 在这个例子中,每个 `.then` 调用后都返回一个新的 Promise 对象,这个新的 Promise 对象会立即执行,并在执行完毕后调用下一个 `.then` 注册的回调。通过这种方式,我们可以以同步的方式处理异步的结果,而这就是 Promise 链式调用的本质。
前端2024年6月24日 16:43
如何基于 Promise.all 实现Ajax请求的串行和并行?### Ajax请求的串行实现 对于串行执行多个Ajax请求,我们通常需要确保一个请求完全完成后,再执行下一个请求。这可以通过链式调用`then`方法来实现,也就是在每个Promise对象的`then`方法中启动下一个Ajax请求。 ```javascript function ajaxRequest(url) { return new Promise((resolve, reject) => { // 这里是Ajax请求的代码,成功时调用resolve,失败时调用reject const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText); xhr.send(); }); } const urls = ['/url1', '/url2', '/url3']; // 假设我们有多个请求需要串行处理 let promiseChain = Promise.resolve(); // 初始化一个已完成的Promise urls.forEach(url => { promiseChain = promiseChain.then(() => ajaxRequest(url)).then(response => { console.log('请求完成:', response); // 这里可以处理每个请求的响应 }); }); // 最后可以在所有请求都完成后执行一些操作 promiseChain.then(() => { console.log('所有请求都已串行完成。'); }); ``` 在这个例子中,每个请求仅在前一个请求的`then`方法中被调用,这确保了请求的串行执行。 ### Ajax请求的并行实现 要并行执行多个Ajax请求,可以使用`Promise.all`方法。`Promise.all`接收一个Promise对象数组,等待所有的Promise对象都成功完成后,它将返回一个新的Promise,这个新Promise将解析为一个结果数组,数组中的每个结果对应于原Promise数组中的每个请求。 ```javascript function ajaxRequest(url) { return new Promise((resolve, reject) => { // 这里是Ajax请求的代码,成功时调用resolve,失败时调用reject const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText); xhr.send(); }); } const urls = ['/url1', '/url2', '/url3']; // 假设我们有多个请求需要并行处理 const promises = urls.map(ajaxRequest); // 创建一个包含所有请求的Promise数组 Promise.all(promises).then(responses => { console.log('所有请求都已并行完成。'); responses.forEach(response => { console.log('请求完成:', response); // 这里可以处理每个请求的响应 }); }).catch(error => { // 如果任何一个请求失败,这里会捕获到错误 console.error('请求失败:', error); }); ``` 在这个例子中,`Promise.all`并行地处理所有的Ajax请求,并在所有请求成功完成后,按照请求的顺序输出响应结果。如果任何一个请求失败,`Promise.all`会立即拒绝,并返回第一个遇到的错误。 这两种方法是处理多个Ajax请求时常用的串行和并行模式。根据实际需求选择合适的方式。在实际面试中,可以根据面试官的要求提供更详细的代码实例或解释。
前端2024年6月24日 16:43
如何实现Promise的resolve?在JavaScript中,`Promise` 对象是异步编程的一种解决方案。一个 `Promise` 在创建时处于 `pending`(等待)状态,可以通过其 `resolve` 方法转变为 `fulfilled`(成功)状态,或通过其 `reject` 方法转变为 `rejected`(失败)状态。 要实现 `Promise` 的 `resolve`,通常是在异步操作成功完成时调用。下面是一个简单的例子说明如何使用 `Promise` 的 `resolve` 方法: ```javascript function asyncOperation() { // 创建一个新的Promise对象 return new Promise((resolve, reject) => { // 执行异步操作 setTimeout(() => { const operationWasSuccessful = true; // 假设这是基于异步操作结果的条件 if (operationWasSuccessful) { resolve('Operation successful'); // 如果操作成功,调用resolve并传递结果 } else { reject('Operation failed'); // 如果操作失败,调用reject并传递错误信息 } }, 1000); // 假设这个异步操作需要1秒钟 }); } asyncOperation() .then(result => { console.log(result); // 打印成功结果 }) .catch(error => { console.error(error); // 打印错误信息 }); ``` 在上述代码中,`asyncOperation` 函数返回一个新的 `Promise` 对象。在这个 `Promise` 的构造函数中,有两个参数:`resolve` 和 `reject`。这两个参数也是函数,它们被用来分别处理异步操作的成功和失败情况。在异步操作(这里使用 `setTimeout` 模拟)完成后,根据操作的结果调用 `resolve` 或 `reject`。 如果异步操作成功(在这个例子中,我们假设 `operationWasSuccessful` 为 `true`),则调用 `resolve` 函数并传递结果消息 `'Operation successful'`。这将使得 `Promise` 对象的状态变为 `fulfilled`,并将结果传递给随后的 `.then` 方法的回调函数。 如果异步操作失败,就调用 `reject` 函数并传递错误消息 `'Operation failed'`。这将使得 `Promise` 对象状态变为 `rejected`,并将错误信息传递给随后的 `.catch` 方法的回调函数。