web前端精选异步面试题

194 阅读49分钟

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 的语法糖,用 asyncawait 以同步方式编写异步代码。
  • 示例
    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 异步编程的发展始终围绕 “简化代码结构”“增强可维护性” 展开:

  1. 回调函数 → 2. Promise → 3. Generator → 4. Async/Await

目前,async/await 是主流的异步方案,结合 Promise 的基础能力,能够以最简洁的方式处理大多数异步场景。对于更复杂的异步流(如取消、并发控制),可结合 Promise.allPromise.race 或第三方库(如 RxJS)实现。

JS执行过程中分为哪些阶段

总结

JavaScript 的执行过程可归纳为:

  1. 解析代码 → 生成 AST。
  2. 编译优化 → 生成字节码或机器码。
  3. 创建执行上下文 → 处理作用域、变量提升。
  4. 同步代码执行 → 处理调用栈。
  5. 异步代码处理 → 通过事件循环调度任务队列。
  6. 垃圾回收 → 释放无用内存。

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),包括:

  1. 全局执行上下文:代码首次运行时的默认环境。
  2. 函数执行上下文:每次调用函数时创建。
  3. Eval 执行上下文(较少使用)。

执行上下文的生命周期

  1. 创建阶段
    • 绑定 this 值。
    • 创建词法环境(Lexical Environment):
      • 变量提升(Hoisting)var 变量和函数声明被初始化(值为 undefined 或函数体)。
      • let/const 变量进入“暂时性死区”(TDZ)。
  2. 执行阶段:逐行执行代码,赋值变量、调用函数等。

4. 代码执行(Code Execution)

引擎按顺序执行代码:

  • 同步代码:逐行执行,遇到函数调用时创建新的函数执行上下文。
  • 异步代码:通过事件循环(Event Loop)处理回调(如 setTimeoutPromise)。
    • 调用栈(Call Stack):记录当前执行的函数位置。
    • 任务队列(Task Queue):存储待执行的异步回调(宏任务)。
    • 微任务队列(Microtask Queue):存储优先级更高的回调(如 Promise.then)。

执行顺序

  1. 同步代码 → 2. 所有微任务 → 3. 一个宏任务 → 重复步骤 2-3。

5. 垃圾回收(Garbage Collection)

JavaScript 引擎自动管理内存,通过标记-清除(Mark-and-Sweep)等算法回收不再使用的对象。


关键机制:事件循环(Event Loop)

事件循环是 JavaScript 处理异步的核心:

  1. 主线程执行同步代码,遇到异步任务时交给 Web API(如 setTimeoutfetch)处理。
  2. Web API 完成后,将回调推入任务队列。
  3. 主线程空闲时,事件循环按优先级依次处理队列:
    • 微任务队列(如 Promise.thenMutationObserver)优先于宏任务。
    • 宏任务队列(如 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');
  • 输出顺序:

    1. start 立即打印
    2. 调用 setTimeout,计时开始,回调未入队列
    3. end 立即打印
    4. 1000ms 后,回调进入宏任务队列
    5. 事件循环取出回调执行,打印 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. 事件循环调度

事件循环按以下顺序处理任务队列:

  1. 执行完所有同步代码(调用栈为空)。
  2. 清空微任务队列:依次执行所有微任务(包括微任务中产生的新的微任务)。
  3. 执行一个宏任务:从宏任务队列中取出一个任务执行。
  4. 重复步骤 2-3,直到所有队列为空。

异步执行周期的关键细节

  1. 微任务优先级高于宏任务
    • 每次调用栈清空后,必须先清空所有微任务,再执行一个宏任务。
  2. 微任务会“插队”
    • 如果在宏任务执行过程中产生新的微任务,这些微任务会在当前宏任务完成后立即执行
  3. 任务队列的类型
    • 宏任务队列可能有多个(如浏览器中的定时器队列、IO 队列、渲染队列等),但每次事件循环只处理一个宏任务。
    • 微任务队列只有一个,必须完全清空。
  4. 渲染时机
    • 在浏览器中,页面渲染(UI 更新)发生在宏任务之间 requestAnimateFrame是宏任务

Node.js 的特殊情况

Node.js 的事件循环分为多个阶段(如 timerspollcheck),每个阶段处理不同类型的宏任务:

  • timers 阶段:处理 setTimeoutsetInterval
  • poll 阶段:处理 I/O 回调。
  • check 阶段:处理 setImmediate 回调。
  • 微任务:在阶段切换之间执行。

总结:异步执行周期

  1. 同步代码 → 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 有明确的三种状态(pendingfulfilledrejected)。
    • 不可逆性:状态一旦从 pending 变为 fulfilledrejected,不可逆转。
    • 链式调用:通过 .then().catch() 实现链式操作,避免回调地狱。
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);    // 统一处理所有错误
链式调用的规则
  1. 每个 .then() 返回一个新的 Promise
  2. 如果回调函数返回一个值,新 Promise 会用该值 resolve
  3. 如果回调函数抛出错误,新 Promise 会 reject
  4. 如果回调函数返回一个 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 的底层原理

  1. 状态管理:维护 pendingfulfilledrejected 状态。
  2. 回调队列:在 pending 状态时收集回调函数,状态变更后触发。
  3. 链式调用:每个 .then() 返回新 Promise,处理返回值可能是 Promise 的情况。
  4. then return出去的值,会被后面的then接收,如果后面还有跟then的话,catch同理
  5. promise不管返回什么值,都会被包装成一个promise对象,即使这个返回值是error
  6. then接收到的值,如果不是一个函数,会穿透到后面的then
  7. 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 的最佳实践

  1. 避免嵌套:始终返回 Promise 以保持链式调用。
  2. 错误处理:每个链式调用末尾添加 .catch()
  3. 合理使用 async/await:结合 async/await 编写更同步化的代码。
  4. 避免冗余 Promise:已有 Promise 时无需再包装。

八、Promise 的局限性

  1. 无法取消:一旦创建 Promise,无法中途取消。
  2. 单一结果:一个 Promise 只能表示一个异步操作的最终结果
  3. 错误吞噬:未处理的 Promise 错误可能导致静默失败(可通过 unhandledrejection 事件监听)。

  • 混淆 Promise.race() 和 Promise.any()

    • 纠正Promise.race() 只要有任何一个 Promise 完成(fulfilled 或 rejected)就会立即结束。Promise.any() (ES2021) 则是只要有一个 Promise fulfilled 就会结束,如果所有 Promise 都 rejected 才会 rejected
  • 性能问题: 并行执行虽然快,但也要考虑服务器并发处理能力,避免过度请求导致服务过载。对于大量并发请求,可以考虑分批处理 (batching)。

总结

Promise 是 JavaScript 异步编程的基石,它通过状态管理、链式调用和统一的错误处理,显著提升了代码的可读性和可维护性。理解其核心机制(如微任务、事件循环)和常见用法(如 Promise.all、错误捕获),是掌握现代 JavaScript 异步编程的关键。结合 async/await 语法,可以进一步简化异步代码的编写。

Promise 使用场景

  1. 异步操作:处理需要时间的异步操作,如网络请求等。
  2. 并发控制:使用Promise.all()来并行处理多个异步操作,并等待它们全部完成。
  3. 串行任务:按顺序执行一系列依赖前一个结果的异步操作。
  4. 错误处理:集中处理异步操作中的错误。
  5. 定时器:使用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 状态时的错误情况。以下是它们之间的区别和使用方式:

  1. then 方法的第二个参数

    • 当 Promise 被拒绝(rejected)时,then 方法的第二个参数,即错误处理函数,会被调用。
    • 这个错误处理函数接收一个参数,即 Promise 被拒绝时返回的错误信息。
    • 需要在每次调用 then 方法时提供第二个参数,以便处理可能出现的错误。
  2. 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"

实现要点

  1. 状态管理
    • pendingfulfilledpendingrejected,状态不可逆。
  2. 异步处理
    • 使用 setTimeout 模拟微任务队列(实际 Promise 使用微任务队列)。
  3. 链式调用
    • then 返回新 Promise,支持链式调用。
    • 处理返回值可能是 Promise 的情况(递归解析)。
  4. 错误处理
    • 捕获执行器函数和回调中的错误。
    • catchthen(null, onRejected) 的语法糖
  5. 值穿透
    • 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


与原生的差异

  1. 微任务队列
    • 原生 Promise 使用微任务队列(如 queueMicrotaskMutationObserver),此处用 setTimeout(宏任务)简化。
  2. 边缘情况
    • 未完全覆盖所有 Promise/A+ 规范细节(如 thenable 对象的深度处理)。
  3. 性能优化
    • 未实现真正的微任务调度和引擎级优化。

扩展建议

  1. 实现 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)
          );
        });
      });
    }
    
  2. 实现 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 ⭐️⭐️⭐️⭐

对比总结

特性CallbackPromiseGeneratorasync/await
代码结构嵌套回调链式调用生成器函数 + yield同步风格 + await
错误处理手动传递错误.catch()try/catch + 执行器try/catch
控制流自行管理链式控制可手动控制自动控制
可读性低(回调地狱)
异步任务管理复杂(需外部库)支持 all/race依赖执行器结合 Promise 方法
适用场景简单异步操作复杂链式异步特殊流程控制现代异步编程

演进关系

  1. Callback → Promise:解决回调地狱,统一错误处理
  2. Promise → Generator尝试用同步风格写异步代码,但依赖执行器
  3. Generator → async/await语法糖封装,成为终极异步方案

如何选择?

  • 简单场景:Callback(如事件监听)。
  • 链式异步:Promise(如多个接口顺序调用)。
  • 复杂异步逻辑:async/await(如循环 + 条件判断)。
  • 特殊控制需求:Generator(如自定义流程调度)。

现代开发中,async/await 是主流方案,但需结合 Promise 的基础能力

promise 有几种状态, Promise 有什么特色和优缺点 x4 ⭐️⭐️

等待中(pending) 完成了 (resolved) 拒绝了(rejected)

一、核心特色

  1. 链式调用:替代回调嵌套(回调地狱),用 then/catch/finally 链式调用,逻辑更线性清晰;
  2. 状态不可逆:只有 3 种状态(pending/fulfilled/rejected),一旦从 pending 转为成功 / 失败,状态永久固定,避免重复触发;
  3. 统一错误处理then 可省略回调,直接透传结果;错误会沿链式自动冒泡到最近的 catch,统一捕获异常;

二、优点

  1. 解决 “回调地狱”:嵌套逻辑扁平化,可读性、可维护性大幅提升;
  2. 错误处理统一:相比回调的 “错误优先回调”(err, res),catch 可集中捕获链式中所有错误,更简洁;
  3. 支持并行 / 串行控制:配合 Promise.all/Promise.race/Promise.allSettled 等静态方法,轻松处理多异步任务协作;
  4. 与现代语法兼容:是 async/await 的基础,可无缝衔接,写出 “类同步” 的异步代码。

三、缺点

  1. 无法取消:一旦创建 Promise 并执行,无法中途取消(需手动封装取消逻辑);
  2. 状态不透明:外部无法直接查看 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会注意哪些东西 ⭐️⭐️

特点

  1. await只能放到async函数中
  2. 相比genrator语义化更强
  3. await后面可以是promise对象,也可以数字、字符串、布尔
  4. async函数总是返回是一个promise对象
  5. 只要await语句后面Promise状态变成 reject, 那么整个async函数会中断执行
  6. 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 函数,再通过一个 “自动执行器” 驱动其完成异步流程。

先铺垫两个关键前提:

  1. Generator 函数:ES6 特性,用 function* 定义,内部用 yield 暂停执行,返回迭代器(需手动调用 next() 恢复执行);
  2. 异步流程需要 “自动执行”: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 执行

  1. await xxx → 编译为 yield Promise.resolve(xxx)(哪怕 xxx 不是 Promise,也会被包装);
  2. 自动执行器等待 yield 后的 Promise 完成,拿到结果后调用 iterator.next(结果),让 Generator 恢复执行;
  3. 重复步骤 2,直到 Generator 执行完毕(done: true),自动执行器返回的 Promise 决议为最终结果;
  4. 若过程中 Promise 失败或抛出错误,自动执行器会 reject 错误,可通过 try/catch 捕获(对应 async 函数内的 try/catch)。

三、关键特性(对应原理的延伸)

  1. await 只能在 async 函数内使用:因为 await 依赖 Generator 的 yield 暂停逻辑,而普通函数没有迭代器机制,无法实现 “暂停 - 恢复”;
  2. async 函数返回值必然是 Promise:这是自动执行器的设计决定的(autoRun 直接返回 Promise);
  3. 错误捕获await 对应的 Promise 失败时,会触发自动执行器的 reject,可通过 try/catch 捕获(替代 Promise 的 catch 回调);
  4. 执行顺序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 后,再返回暂存结果或抛出暂存错误。

简单拆解内部机制:

  1. 执行流程重排(关键) 当 try 里有 return 时,JS 不会直接返回,而是分 3 步走:

    • 第一步:执行 try 块内代码,遇到 return 时,先计算 return 后面的表达式值(比如 return 1+2 会先算出 3),并把这个结果 “暂存” 起来
    • 第二步:暂停 try 的返回逻辑,转去执行 finally 块内的所有代码(无论 finally 里有没有逻辑,都会执行);
    • 第三步:finally 执行完后,再把第一步暂存的结果返回出去(完成 try 的 return 逻辑)。
  2. 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
  1. 本质:finally 的 “强制执行” 语义JS 引擎在编译时,会把 finally 块的代码 “插入” 到 try/catch 的所有退出路径(return、throw、break 等)之前,确保无论如何都能执行。它的优先级高于 try/catch 的退出逻辑,这是语言层面的设计规则,目的是让 finally 承担 “清理资源”(比如关闭文件、取消请求)的职责 —— 这些操作无论代码是否正常执行,都必须完成。

总结:try 里的 return 会 “先算结果、暂存起来”,等 finally 执行完再返回;finally 的执行是 JS 引擎强制保证的,不受 try 里的 return 影响,核心是为了满足 “必须执行清理逻辑” 的语义。

Node 与浏览器 EventLoop 的差异

核心差异:宏任务队列的分类与执行顺序不同(微任务队列逻辑一致),Node 有多个宏任务队列且按优先级执行,浏览器只有一个宏任务队列按 “先进先出” 执行,具体简单总结:

一、相同点(基础逻辑一致)

  1. 执行顺序核心:同步代码 → 所有微任务(Promise.then/catch/finally、queueMicrotask 等)→ 一轮宏任务 → 所有微任务 → ...(循环);
  2. 微任务队列:两者都只有一个,优先级高于所有宏任务,同一轮微任务会全部执行完。

二、核心差异(宏任务的处理)

维度浏览器 EventLoopNode.js EventLoop(v11+ 后,接近浏览器但仍有差异)
宏任务队列只有 1 个 “全局宏任务队列”(先进先出)分 6 个优先级不同的宏任务队列(如 timers、poll、check 等)
宏任务执行方式每次取 1 个宏任务执行,完后清微任务每次按优先级遍历队列,取当前队列所有任务执行,完后清微任务
典型宏任务优先级无优先级,按添加顺序执行timers(setTimeout/setInterval)> poll(I/O 回调)> check(setImmediate)等

三、关键场景体现差异

  1. setTimeout(0) vs setImmediate(最直观):

    • 浏览器:两者同属一个宏任务队列,按添加顺序执行(谁先写谁先跑);
    • Node:timers 队列优先级高于 check 队列,但因 Node 启动有微小延迟,setTimeout(0) 实际可能比 setImmediate 后执行(多数情况 timers 先跑,偶尔因延迟反转)。
  2. I/O 回调的位置

    • 浏览器:I/O 回调(如 AJAX、DOM 事件)和 setTimeout 同属一个宏任务队列,按顺序执行;
    • Node:I/O 回调属于 poll 队列,优先级介于 timers 和 check 之间,会在 timers 执行完后、check 执行前处理。

四、总结(极简版)

  • 微任务:两者完全一致,优先执行;
  • 宏任务:浏览器 “一锅炖”(先进先出),Node“分队列、按优先级” 执行;
  • 核心记住:Node 有宏任务优先级,浏览器没有,这是最主要差异。

ECMAScript和JavaScript的关系

核心关系:ECMAScript 是 “标准 / 规范”,JavaScript 是 “标准的实现” —— 简单说就是「标准 vs 具体产品」的关系,且 JavaScript 是 ECMAScript 最主流的实现。