【JavaScript 事件循环实战解析】

JavaScript 事件循环实战解析

引言

JavaScript 的事件循环机制是理解异步编程的关键。本文通过实际代码示例和详细解析,帮助你掌握事件循环的工作原理,准确预测代码执行顺序。

事件循环基础

JavaScript 是单线程语言,通过事件循环处理异步操作。事件循环由以下几个关键部分组成:

  1. 调用栈(Call Stack): 执行同步代码
  2. 宏任务队列(Macrotask Queue): 存放 setTimeout 等 API 的回调
  3. 微任务队列(Microtask Queue): 存放 Promise 回调等
  4. 事件循环(Event Loop): 协调以上三者的工作机制

宏任务与微任务

宏任务(Macrotask)包括:

  • 主脚本代码(script)
  • setTimeout/setInterval 回调
  • setImmediate(Node.js 环境)
  • I/O 操作回调
  • UI 渲染(浏览器环境)

微任务(Microtask)包括:

  • Promise.then/catch/finally
  • queueMicrotask()
  • MutationObserver 回调
  • process.nextTick(Node.js 环境)

事件循环执行顺序

事件循环遵循以下规则:

  1. 执行当前宏任务(一开始是主脚本)
  2. 执行所有微任务
  3. 执行下一个宏任务
  4. 重复步骤 2 和 3

代码实例解析

示例 1: 基础 Promise 和 setTimeout

console.log('1 - 开始');

setTimeout(() => {
  console.log('2 - 定时器回调');
}, 0);

Promise.resolve().then(() => {
  console.log('3 - Promise回调');
});

console.log('4 - 结束');

输出顺序:

1 - 开始
4 - 结束
3 - Promise回调
2 - 定时器回调

解析:

  1. 执行主脚本(宏任务): 输出1 - 开始4 - 结束
  2. 遇到 setTimeout,将其回调放入宏任务队列
  3. 遇到 Promise.then,将其回调放入微任务队列
  4. 主脚本执行完毕,检查微任务队列,执行 Promise 回调,输出3 - Promise回调
  5. 执行下一个宏任务(setTimeout 回调),输出2 - 定时器回调

示例 2: Promise 构造函数与回调执行顺序

const promise = new Promise((resolve, reject) => {
  console.log(1);
  setTimeout(() => {
    console.log('timerStart');
    resolve('success');
    console.log('timerEnd');
  }, 0);
  console.log(2);
});

promise.then((res) => {
  console.log(res);
});

console.log(4);

输出顺序:

1
2
4
timerStart
timerEnd
success

解析:

  1. 宏任务 1(主脚本):

    • 执行 Promise 构造函数的回调(同步执行)
    • 输出1
    • 设置 setTimeout(创建一个新的宏任务)
    • 输出2
    • 注册 promise.then 回调(但不执行)
    • 输出4
  2. 宏任务 2(setTimeout 回调):

    • 输出timerStart
    • 调用 resolve(“success”) (此时将 promise.then 回调加入微任务队列)
    • 输出timerEnd
  3. 微任务队列(在宏任务 2 之后执行):

    • 执行 promise.then 回调
    • 输出success

示例 3: 嵌套 Promise 和 setTimeout

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

Promise.resolve()
  .then(function () {
    console.log('promise1');
  })
  .then(function () {
    console.log('promise2');
  });

console.log('script end');

输出顺序:

script start
script end
promise1
promise2
setTimeout

解析:

  1. 执行主脚本: 输出script startscript end
  2. 将 setTimeout 回调放入宏任务队列
  3. 将第一个 Promise.then 回调放入微任务队列
  4. 主脚本执行完毕,检查微任务队列:
    • 执行第一个 then 回调,输出promise1
    • 第一个 then 返回 undefined,触发第二个 then,将其回调放入微任务队列
    • 继续检查微任务队列,执行第二个 then 回调,输出promise2
  5. 微任务队列清空,执行下一个宏任务(setTimeout 回调),输出setTimeout

示例 4: async/await 执行顺序

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');
async1();
console.log('script end');

输出顺序:

script start
async1 start
async2
script end
async1 end

解析:

  1. 执行主脚本: 输出script start
  2. 调用 async1():
    • 输出async1 start
    • 调用 async2(),输出async2
    • await 会暂停 async1 函数的执行,将后续代码作为微任务
  3. 继续执行主脚本: 输出script end
  4. 主脚本执行完毕,检查微任务队列:
    • 执行 await 后的代码,输出async1 end

任务划分的正确思考方式

在分析事件循环时,我们应该以"回调任务"为单位思考,而不是简单地看单行代码。一个回调函数内的所有同步代码会作为一个整体执行,不会被其他任务打断。

示例 5: Promise 内部的 resolve

const p = new Promise((resolve) => {
  console.log('Promise开始');
  resolve('完成');
  console.log('Promise结束'); // 这行仍会在then回调之前执行
});

p.then((result) => {
  console.log('Then:', result);
});

console.log('主脚本结束');

输出顺序:

Promise开始
Promise结束
主脚本结束
Then: 完成

解析: 即使resolve()console.log('Promise结束')之前调用,Promise 的 then 回调也不会立即执行,而是要等到当前同步代码执行完毕后才会作为微任务执行。

示例 6: setTimeout 内部的 Promise

setTimeout(() => {
  console.log('定时器开始');
  Promise.resolve().then(() => {
    console.log('定时器内的Promise');
  });
  console.log('定时器结束');
}, 0);

Promise.resolve().then(() => {
  console.log('外部Promise');
});

console.log('主脚本结束');

输出顺序:

主脚本结束
外部Promise
定时器开始
定时器结束
定时器内的Promise

解析:

  1. 主脚本执行,输出主脚本结束
  2. 检查微任务队列,执行外部 Promise 回调,输出外部Promise
  3. 执行 setTimeout 回调(宏任务):
    • 输出定时器开始
    • 将 Promise.then 回调放入微任务队列
    • 输出定时器结束
  4. setTimeout 回调执行完毕,检查微任务队列,执行 Promise 回调,输出定时器内的Promise

async/await 的执行机制

async/await 是 Promise 的语法糖,它的本质仍然是基于 Promise 的微任务机制。

示例 7: 连续 await 的执行顺序

async function test() {
  console.log(1);
  await Promise.resolve();
  console.log(2);
  await Promise.resolve();
  console.log(3);
}

test();
console.log(4);

输出顺序:

1
4
2
3

解析:

  1. 调用 test(),输出1
  2. 遇到第一个 await,将后续代码(console.log(2))作为微任务
  3. 继续执行主脚本,输出4
  4. 主脚本执行完毕,检查微任务队列:
    • 执行第一个 await 后的代码,输出2
    • 遇到第二个 await,将后续代码(console.log(3))作为新的微任务
    • 继续检查微任务队列,执行第二个 await 后的代码,输出3

示例 8: await 与 Promise.then 的等价性

// 使用async/await
async function asyncFunc() {
  console.log('async开始');
  const result = await Promise.resolve('await结果');
  console.log(result);
  console.log('async结束');
}

// 等价的Promise写法
function promiseFunc() {
  console.log('promise开始');
  return Promise.resolve('then结果').then((result) => {
    console.log(result);
    console.log('promise结束');
  });
}

asyncFunc();
promiseFunc();

这两个函数的行为是等价的,都会将 await/then 后的代码作为微任务执行。

示例 9: Promise 错误处理与链式调用

const promise = new Promise((resolve, reject) => {
  reject('error');
  resolve('success2');
});

promise
  .then((res) => {
    console.log('then1: ', res);
  })
  .then((res) => {
    console.log('then2: ', res);
  })
  .catch((err) => {
    console.log('catch: ', err);
  })
  .then((res) => {
    console.log('then3: ', res);
  });

输出顺序:

catch:  error
then3:  undefined

解析:

  1. Promise 状态确定: Promise 构造函数中先调用 reject("error"),Promise 状态变为 rejected,后续的 resolve("success2") 不会执行(Promise 状态一旦确定就不能改变)

  2. 链式调用执行流程:

    • 第一个 .then() 被跳过,因为 Promise 是 rejected 状态
    • 第二个 .then() 也被跳过,继续寻找错误处理器
    • .catch() 捕获错误,输出 catch: error
    • 关键点: .catch() 处理错误后返回一个 resolved 状态的 Promise(返回值为 undefined)
    • 最后的 .then() 接收到 resolved 状态的 Promise,执行并输出 then3: undefined
  3. Promise 链式调用规则:

    • .catch() 成功处理错误后,会返回一个 resolved 状态的 Promise
    • 如果 .catch() 没有显式返回值,默认返回 undefined
    • 后续的 .then() 会正常执行,因为错误已被处理

重要概念:

  • 错误恢复: .catch() 不仅用于捕获错误,还用于从错误中恢复,让 Promise 链继续执行
  • 状态转换: .catch() 成功执行后,Promise 链的状态从 rejected 转为 resolved
  • 链式继续: 这就是为什么 .catch() 后面的 .then() 仍然可以执行的原因

示例 10: Promise 多次调用 then

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('timer');
    resolve('success');
  }, 1000);
});

const start = Date.now();

promise.then((res) => {
  console.log(res, Date.now() - start);
});

promise.then((res) => {
  console.log(res, Date.now() - start);
});

输出顺序 (约 1 秒后):

timer
success 1001
success 1002

解析:

  1. Promise 状态共享:

    • 同一个 Promise 实例可以被多次调用 .then()
    • 所有的 .then() 回调都会在 Promise resolve 时执行
    • 它们共享同一个 Promise 的状态和结果值
  2. 执行时机:

    • 1 秒后 setTimeout 回调执行,输出 timer
    • 调用 resolve('success'),Promise 状态变为 resolved
    • 两个 .then() 回调都被加入微任务队列
    • 按注册顺序依次执行两个 .then() 回调
  3. 时间差异:

    • 两个 .then() 回调几乎同时执行(在同一个微任务队列中)
    • 时间差通常只有 1-2 毫秒,这是由于代码执行的微小时间差

与链式调用的区别:

// 链式调用 - 每个then返回新的Promise
promise
  .then((res) => {
    console.log('第一个then:', res);
    return res; // 返回值传递给下一个then
  })
  .then((res) => {
    console.log('第二个then:', res);
  });

// 多次调用 - 都基于同一个Promise
promise.then((res) => {
  console.log('独立then1:', res);
});

promise.then((res) => {
  console.log('独立then2:', res);
});

重要概念:

  • 独立回调: 多次调用 .then() 创建的是独立的回调,不是链式关系
  • 并行执行: 这些回调会在同一个微任务队列中并行执行
  • 共享结果: 所有回调都接收到相同的 resolve 值
  • 执行顺序: 按照 .then() 注册的顺序执行

示例 11: return Error 与 throw Error 的区别

Promise.resolve()
  .then(() => {
    return new Error('error!!!');
  })
  .then((res) => {
    console.log('then: ', res);
  })
  .catch((err) => {
    console.log('catch: ', err);
  });

输出顺序:

then:  Error: error!!!
    at :2:10
    at 

解析:

这是一个常见的误解!开发者经常认为返回 Error 对象会触发 .catch(),但实际上:

  1. return vs throw 的区别:

    • return new Error() - 返回一个 Error 对象作为正常值,Promise 状态为 resolved
    • throw new Error() - 抛出异常,Promise 状态为 rejected
  2. 执行流程:

    • 第一个 .then() 返回 new Error('error!!!')
    • 这个 Error 对象被当作普通返回值传递给下一个 .then()
    • 第二个 .then() 正常执行,输出 Error 对象
    • .catch() 不会执行,因为没有异常发生

对比示例:

// 情况1: return Error对象 - 不会触发catch
Promise.resolve()
  .then(() => {
    return new Error('这只是一个普通对象');
  })
  .then((res) => {
    console.log('then执行:', res.message); // 输出: then执行: 这只是一个普通对象
  })
  .catch((err) => {
    console.log('catch不会执行');
  });

// 情况2: throw Error对象 - 会触发catch
Promise.resolve()
  .then(() => {
    throw new Error('这是真正的异常');
  })
  .then((res) => {
    console.log('then不会执行');
  })
  .catch((err) => {
    console.log('catch执行:', err.message); // 输出: catch执行: 这是真正的异常
  });

// 情况3: return Promise.reject() - 也会触发catch
Promise.resolve()
  .then(() => {
    return Promise.reject(new Error('通过Promise.reject抛出'));
  })
  .then((res) => {
    console.log('then不会执行');
  })
  .catch((err) => {
    console.log('catch执行:', err.message); // 输出: catch执行: 通过Promise.reject抛出
  });

关键理解:

  • Error 对象本身不是异常: new Error() 只是创建了一个 Error 类型的对象
  • 异常需要被抛出: 只有通过 throwPromise.reject() 或其他异常机制才能改变 Promise 状态
  • 返回值都是正常值: 在 .then()return 任何值(包括 Error 对象)都会被当作正常的 resolved 值
  • 常见误导: 这种写法容易误导开发者,实际项目中应该明确使用 throw 来抛出异常

最佳实践:

// ❌ 错误写法 - 容易误解
.then(() => {
  if (someCondition) {
    return new Error('出错了'); // 这不会触发catch
  }
})

// ✅ 正确写法 - 明确抛出异常
.then(() => {
  if (someCondition) {
    throw new Error('出错了'); // 这会触发catch
  }
})

示例 12: Promise 值透传机制

Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log);

输出顺序:

1

解析:

这个示例展示了 Promise 的值透传机制:

  1. 非函数参数的处理:

    • 第一个 .then(2) - 传入数字 2,不是函数
    • 第二个 .then(Promise.resolve(3)) - 传入 Promise 对象,不是函数
    • 由于参数不是函数,发生值透传
  2. 值透传规则:

    • .then().catch() 的参数期望是函数
    • 传入非函数时,会忽略该参数,直接将上一个 Promise 的值传递给下一个 .then()
    • Promise.resolve(1) 的值 1 直接透传到最后一个 .then(console.log)
  3. 等价写法:

// 原代码等价于:
Promise.resolve(1)
  .then((value) => value) // 透传
  .then((value) => value) // 透传
  .then(console.log); // 输出: 1

对比示例:

// ❌ 值透传 - 非函数参数被忽略
Promise.resolve(1).then(2).then(3).then(console.log); // 输出: 1

// ✅ 正常处理 - 函数参数正常执行
Promise.resolve(1)
  .then((value) => 2)
  .then((value) => 3)
  .then(console.log); // 输出: 3

// ❌ 常见错误 - 想要返回新值但写法错误
Promise.resolve(1)
  .then(Promise.resolve(2)) // 这不会返回2,而是透传1
  .then(console.log); // 输出: 1

// ✅ 正确写法 - 返回新的Promise
Promise.resolve(1)
  .then(() => Promise.resolve(2))
  .then(console.log); // 输出: 2

示例 13: .then() 双参数与 .catch() 的执行优先级

Promise.resolve()
  .then(
    function success(res) {
      throw new Error('error!!!');
    },
    function fail1(err) {
      console.log('fail1', err);
    }
  )
  .catch(function fail2(err) {
    console.log('fail2', err);
  });

输出顺序:

fail2 Error: error!!!
    at success (:3:11)
    at 

解析:

这个示例展示了 .then() 双参数处理与 .catch() 的重要区别:

  1. 执行流程分析:

    • Promise.resolve() 创建一个 resolved 状态的 Promise
    • 调用 .then() 的第一个参数 success 函数
    • success 函数中抛出异常 throw new Error('error!!!')
    • 关键点: .then() 的第二个参数 fail1 不会执行
    • 异常被后续的 .catch() 捕获,执行 fail2 函数
  2. 为什么 fail1 不执行:

    • .then(onFulfilled, onRejected) 中的 onRejected 只处理当前 Promise 的 rejected 状态
    • 当前 Promise 是 Promise.resolve(),状态为 resolved
    • success 函数执行时抛出的异常会创建一个新的 rejected Promise
    • 这个新的 rejected Promise 需要由下一个错误处理器来捕获
  3. Promise 链的状态传递:

Promise.resolve() // Promise A: resolved
  .then(
    // 返回 Promise B
    function success(res) {
      throw new Error('error!!!'); // Promise B 变为 rejected
    },
    function fail1(err) {
      // 只能处理 Promise A 的 rejected 状态
      console.log('fail1', err);
    }
  )
  .catch(function fail2(err) {
    // 处理 Promise B 的 rejected 状态
    console.log('fail2', err);
  });

对比示例:

// 情况1: 初始Promise就是rejected - fail1会执行
Promise.reject(new Error('初始错误'))
  .then(
    function success(res) {
      console.log('success不会执行');
    },
    function fail1(err) {
      console.log('fail1捕获:', err.message); // 输出: fail1捕获: 初始错误
    }
  )
  .catch(function fail2(err) {
    console.log('fail2不会执行');
  });

// 情况2: success函数中抛出异常 - fail2会执行
Promise.resolve()
  .then(
    function success(res) {
      throw new Error('success中的错误');
    },
    function fail1(err) {
      console.log('fail1不会执行');
    }
  )
  .catch(function fail2(err) {
    console.log('fail2捕获:', err.message); // 输出: fail2捕获: success中的错误
  });

// 情况3: fail1中抛出异常 - fail2会执行
Promise.reject(new Error('初始错误'))
  .then(
    function success(res) {
      console.log('success不会执行');
    },
    function fail1(err) {
      console.log('fail1执行:', err.message); // 输出: fail1执行: 初始错误
      throw new Error('fail1中的新错误');
    }
  )
  .catch(function fail2(err) {
    console.log('fail2捕获:', err.message); // 输出: fail2捕获: fail1中的新错误
  });

核心理解:

  • .then() 的第二个参数:只能捕获当前 Promise 的 rejected 状态
  • .catch() 方法:能捕获前面任何环节产生的 rejected 状态
  • 异常传播:在 Promise 链中,异常会向后传播直到被捕获
  • 状态隔离:每个 .then() 都会返回新的 Promise,状态独立管理

最佳实践:

// ❌ 容易混淆的写法
promise.then(success, fail1).catch(fail2);

// ✅ 推荐的清晰写法
promise
  .then(success)
  .catch(fail1)  // 专门处理错误
  .then(...)     // 继续后续处理

示例 14: .finally() 方法的执行机制

Promise.resolve('1')
  .then((res) => {
    console.log(res);
  })
  .finally(() => {
    console.log('finally');
  });

Promise.resolve('2')
  .finally(() => {
    console.log('finally2');
    return '我是finally2返回的值';
  })
  .then((res) => {
    console.log('finally2后面的then函数', res);
  });

输出顺序:

1
finally2
finally
finally2后面的then函数 2

解析:

这个示例展示了 .finally() 方法的特殊执行机制:

  1. 链式调用的微任务执行机制:

    关键理解: 链式调用后面的内容需要等前一个调用执行完才会执行,不是所有回调都会在初始阶段就加入微任务队列。

    初始阶段:

    • Promise.resolve('1') 创建 resolved 状态的 Promise
    • 第一个 .then() 回调加入微任务队列:[then1]
    • Promise.resolve('2') 创建 resolved 状态的 Promise
    • 第二个 .finally() 回调加入微任务队列:[then1, finally2]
    • 注意: 此时第一个 .finally() 和第二个 .then() 还没有加入队列,因为它们要等前面的微任务执行完

    微任务队列执行过程:

    • 第 1 轮: 执行 then1 → 输出 1 → 执行完后将第一个 .finally() 加入队列 → 微任务队列:[finally2, finally1]
    • 第 2 轮: 执行 finally2 → 输出 finally2 → 执行完后将第二个 .then() 加入队列 → 微任务队列:[finally1, then2]
    • 第 3 轮: 执行 finally1 → 输出 finally → 微任务队列:[then2]
    • 第 4 轮: 执行 then2 → 输出 finally2后面的then函数 2 → 微任务队列:[]

    执行顺序: then1finally2finally1then2

    核心原理:

    • 链式延迟: .then().finally() 等方法返回新的 Promise,链式后面的回调要等当前回调执行完才会加入微任务队列
    • 逐步加入: 不是一次性将所有回调加入队列,而是执行一个,再加入下一个
    • 队列动态变化: 微任务队列在执行过程中会动态添加新的微任务
  2. finally() 的关键特性:

    • 值透传: .finally() 不会改变 Promise 的值,即使有 return 语句
    • 状态透传: .finally() 不会改变 Promise 的状态(resolved/rejected)
    • 总是执行: 无论 Promise 是 resolved 还是 rejected,.finally() 都会执行
  3. 返回值处理:

    • .finally() 中的 return '我是finally2返回的值' 被忽略
    • 原始值 '2' 继续传递给后续的 .then()
    • 这就是为什么输出 finally2后面的then函数 2 而不是 finally2后面的then函数 我是finally2返回的值

详细对比示例:

// 示例1: finally不会改变Promise的值
Promise.resolve('原始值')
  .finally(() => {
    console.log('finally执行');
    return '尝试改变的值';
  })
  .then((res) => {
    console.log('then接收到:', res); // 输出: then接收到: 原始值
  });

// 示例2: finally在rejected状态下的行为
Promise.reject('错误信息')
  .finally(() => {
    console.log('finally总是执行');
    return '尝试改变的值';
  })
  .catch((err) => {
    console.log('catch接收到:', err); // 输出: catch接收到: 错误信息
  });

// 示例3: finally中抛出异常会改变Promise状态
Promise.resolve('原始值')
  .finally(() => {
    console.log('finally执行');
    throw new Error('finally中的错误');
  })
  .then((res) => {
    console.log('then不会执行');
  })
  .catch((err) => {
    console.log('catch捕获:', err.message); // 输出: catch捕获: finally中的错误
  });

// 示例4: finally返回rejected Promise会改变状态
Promise.resolve('原始值')
  .finally(() => {
    console.log('finally执行');
    return Promise.reject('finally返回的rejected');
  })
  .then((res) => {
    console.log('then不会执行');
  })
  .catch((err) => {
    console.log('catch捕获:', err); // 输出: catch捕获: finally返回的rejected
  });

finally() 的执行规则:

  1. 正常情况: 返回值被忽略,原始 Promise 的值和状态继续传递
  2. 异常情况: 如果 .finally() 中抛出异常或返回 rejected Promise,会改变 Promise 链的状态
  3. 执行时机: 作为微任务在当前宏任务结束后执行
  4. 用途: 通常用于清理资源、关闭连接等无论成功失败都需要执行的操作

实际应用场景:

function fetchData() {
  showLoading(); // 显示加载状态

  return fetch('/api/data')
    .then((response) => response.json())
    .then((data) => {
      console.log('数据获取成功:', data);
      return data;
    })
    .catch((error) => {
      console.error('数据获取失败:', error);
      throw error;
    })
    .finally(() => {
      hideLoading(); // 无论成功失败都隐藏加载状态
    });
}

示例 15: Promise.all() 并行执行机制

// 模拟异步任务
function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`获取用户${id}信息完成`);
      resolve(`用户${id}数据`);
    }, Math.random() * 1000 + 500); // 随机延时500-1500ms
  });
}

function fetchOrders(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`获取用户${userId}订单完成`);
      resolve(`用户${userId}的订单列表`);
    }, Math.random() * 1000 + 300); // 随机延时300-1300ms
  });
}

console.log('开始并行获取数据');

Promise.all([fetchUser(1), fetchUser(2), fetchOrders(1)])
  .then((results) => {
    console.log('所有数据获取完成:', results);
  })
  .catch((error) => {
    console.log('有任务失败:', error);
  });

console.log('Promise.all已启动,继续执行其他代码');

可能的输出顺序 (由于随机延时,每次执行顺序可能不同):

开始并行获取数据
Promise.all已启动,继续执行其他代码
获取用户1订单完成
获取用户1信息完成
获取用户2信息完成
所有数据获取完成: ['用户1数据', '用户2数据', '用户1的订单列表']

解析:

  1. 并行执行: 三个异步任务同时开始执行,不是顺序等待
  2. 结果顺序: 无论哪个任务先完成,结果数组的顺序始终与传入的 Promise 数组顺序一致
  3. 全部完成: 只有当所有 Promise 都 resolve 后,.then()才会执行
  4. 任一失败: 如果任何一个 Promise reject,整个Promise.all()立即 reject

示例 16: Promise.race() 竞速机制

function fetchFromServer1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('服务器1响应');
      resolve('服务器1的数据');
    }, 800);
  });
}

function fetchFromServer2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('服务器2响应');
      resolve('服务器2的数据');
    }, 600);
  });
}

function fetchFromServer3() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('服务器3响应');
      reject('服务器3连接失败');
    }, 400);
  });
}

console.log('开始竞速请求');

Promise.race([fetchFromServer1(), fetchFromServer2(), fetchFromServer3()])
  .then((result) => {
    console.log('最快响应的结果:', result);
  })
  .catch((error) => {
    console.log('最快响应是错误:', error);
  });

console.log('Promise.race已启动');

输出顺序:

开始竞速请求
Promise.race已启动
服务器3响应
最快响应是错误: 服务器3连接失败
服务器2响应
服务器1响应

解析:

  1. 竞速机制: 哪个 Promise 最先改变状态(resolve 或 reject),就以它的结果为准
  2. 其他继续执行: 虽然 race 已经有结果,但其他 Promise 仍会继续执行完成
  3. 结果被抛弃: 除了最快的那个,其他 Promise 的结果都会被忽略
  4. 错误优先: 如果最快的是 reject,整个 race 就会 reject

示例 17: 异步任务的参数传递模式

参数传递问题很重要,以下是实际应用中的几种模式:

// 模式1: 函数返回Promise (推荐)
function fetchUserData(userId, options = {}) {
  return new Promise((resolve, reject) => {
    console.log(`开始获取用户${userId}的数据,选项:`, options);
    setTimeout(() => {
      if (userId > 0) {
        resolve({
          id: userId,
          name: `用户${userId}`,
          ...options
        });
      } else {
        reject(`无效的用户ID: ${userId}`);
      }
    }, 500);
  });
}

// 模式2: 立即执行的Promise (不推荐用于Promise.all)
const immediatePromise1 = new Promise((resolve) => {
  setTimeout(() => resolve('立即创建的Promise1'), 300);
});

const immediatePromise2 = new Promise((resolve) => {
  setTimeout(() => resolve('立即创建的Promise2'), 400);
});

// 使用Promise.all的正确方式
console.log('=== 模式1: 函数返回Promise ===');
Promise.all([
  fetchUserData(1, { role: 'admin' }),
  fetchUserData(2, { role: 'user' }),
  fetchUserData(3, { role: 'guest' })
]).then((results) => {
  console.log('所有用户数据:', results);
});

// 使用Promise.all的另一种方式
console.log('=== 模式2: 立即执行的Promise ===');
Promise.all([immediatePromise1, immediatePromise2]).then((results) => {
  console.log('立即Promise结果:', results);
});

// 错误示例: 这样无法传递不同参数
console.log('=== 错误示例 ===');
// Promise.all([fetchUserData, fetchUserData, fetchUserData]) // ❌ 这样不行

// 正确的动态创建方式
console.log('=== 动态创建Promise数组 ===');
const userIds = [10, 20, 30];
const userPromises = userIds.map((id) => fetchUserData(id, { source: 'batch' }));

Promise.all(userPromises).then((results) => {
  console.log('批量获取的用户:', results);
});

输出顺序:

=== 模式1: 函数返回Promise ===
开始获取用户1的数据,选项: { role: 'admin' }
开始获取用户2的数据,选项: { role: 'user' }
开始获取用户3的数据,选项: { role: 'guest' }
=== 模式2: 立即执行的Promise ===
=== 错误示例 ===
=== 动态创建Promise数组 ===
开始获取用户10的数据,选项: { source: 'batch' }
开始获取用户20的数据,选项: { source: 'batch' }
开始获取用户30的数据,选项: { source: 'batch' }
立即Promise结果: ['立即创建的Promise1', '立即创建的Promise2']
所有用户数据: [
  { id: 1, name: '用户1', role: 'admin' },
  { id: 2, name: '用户2', role: 'user' },
  { id: 3, name: '用户3', role: 'guest' }
]
批量获取的用户: [
  { id: 10, name: '用户10', source: 'batch' },
  { id: 20, name: '用户20', source: 'batch' },
  { id: 30, name: '用户30', source: 'batch' }
]

参数传递的关键理解:

  1. 函数返回 Promise 模式: 最灵活,可以传递不同参数

    Promise.all([fetchData(param1), fetchData(param2), fetchData(param3)]);
    
  2. 立即执行 Promise: 参数在创建时就固定了

    const promise1 = fetchData(param1); // 立即开始执行
    const promise2 = fetchData(param2); // 立即开始执行
    Promise.all([promise1, promise2]);
    
  3. 动态创建: 使用数组方法批量创建

    const promises = params.map((param) => fetchData(param));
    Promise.all(promises);
    

实际应用场景:

// 场景1: 获取用户详情页所需的所有数据
async function loadUserProfile(userId) {
  try {
    const [userInfo, userPosts, userFriends] = await Promise.all([
      fetchUserInfo(userId),
      fetchUserPosts(userId),
      fetchUserFriends(userId)
    ]);

    return {
      user: userInfo,
      posts: userPosts,
      friends: userFriends
    };
  } catch (error) {
    console.error('加载用户资料失败:', error);
    throw error;
  }
}

// 场景2: 超时控制
function fetchWithTimeout(url, timeout = 5000) {
  const fetchPromise = fetch(url);
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('请求超时')), timeout);
  });

  return Promise.race([fetchPromise, timeoutPromise]);
}

示例 18: async/await 的陷阱 - 未 resolve 的 Promise

async function async1() {
  console.log('async1 start');
  await new Promise((resolve) => {
    console.log('promise1');
    // 注意:这里没有调用resolve()!
  });
  console.log('async1 success');
  return 'async1 end';
}

console.log('script start');
async1().then((res) => console.log(res));
console.log('script end');

输出顺序:

script start
async1 start
promise1
script end

解析:

这是一道经典的陷阱题,开发者经常会错误地认为会输出更多内容。

  1. 执行流程分析:

    • 执行主脚本,输出 script start
    • 调用 async1() 函数
    • async1 中输出 async1 start
    • 遇到 await new Promise(...),Promise 构造函数同步执行,输出 promise1
    • 关键点: Promise 构造函数中没有调用 resolve(),Promise 永远处于 pending 状态
    • await 会一直等待这个 Promise resolve,但它永远不会 resolve
    • 继续执行主脚本,输出 script end
  2. 为什么后续代码不执行:

    • console.log('async1 success') 永远不会执行,因为 await 在等待一个永远不会 resolve 的 Promise
    • return 'async1 end' 永远不会执行
    • async1().then(res => console.log(res)) 中的 then 回调永远不会执行,因为 async1 函数永远不会返回
  3. 常见误解:

    开发者可能认为会输出:

    script start
    async1 start
    promise1
    script end
    async1 success  // ❌ 实际不会输出
    async1 end      // ❌ 实际不会输出
    

对比正确的写法:

// 正确写法1: 调用resolve
async function async1() {
  console.log('async1 start');
  await new Promise((resolve) => {
    console.log('promise1');
    resolve(); // 添加这行
  });
  console.log('async1 success');
  return 'async1 end';
}

// 正确写法2: 使用Promise.resolve()
async function async1() {
  console.log('async1 start');
  await Promise.resolve().then(() => {
    console.log('promise1');
  });
  console.log('async1 success');
  return 'async1 end';
}

// 正确写法3: 直接await一个值
async function async1() {
  console.log('async1 start');
  console.log('promise1');
  await Promise.resolve(); // 或者 await undefined;
  console.log('async1 success');
  return 'async1 end';
}

正确写法的输出:

script start
async1 start
promise1
script end
async1 success
async1 end

关键学习点:

  1. Promise 状态管理: Promise 构造函数中必须调用 resolve 或 reject 来改变 Promise 状态
  2. await 的阻塞性: await 会暂停 async 函数的执行,直到 Promise 状态改变
  3. pending 状态的危险: 永远处于 pending 状态的 Promise 会导致代码永远等待
  4. 调试技巧: 遇到 async/await 不按预期执行时,检查 Promise 是否正确 resolve/reject

实际开发中的类似陷阱:

// 陷阱1: 忘记在条件分支中resolve
async function fetchData(shouldSucceed) {
  await new Promise((resolve, reject) => {
    if (shouldSucceed) {
      resolve('success');
    }
    // 如果shouldSucceed为false,既不resolve也不reject
    // 这会导致Promise永远pending
  });
}

// 陷阱2: 异步操作中忘记调用resolve
async function processFile() {
  await new Promise((resolve, reject) => {
    fs.readFile('file.txt', (err, data) => {
      if (err) {
        reject(err);
      } else {
        // 忘记调用resolve(data)
        console.log('文件读取完成');
      }
    });
  });
}

// 正确的写法
async function processFileCorrect() {
  await new Promise((resolve, reject) => {
    fs.readFile('file.txt', (err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data); // 必须调用resolve
      }
    });
  });
}

这个陷阱题很好地说明了 Promise 状态管理的重要性,以及在使用 async/await 时需要确保 Promise 能够正确地 resolve 或 reject。

示例 19: async/await 中的错误处理机制

async function async1() {
  await async2();
  console.log('async1');
  return 'async1 success';
}

async function async2() {
  return new Promise((resolve, reject) => {
    console.log('async2');
    reject('error');
  });
}

async1().then((res) => console.log(res));

输出顺序:

async2

解析:

这道题展示了 async/await 中错误处理的关键机制:

  1. 执行流程分析:

    • 调用 async1() 函数
    • async1 中遇到 await async2()
    • 执行 async2() 函数,输出 async2
    • async2() 返回一个 rejected 状态的 Promise
    • 关键点: await 遇到 rejected 的 Promise 会抛出异常
    • 异常导致 async1 函数终止执行,后续代码不会执行
  2. 不会执行的代码:

    • console.log('async1') 不会执行,因为 await 抛出了异常
    • return 'async1 success' 不会执行
    • async1().then(res => console.log(res)) 中的 then 回调不会执行,因为 async1 返回 rejected 状态的 Promise
  3. 错误传播机制:

    • async2() 返回 rejected Promise
    • await async2() 将 rejection 转换为异常抛出
    • async1() 函数因为未捕获的异常而返回 rejected Promise
    • 最终 async1().then() 不会执行,因为 Promise 是 rejected 状态

正确的错误处理方式:

// 方式1: 使用try-catch捕获错误
async function async1() {
  try {
    await async2();
    console.log('async1');
    return 'async1 success';
  } catch (error) {
    console.log('捕获到错误:', error);
    return 'async1 failed';
  }
}

async function async2() {
  return new Promise((resolve, reject) => {
    console.log('async2');
    reject('error');
  });
}

async1().then((res) => console.log(res));

// 输出:
// async2
// 捕获到错误: error
// async1 failed
// 方式2: 在调用处使用catch
async function async1() {
  await async2();
  console.log('async1');
  return 'async1 success';
}

async function async2() {
  return new Promise((resolve, reject) => {
    console.log('async2');
    reject('error');
  });
}

async1()
  .then((res) => console.log(res))
  .catch((err) => console.log('外部捕获错误:', err));

// 输出:
// async2
// 外部捕获错误: error
// 方式3: 使用Promise.resolve包装可能失败的操作
async function async1() {
  const result = await async2().catch((err) => {
    console.log('内联捕获错误:', err);
    return 'default value'; // 返回默认值继续执行
  });

  console.log('async1, result:', result);
  return 'async1 success';
}

async function async2() {
  return new Promise((resolve, reject) => {
    console.log('async2');
    reject('error');
  });
}

async1().then((res) => console.log(res));

// 输出:
// async2
// 内联捕获错误: error
// async1, result: default value
// async1 success

核心理解:

  1. await 的异常转换: await 会将 rejected Promise 转换为抛出的异常
  2. 函数终止: 未捕获的异常会导致 async 函数立即终止执行
  3. 错误传播: async 函数中的异常会使该函数返回 rejected Promise
  4. 错误处理策略:
    • 使用 try-catch 在函数内部处理
    • 使用.catch()在调用处处理
    • 使用内联 catch 进行局部错误处理

实际应用场景:

// 场景1: API调用错误处理
async function fetchUserData(userId) {
  try {
    const user = await fetch(`/api/users/${userId}`);
    const userData = await user.json();
    console.log('用户数据获取成功:', userData);
    return userData;
  } catch (error) {
    console.error('获取用户数据失败:', error);
    return null; // 返回默认值而不是让错误继续传播
  }
}

// 场景2: 多个异步操作的错误处理
async function processMultipleOperations() {
  try {
    const step1 = await operation1();
    const step2 = await operation2(step1);
    const step3 = await operation3(step2);
    return step3;
  } catch (error) {
    console.error('操作流程中断:', error);
    // 可以根据错误类型进行不同的处理
    if (error.code === 'NETWORK_ERROR') {
      return 'network_fallback_result';
    }
    throw error; // 重新抛出其他类型的错误
  }
}

这个示例强调了在 async/await 中正确处理错误的重要性,避免因为未处理的 rejection 导致程序意外终止。

示例 20: try-catch 让 async 函数继续执行

async function async1() {
  try {
    await Promise.reject('error!!!');
  } catch (e) {
    console.log(e);
  }
  console.log('async1');
  return Promise.resolve('async1 success');
}

async1().then((res) => console.log(res));
console.log('script start');

输出顺序:

error!!!
async1
script start
async1 success

解析:

这个示例完美展示了 try-catch 如何让 async 函数在遇到错误后继续执行:

  1. 执行流程分析:

    • 调用 async1() 函数
    • 在 try 块中执行 await Promise.reject('error!!!')
    • Promise.reject 立即返回 rejected 状态的 Promise
    • await 将 rejection 转换为异常并抛出
    • catch 块捕获异常,输出 error!!!
    • 关键点: 错误被捕获后,函数继续执行后续代码
    • 输出 async1
    • 返回 Promise.resolve('async1 success')
    • 继续执行主脚本,输出 script start
    • 最后执行 async1 的 then 回调,输出 async1 success
  2. 与未处理错误的对比:

    // 未处理错误的版本
    async function async1() {
      await Promise.reject('error!!!'); // 这里会终止函数执行
      console.log('async1'); // 不会执行
      return Promise.resolve('async1 success'); // 不会执行
    }
    
    async1().then((res) => console.log(res)); // then不会执行
    console.log('script start');
    
    // 输出只有: script start
    
  3. try-catch 的作用机制:

    • 异常捕获: catch 块捕获 await 抛出的异常
    • 执行恢复: 错误处理完成后,函数继续执行
    • 状态恢复: async 函数不会因为捕获的错误而返回 rejected Promise
    • 正常返回: 函数可以正常返回 resolved Promise
  4. 另一种优雅的错误处理方式 - 链式 catch:

    async function async1() {
      // 方式1: try-catch (上面的例子)
      // try {
      //   await Promise.reject('error!!!')
      // } catch(e) {
      //   console.log(e)
      // }
    
      // 方式2: 直接在Promise后面链式调用catch
      await Promise.reject('error!!!').catch((e) => console.log(e));
    
      console.log('async1');
      return Promise.resolve('async1 success');
    }
    
    async1().then((res) => console.log(res));
    console.log('script start');
    
    // 输出顺序相同:
    // error!!!
    // async1
    // script start
    // async1 success
    

    链式 catch 的优势:

    • 更简洁: 不需要 try-catch 块包装
    • 局部处理: 只处理特定 Promise 的错误
    • 链式风格: 保持 Promise 的链式调用风格
    • 精确控制: 可以为不同的 Promise 设置不同的错误处理

关键理解:

  1. 错误隔离: try-catch 可以将错误限制在特定范围内,不影响后续代码执行
  2. 优雅降级: 可以在捕获错误后提供备选方案或默认值
  3. 批量处理: 在处理多个异步操作时,单个失败不会影响整体流程
  4. 资源管理: 结合 finally 确保资源得到正确清理

这个示例展示了 try-catch 在 async/await 中的强大作用,让错误处理变得更加灵活和可控。

示例 21: 嵌套 Promise 与状态管理的复杂交互

const first = () =>
  new Promise((resolve, reject) => {
    console.log(3);
    let p = new Promise((resolve, reject) => {
      console.log(7);
      setTimeout(() => {
        console.log(5);
        resolve(6);
        console.log(p);
      }, 0);
      resolve(1);
    });
    resolve(2);
    p.then((arg) => {
      console.log(arg);
    });
  });

first().then((arg) => {
  console.log(arg);
});
console.log(4);

输出顺序:

3
7
4
1
2
5
Promise { : 1 }

解析:

这道题是一个综合性的复杂示例,涉及嵌套 Promise、状态管理和事件循环的多重交互:

  1. 同步代码执行阶段:

    • 调用 first() 函数
    • 执行外层 Promise 构造函数,输出 3
    • 创建内层 Promise p,执行其构造函数,输出 7
    • 设置 setTimeout(宏任务)
    • 关键点: 内层 Promise 调用 resolve(1),状态变为 resolved,值为 1
    • 外层 Promise 调用 resolve(2),状态变为 resolved,值为 2
    • 注册 p.then() 回调到微任务队列
    • 注册 first().then() 回调到微任务队列
    • 继续执行主脚本,输出 4
  2. 微任务队列执行:

    • 执行 p.then() 回调,输出 1(内层 Promise 的 resolved 值)
    • 执行 first().then() 回调,输出 2(外层 Promise 的 resolved 值)
  3. 宏任务执行:

    • 执行 setTimeout 回调,输出 5
    • 尝试调用 resolve(6),但 Promise p 已经是 resolved 状态,这次调用无效
    • 输出 Promise p 的状态,显示 Promise { : 1 }

关键理解点:

  1. Promise 状态不可逆:

    • 内层 Promise p 先调用 resolve(1),状态确定为 resolved,值为 1
    • 后续在 setTimeout 中的 resolve(6) 调用无效,不会改变 Promise 状态
  2. 嵌套 Promise 的独立性:

    • 外层 Promise 和内层 Promise p 是两个独立的 Promise 实例
    • 外层 Promise resolve(2),内层 Promise resolve(1)
    • 它们的状态变化互不影响
  3. 执行时机:

    • Promise 构造函数中的代码是同步执行的
    • .then() 回调是异步执行的(微任务)
    • setTimeout 回调是异步执行的(宏任务)
  4. 微任务队列顺序:

    • p.then() 先注册,先执行
    • first().then() 后注册,后执行

详细执行流程:

// 执行顺序分析
console.log(3); // 1. 同步执行
console.log(7); // 2. 同步执行
// resolve(1) - p状态变为resolved
// resolve(2) - first()状态变为resolved
// p.then() 加入微任务队列
// first().then() 加入微任务队列
console.log(4); // 3. 同步执行

// 微任务队列执行
console.log(1); // 4. p.then()回调执行
console.log(2); // 5. first().then()回调执行

// 宏任务队列执行
console.log(5); // 6. setTimeout回调执行
// resolve(6) - 无效,p已经resolved
console.log(p); // 7. 输出Promise状态

常见误解:

开发者可能认为:

  • setTimeout 中的 resolve(6) 会改变 Promise 的值 ❌
  • 输出顺序可能是 3, 7, 4, 2, 1, 5 ❌
  • Promise p 的最终值是 6 ❌

实际情况:

  • Promise 状态一旦确定就不能改变
  • 微任务优先于宏任务执行
  • Promise p 的值始终是 1

实际应用启示:

// 避免在异步回调中重复resolve
function createPromise() {
  return new Promise((resolve, reject) => {
    resolve('immediate value');

    setTimeout(() => {
      resolve('delayed value'); // 这个调用无效
    }, 1000);
  });
}

// 正确的做法:明确Promise的resolve时机
function createPromiseCorrect() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('delayed value'); // 只在需要的时候resolve
    }, 1000);
  });
}

这个示例很好地展示了 Promise 状态管理的复杂性和事件循环中同步/异步代码的执行顺序。

示例 22: 综合性陷阱题 - async/await + Promise 值透传 + setTimeout 时序

const async1 = async () => {
  console.log('async1');
  setTimeout(() => {
    console.log('timer1');
  }, 2000);
  await new Promise((resolve) => {
    console.log('promise1');
  });
  console.log('async1 end');
  return 'async1 success';
};

console.log('script start');
async1().then((res) => console.log(res));
console.log('script end');
Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .catch(4)
  .then((res) => console.log(res));
setTimeout(() => {
  console.log('timer2');
}, 1000);

输出顺序:

script start
async1
promise1
script end
1
timer2
timer1

解析:

这是一道综合性很强的题目,包含了多个重要的 JavaScript 异步编程概念:

  1. 同步代码执行阶段:

    • 输出 script start
    • 调用 async1() 函数
    • async1 中输出 async1
    • 设置 2 秒的 setTimeout(timer1)
    • 遇到 await new Promise(resolve => { console.log('promise1') })
    • 输出 promise1
    • 关键陷阱: Promise 构造函数中没有调用 resolve(),Promise 永远处于 pending 状态
    • await 会一直等待,async1 函数后续代码永远不会执行
    • 继续执行主脚本,输出 script end
  2. Promise 值透传链分析:

    Promise.resolve(1)
      .then(2) // 值透传,2不是函数
      .then(Promise.resolve(3)) // 值透传,Promise.resolve(3)不是函数
      .catch(4) // 不会执行,因为没有错误
      .then((res) => console.log(res)); // 接收到透传的值1
    

    详细分析:

    • Promise.resolve(1) 创建 resolved 状态的 Promise,值为 1
    • .then(2) - 参数 2 不是函数,发生值透传,值 1 继续传递
    • .then(Promise.resolve(3)) - 关键理解: Promise.resolve(3)虽然是 Promise 对象,但它不是函数!.then()期望的是函数参数,所以发生值透传,值 1 继续传递
    • .catch(4) - 没有错误,不执行,值 1 继续传递
    • 最后的 .then() 接收到值 1,输出 1

    常见误解: 很多人认为.then(Promise.resolve(3))会返回值 3,但实际上Promise.resolve(3)不是函数,正确写法应该是.then(() => Promise.resolve(3)).then(() => 3)

  3. 宏任务执行:

    • 1 秒后执行 timer2,输出 timer2
    • 2 秒后执行 timer1,输出 timer1

    重要说明: 虽然 timer1 在代码中先遇到,但 setTimeout 的执行顺序是按延时时间决定的,不是按代码遇到的顺序!timer2 延时 1 秒,timer1 延时 2 秒,所以 timer2 先执行。

不会执行的代码:

  • console.log('async1 end') - 永远不会执行
  • return 'async1 success' - 永远不会执行
  • async1().then(res => console.log(res)) - then 回调永远不会执行

关键知识点:

  1. async/await 陷阱:

    // 陷阱代码
    await new Promise((resolve) => {
      console.log('promise1'); // 只输出,不resolve
    });
    // 后续代码永远不会执行
    
  2. Promise 值透传机制:

    Promise.resolve(1)
      .then(2) // ❌ 参数2不是函数,发生值透传
      .then(Promise.resolve(3)) // ❌ 参数Promise.resolve(3)不是函数,发生值透传
      .then((res) => console.log(res)); // ✅ 接收到透传的原始值1
    

    关键理解:

    • .then()期望接收一个函数作为参数
    • 2显然不是函数
    • Promise.resolve(3)虽然返回 Promise 对象,但它本身不是函数,而是一个 Promise 实例
    • 正确写法应该是:.then(() => Promise.resolve(3)).then(() => 3)
  3. setTimeout 时序:

    // 虽然timer1在代码中先遇到,但延时更长
    setTimeout(() => console.log('timer1'), 2000); // 2秒后执行
    setTimeout(() => console.log('timer2'), 1000); // 1秒后执行
    // 执行顺序: timer2 → timer1 (按延时时间,不是代码顺序)
    

执行时间线:

0ms:    script start, async1, promise1, script end, 1
1000ms: timer2
2000ms: timer1

这道题很好地综合了多个 JavaScript 异步编程的陷阱和机制,是检验对 Promise、async/await 和事件循环理解程度的优秀题目。

示例 23: Promise 状态不可逆 + finally 特性 + 链式调用返回值

const p1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('resolve3');
    console.log('timer1');
  }, 0);
  resolve('resovle1');
  resolve('resolve2');
})
  .then((res) => {
    console.log(res);
    setTimeout(() => {
      console.log(p1);
    }, 1000);
  })
  .finally((res) => {
    console.log('finally', res);
  });

输出顺序:

resovle1
finally undefined
timer1
Promise { : undefined }

解析:

这道题综合考查了多个 Promise 的核心概念:

  1. Promise 状态不可逆性:

    • Promise 构造函数中先调用resolve('resovle1'),Promise 状态立即变为 resolved,值为'resovle1'
    • 后续的resolve('resolve2')调用无效,因为 Promise 状态已经确定
    • setTimeout 中的resolve('resolve3')也无效,Promise 状态不能再次改变
  2. 链式调用的执行流程:

    • 初始 Promise resolve 为'resovle1'
    • .then()回调执行,输出resovle1
    • 关键理解: .then()回调函数没有显式 return,JavaScript 会自动返回undefined
    • 重要: .then()只要正常执行完成(没有抛出异常),就会返回一个resolved 状态的 Promise
    • 即使什么都不做,.then()也会返回Promise.resolve(undefined)
    • .finally()接收到的是.then()返回的 Promise,状态为 resolved,值为undefined
  3. finally 的特殊性质:

    • 参数迷惑性: finally(res => {...})中的res参数是迷惑项!
    • 不接收参数: .finally()的回调函数实际上不接收任何参数,res始终为undefined
    • 状态透传: .finally()不会改变 Promise 的值,会透传上一个 Promise 的结果
    • 返回值: .finally()返回一个新的 Promise,值为上一步的结果(这里是undefined
  4. 变量 p1 的真实含义:

    • p1不是指向最初的 Promise,而是指向整个链式调用的最终结果
    • p1实际上是.finally()返回的 Promise
    • 由于.then()返回undefined.finally()透传这个值,所以p1的值是undefined
  5. 执行时序:

    • 同步代码执行:Promise 构造函数,resolve(‘resovle1’)生效
    • 微任务执行:.then()回调,输出resovle1,设置 1 秒定时器
    • 微任务执行:.finally()回调,输出finally undefined
    • 宏任务执行:setTimeout 回调,输出timer1
    • 宏任务执行:1 秒后的定时器,输出Promise { : undefined }

关键理解点:

  1. .then()的默认返回值机制:

    // 情况1: 没有显式return
    .then(res => {
      console.log(res);
      // 没有return语句,自动返回undefined
    }) // 等价于 .then(res => { console.log(res); return undefined; })
    
    // 情况2: 显式return
    .then(res => {
      console.log(res);
      return 'new value';
    }) // 返回Promise.resolve('new value')
    
    // 情况3: 抛出异常
    .then(res => {
      console.log(res);
      throw new Error('error');
    }) // 返回Promise.reject(new Error('error'))
    
    // 关键理解:只要.then()正常执行完成,就返回resolved状态的Promise
    
  2. finally 参数陷阱:

    // ❌ 错误理解:认为finally能接收Promise的结果
    .finally(res => {
      console.log('finally', res); // res永远是undefined
    })
    
    // ✅ 正确理解:finally不接收参数
    .finally(() => {
      console.log('finally执行,但不知道Promise的结果');
    })
    
  3. finally 透传值的规则:

    // 情况1: .then()没有return,finally透传undefined
    Promise.resolve('initial')
      .then((res) => {
        console.log(res); // 'initial'
        // 没有return,相当于return undefined
      })
      .finally(() => {
        console.log('finally');
      }); // 最终Promise的值: undefined
    
    // 情况2: .then()有return,finally透传return的值
    Promise.resolve('initial')
      .then((res) => {
        console.log(res); // 'initial'
        return 1; // 显式返回1
      })
      .finally(() => {
        console.log('finally');
      }); // 最终Promise的值: 1
    
    // 情况3: finally本身的返回值会被忽略(除非抛异常)
    Promise.resolve('initial')
      .then((res) => {
        return 'from then';
      })
      .finally(() => {
        return 'from finally'; // 这个返回值被忽略
      }); // 最终Promise的值: 'from then'
    
    // 情况4: finally中抛异常会改变Promise状态
    Promise.resolve('initial')
      .then((res) => {
        return 'from then';
      })
      .finally(() => {
        throw new Error('finally error'); // 抛异常会改变状态
      }); // 最终Promise变为rejected状态
    

    finally 透传规则总结:

    • 正常情况: finally 透传上一个 Promise 的值和状态,忽略自己的返回值
    • 异常情况: 如果 finally 中抛出异常或返回 rejected Promise,会改变最终 Promise 的状态
    • 参数问题: finally 回调不接收参数,参数永远是 undefined
  4. Promise 状态控制的两种方式对比:

    // 方式1: 在 new Promise 构造函数中 - 必须显式调用 resolve/reject
    new Promise((resolve, reject) => {
      console.log('执行中...');
      // 必须显式调用 resolve 或 reject,否则 Promise 永远是 pending 状态
      resolve('成功结果'); // 显式 resolve
      // reject('失败原因');  // 显式 reject
    });
    
    // 方式2: 在 .then() 回调中 - 通过 return 或 throw 控制状态
    Promise.resolve('初始值').then((res) => {
      console.log(res);
      // 情况1: return 值 → 自动包装为 Promise.resolve(值)
      return '新值';
    
      // 情况2: 不写 return → 自动 return undefined → Promise.resolve(undefined)
    
      // 情况3: throw 异常 → 自动包装为 Promise.reject(异常)
      // throw new Error('出错了');
    });
    

    关键区别:

    • new Promise: 必须显式调用 resolve()reject(),否则 Promise 永远处于 pending 状态
    • .then() 回调: 通过 return 自动 resolve,通过 throw 自动 reject,不需要显式调用
    • 自动包装: .then() 中的返回值会被自动包装成 Promise 状态
  5. 链式调用的返回值:

    // 原始情况:没有return
    const p1 = Promise.resolve('initial')
      .then((res) => {
        console.log(res); // 'initial'
        setTimeout(() => {
          console.log(p1);
        }, 1000);
        // 没有return,相当于return undefined
      })
      .finally(() => {
        console.log('finally');
      });
    // p1最终值: undefined
    
    // 如果加上return 1:
    const p2 = Promise.resolve('initial')
      .then((res) => {
        console.log(res); // 'initial'
        setTimeout(() => {
          console.log(p2);
        }, 1000);
        return 1; // 显式返回1
      })
      .finally(() => {
        console.log('finally');
      });
    // p2最终值: 1 (finally透传了.then()的返回值)
    
  6. Promise 状态的一次性:

    new Promise((resolve) => {
      resolve('first'); // ✅ 生效
      resolve('second'); // ❌ 无效
      resolve('third'); // ❌ 无效
    });
    

实际应用启示:

// 正确使用finally进行资源清理
function fetchData() {
  let loading = true;

  return fetch('/api/data')
    .then((response) => {
      console.log('请求成功');
      return response.json();
    })
    .catch((error) => {
      console.log('请求失败');
      throw error;
    })
    .finally(() => {
      loading = false; // 无论成功失败都清理状态
      console.log('请求结束,清理loading状态');
      // 注意:这里不要试图访问Promise的结果
    });
}

这道题很好地展示了 Promise 状态管理、finally 特性和链式调用返回值的复杂交互,是理解 Promise 高级特性的优秀示例。

示例 24: Promise 实现定时输出 + setTimeout 回调函数限制

需求: 使用 Promise 实现每隔 1 秒输出 1, 2, 3

// 方法1: 使用 Promise 链式调用
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function printNumbers1() {
  console.log('开始方法1');
  return Promise.resolve()
    .then(() => {
      console.log(1);
      return delay(1000);
    })
    .then(() => {
      console.log(2);
      return delay(1000);
    })
    .then(() => {
      console.log(3);
      console.log('方法1完成');
    });
}

// 方法2: 使用 async/await
async function printNumbers2() {
  console.log('开始方法2');
  console.log(1);
  await delay(1000);
  console.log(2);
  await delay(1000);
  console.log(3);
  console.log('方法2完成');
}

// 方法3: 使用递归 Promise
function printNumbers3() {
  console.log('开始方法3');
  let count = 1;

  function printNext() {
    return new Promise((resolve) => {
      console.log(count);
      count++;

      if (count <= 3) {
        setTimeout(() => {
          printNext().then(resolve);
        }, 1000);
      } else {
        console.log('方法3完成');
        resolve();
      }
    });
  }

  return printNext();
}

// 方法4: 使用 Promise 构造函数
function printNumbers4() {
  console.log('开始方法4');
  return new Promise((resolve) => {
    let count = 1;

    function print() {
      console.log(count);
      count++;

      if (count <= 3) {
        setTimeout(print, 1000);
      } else {
        console.log('方法4完成');
        resolve();
      }
    }

    print();
  });
}

// 方法5: 使用 reduce 实现串行 Promise (最优雅的函数式解法)
function printNumbers5() {
  console.log('开始方法5');
  const arr = [1, 2, 3];

  return arr
    .reduce((p, x) => {
      return p.then(() => {
        return new Promise((r) => {
          setTimeout(() => r(console.log(x)), 1000);
        });
      });
    }, Promise.resolve())
    .then(() => {
      console.log('方法5完成');
    });
}

// 方法6: reduce 的简化版本
function printNumbers6() {
  console.log('开始方法6');
  const arr = [1, 2, 3];

  return arr
    .reduce(
      (p, x) => p.then(() => new Promise((r) => setTimeout(() => r(console.log(x)), 1000))),
      Promise.resolve()
    )
    .then(() => console.log('方法6完成'));
}

// 执行示例
printNumbers5(); // 推荐使用这个优雅的函数式解法

方法 5 和方法 6 的区别:

虽然两个方法实现的功能完全相同,但在代码风格和可读性上有区别:

// 方法5: 多行展开版本 - 更清晰易读
return arr
  .reduce((p, x) => {
    return p.then(() => {
      return new Promise((r) => {
        setTimeout(() => r(console.log(x)), 1000);
      });
    });
  }, Promise.resolve())
  .then(() => {
    console.log('方法5完成');
  });

// 方法6: 单行简化版本 - 更简洁
return arr
  .reduce(
    (p, x) => p.then(() => new Promise((r) => setTimeout(() => r(console.log(x)), 1000))),
    Promise.resolve()
  )
  .then(() => console.log('方法6完成'));

具体区别:

  1. 代码格式:

    • 方法 5: 多行展开,每个步骤都有明确的 return 语句
    • 方法 6: 单行箭头函数,省略了花括号和 return 关键字
  2. 可读性:

    • 方法 5: 结构清晰,适合初学者理解 Promise 链的构建过程
    • 方法 6: 代码紧凑,适合有经验的开发者快速编写
  3. 调试友好性:

    • 方法 5: 可以在每个 return 语句处设置断点,便于调试
    • 方法 6: 调试时需要展开单行代码,稍微不便
  4. 团队协作:

    • 方法 5: 代码审查时更容易理解逻辑流程
    • 方法 6: 代码更简洁,但可能需要团队成员有一定的函数式编程基础

推荐使用场景:

  • 学习阶段: 推荐方法 5,有助于理解 Promise 链的构建
  • 生产环境: 两者都可以,根据团队代码风格指南选择
  • 复杂逻辑: 如果需要在 Promise 中添加更多逻辑,方法 5 更容易扩展

功能上完全等价: 两个方法的执行结果、性能、内存占用都完全相同,只是代码风格的差异。

特殊写法分析:

// 这种写法实际上也能工作!
const arr = [1, 2, 3];
const result = arr.reduce(
  (p, x) => p.then(new Promise((r) => setTimeout(() => r(console.log(x)), 1000))),
  Promise.resolve()
);

为什么这种写法也能按顺序输出:

经过实际测试,这种写法确实能按顺序输出 1, 2, 3,原因如下:

  1. Promise 对象的特殊处理: 当 .then() 接收到 Promise 对象时,会等待这个 Promise resolve
  2. reduce 的累积效果: 每次 reduce 迭代都会创建新的 Promise 链
  3. 链式等待机制: 虽然所有 Promise 在 reduce 时就创建了,但 .then() 仍然会按顺序等待

执行流程分析:

// 第1次迭代: p = Promise.resolve(), x = 1
// 创建: new Promise(r => setTimeout(() => r(console.log(1)), 1000))
// 返回: Promise.resolve().then(上面的Promise)

// 第2次迭代: p = 上次返回的Promise, x = 2
// 创建: new Promise(r => setTimeout(() => r(console.log(2)), 1000))
// 返回: 上次Promise.then(这次的Promise)

// 第3次迭代: p = 上次返回的Promise, x = 3
// 创建: new Promise(r => setTimeout(() => r(console.log(3)), 1000))
// 返回: 上次Promise.then(这次的Promise)

与标准写法的区别:

// 标准写法:传入函数
p.then(() => new Promise(...))  // 函数在.then()执行时才调用

// 特殊写法:传入Promise对象
p.then(new Promise(...))        // Promise在reduce迭代时就创建

关键差异:

经过深入分析,创建时机的机制如下:

// 标准写法:
p.then(() => new Promise(...))  // Promise在.then()执行时创建

// 特殊写法:
p.then(new Promise(...))        // Promise在reduce迭代时就创建,不是在.then()执行时

重要纠正:

p.then() 里面的 Promise 确实需要等到链式调用执行时才会被处理,但是:

  1. 为什么所有 setTimeout 几乎同时启动?

    关键在于 reduce 的执行机制:

    // 特殊语法执行流程分析
    [1, 2, 3].reduce(
      (p, x) =>
        p.then(
          new Promise((resolve) => {
            setTimeout(() => {
              // ← 这里立即执行!
              console.log(x);
              resolve();
            }, 1000);
          })
        ),
      Promise.resolve()
    );
    
    // 执行步骤:
    // 1. reduce 开始遍历数组 [1, 2, 3]
    // 2. 第一次迭代 (x=1): new Promise(...) 立即创建 → setTimeout(1) 立即启动
    // 3. 第二次迭代 (x=2): new Promise(...) 立即创建 → setTimeout(2) 立即启动
    // 4. 第三次迭代 (x=3): new Promise(...) 立即创建 → setTimeout(3) 立即启动
    // 5. 三个 setTimeout 几乎在同一时刻启动!
    

    对比标准语法:

    // 标准语法 - 真正的顺序执行
    [1, 2, 3].reduce(
      (p, x) =>
        p.then(
          () =>
            new Promise((resolve) => {
              setTimeout(() => {
                // ← 只有在 .then() 回调执行时才启动
                console.log(x);
                resolve();
              }, 1000);
            })
        ),
      Promise.resolve()
    );
    
    // 执行步骤:
    // 1. 只有第一个 setTimeout 立即启动
    // 2. 1秒后第一个完成,触发第二个 .then() → 第二个 setTimeout 启动
    // 3. 再1秒后第二个完成,触发第三个 .then() → 第三个 setTimeout 启动
    
  2. 为什么仍然有序输出:

    • 虽然所有 setTimeout 同时启动,但 .then() 链仍然确保了顺序等待
    • 所有 Promise 都在 1 秒后 resolve
    • 但第二个 Promise 的结果要等第一个 Promise 通过 .then() 链传递
    • 第三个 Promise 的结果要等前两个都通过 .then()
  3. 关键疑问解答:为什么特殊写法中的 Promise 不用等待就立即执行?

    这是一个非常重要的理解点!深入分析如下:

    // 特殊写法分析
    [1, 2, 3].reduce(
      (p, x) =>
        p.then(
          new Promise((resolve) => {
            setTimeout(() => {
              console.log(x);
              resolve();
            }, 1000);
          })
        ),
      Promise.resolve()
    );
    

    核心原因:关键在于理解函数调用函数传递的区别!

    // 特殊写法 - 直接传递 Promise 对象
    p.then(
      new Promise((resolve) => {
        // ← new Promise() 在这里就执行了!
        setTimeout(() => {
          console.log(x);
          resolve();
        }, 1000);
      })
    );
    
    // 标准写法 - 传递函数
    p.then(
      () =>
        new Promise((resolve) => {
          // ← 函数在 .then() 执行时才调用
          setTimeout(() => {
            console.log(x);
            resolve();
          }, 1000);
        })
    );
    

    核心区别

    1. 特殊写法p.then(new Promise(...))

      • new Promise(...)立即执行的表达式
      • 在 reduce 迭代时,这个表达式就被求值了
      • 相当于先执行 const promise = new Promise(...),然后 p.then(promise)
      • Promise 构造函数在 reduce 阶段就执行了,不是在 .then() 回调中执行
    2. 标准写法p.then(() => new Promise(...))

      • () => new Promise(...) 是一个函数
      • 这个函数只有在 .then() 的回调被调用时才执行
      • Promise 构造函数在回调函数执行时才被调用

    用更简单的例子说明

    // 情况1:立即执行
    console.log('开始');
    const result = console.log('立即执行'); // 这行立即输出 "立即执行"
    setTimeout(() => {
      console.log('1秒后', result); // result 是 undefined
    }, 1000);
    
    // 情况2:延迟执行
    console.log('开始');
    setTimeout(() => {
      const result = console.log('延迟执行'); // 这行在1秒后才输出 "延迟执行"
      console.log('1秒后', result);
    }, 1000);
    

    应用到 Promise

    // 特殊写法:Promise 立即创建
    [1, 2, 3].reduce((p, x) => {
      const promise = new Promise((resolve) => {
        // ← 这里立即执行!
        setTimeout(() => {
          console.log(x);
          resolve();
        }, 1000);
      });
      return p.then(promise); // 传递已创建的 Promise
    }, Promise.resolve());
    
    // 标准写法:Promise 延迟创建
    [1, 2, 3].reduce((p, x) => {
      return p.then(() => {
        // 传递函数
        return new Promise((resolve) => {
          // ← 只有在回调执行时才创建!
          setTimeout(() => {
            console.log(x);
            resolve();
          }, 1000);
        });
      });
    }, Promise.resolve());
    

核心答案

关键在于理解 JavaScript 参数传递的执行时机!

核心机制分析

// 特殊写法的等价形式
[1, 2, 3].reduce((p, x) => {
  const promise = new Promise((resolve) => {
    // ← 这里立即执行!在 .then() 调用之前就执行了
    setTimeout(() => {
      console.log(x);
      resolve();
    }, 1000);
  });
  return p.then(promise); // 传递已创建的 Promise
}, Promise.resolve());

关键在于构造函数调用 vs 函数引用传递

JavaScript 函数调用时,所有参数都会先被求值,然后才传递给函数!

// 原始写法
p.then(
  new Promise((resolve) => {
    setTimeout(() => {
      console.log(x);
      resolve();
    }, 1000);
  })
);

// 执行步骤分解:
// 1. 首先执行 new Promise(...) - 构造函数立即执行!
// 2. 然后将创建好的 Promise 对象传递给 .then()
// 3. .then() 接收到的是一个已经创建好的 Promise

关键对比

// 情况1:传递构造函数调用结果(立即执行)
function createPromise() {
  console.log('Promise 构造函数执行了!');
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('setTimeout 执行了');
      resolve();
    }, 1000);
  });
}

p.then(createPromise()); // ← createPromise() 立即执行!

// 情况2:传递函数引用(延迟执行)
function createPromise() {
  console.log('Promise 构造函数执行了!');
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('setTimeout 执行了');
      resolve();
    }, 1000);
  });
}

p.then(createPromise); // ← createPromise 只是传递函数引用,不执行

为什么会立即执行

  1. 参数求值规则:JavaScript 在调用函数前,必须先计算所有参数的值

  2. 构造函数特性new Promise(...) 的构造函数会立即执行

  3. 执行顺序

    // 当执行这行代码时:
    p.then(
      new Promise((resolve) => {
        /* ... */
      })
    );
    
    // JavaScript 的执行顺序:
    // 步骤1:执行 new Promise(...) - 构造函数立即运行
    // 步骤2:将创建的 Promise 对象传递给 .then()
    // 步骤3:.then() 处理这个已经存在的 Promise
    

深入理解

// 这两种写法的本质区别:

// 写法1:传递构造函数调用结果
p.then(new Promise(...))  // Promise 构造函数立即执行

// 写法2:传递函数引用
p.then(() => new Promise(...))  // 函数延迟执行,Promise 构造函数也延迟执行

总结

  • 核心区别:传递的是构造函数调用结果 vs 函数引用
  • new Promise(...) 虽然在 p.then() 的参数位置,但它在 .then() 调用时就立即执行了
  • 这是 JavaScript 参数传递机制决定的:参数先求值,再传递
  • 所有的 setTimeout 几乎同时启动,因为所有的 Promise 构造函数都在 reduce 迭代时立即执行了
  1. 真正的区别:
    • 标准写法: Promise 按需创建,真正的串行执行
    • 特殊写法: Promise 并行创建,但通过 .then() 链实现有序输出

性能差异:

  • 特殊写法会同时占用更多资源(所有 setTimeout 同时运行)
  • 标准写法更节省资源(按需创建和执行)

这个细节很重要,理解了参数传递机制就能明白执行时机的差异。

推荐建议:

虽然特殊写法能工作,但推荐使用标准写法

// ✅ 推荐:清晰明确的函数式写法
arr.reduce(
  (p, x) => p.then(() => new Promise((r) => setTimeout(() => r(console.log(x)), 1000))),
  Promise.resolve()
);

原因:

  • 代码意图更清晰
  • 符合 Promise 链式调用的最佳实践
  • 更容易被团队成员理解和维护
  • 内存使用更优化

reduce 方法的核心机制解析:

// reduce 串行 Promise 的工作原理
const arr = [1, 2, 3];

// 展开后的执行过程:
Promise.resolve() // 初始值
  .then(() => {
    return new Promise((r) => {
      setTimeout(() => r(console.log(1)), 1000);
    });
  })
  .then(() => {
    return new Promise((r) => {
      setTimeout(() => r(console.log(2)), 1000);
    });
  })
  .then(() => {
    return new Promise((r) => {
      setTimeout(() => r(console.log(3)), 1000);
    });
  });

// reduce 的累积过程:
// 第1次: p = Promise.resolve(), x = 1
//       返回 p.then(() => new Promise(...))
// 第2次: p = 上次返回的Promise, x = 2
//       返回 p.then(() => new Promise(...))
// 第3次: p = 上次返回的Promise, x = 3
//       返回 p.then(() => new Promise(...))

为什么 reduce 能实现串行执行:

  1. 累积器的作用: 每次 reduce 迭代都将前一个 Promise 作为累积器传递
  2. 链式依赖: 每个新的 Promise 都依赖于前一个 Promise 的完成
  3. 顺序保证: 只有当前一个 Promise resolve 后,下一个 then 才会执行

reduce 方法的优势:

  • 函数式风格: 声明式编程,代码简洁优雅
  • 动态数组: 可以轻松处理任意长度的数组
  • 可复用: 可以抽象成通用的串行执行函数
// 通用的串行执行函数
function executeSequentially(tasks, delay = 1000) {
  return tasks.reduce((promise, task) => {
    return promise.then(() => {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log(task);
          resolve();
        }, delay);
      });
    });
  }, Promise.resolve());
}

// 使用示例
executeSequentially(['任务A', '任务B', '任务C'], 1500);
executeSequentially([1, 2, 3, 4, 5], 800);

输出顺序 (每个数字间隔 1 秒):

开始方法1
1
(1秒后) 2
(1秒后) 3
方法1完成

setTimeout 回调函数的限制和特性:

// 1. 回调函数的参数限制
setTimeout(
  (param1, param2) => {
    console.log('参数1:', param1); // 'hello'
    console.log('参数2:', param2); // 'world'
  },
  1000,
  'hello',
  'world'
); // 第3个参数开始会传递给回调函数

// 2. this 绑定问题
const obj = {
  name: 'MyObject',
  method1: function () {
    setTimeout(function () {
      console.log(this.name); // undefined (普通函数this指向window/global)
    }, 1000);
  },
  method2: function () {
    setTimeout(() => {
      console.log(this.name); // 'MyObject' (箭头函数继承外层this)
    }, 1000);
  }
};

// 3. 闭包变量捕获问题
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 输出3次3,而不是0,1,2
  }, 1000);
}

// 解决方案1: 使用let
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 输出0,1,2
  }, 1000);
}

// 解决方案2: 使用闭包
for (var i = 0; i < 3; i++) {
  (function (index) {
    setTimeout(() => {
      console.log(index); // 输出0,1,2
    }, 1000);
  })(i);
}

// 4. 最小延时限制
setTimeout(() => {
  console.log('实际延时可能大于0ms');
}, 0); // 浏览器最小延时通常是4ms

// 5. 嵌套setTimeout的延时递增
let count = 0;
function nestedTimeout() {
  console.log('执行次数:', ++count);
  if (count < 10) {
    setTimeout(nestedTimeout, 0); // 嵌套超过5层后,最小延时变为4ms
  }
}
nestedTimeout();

// 6. 回调函数中的异常处理
setTimeout(() => {
  throw new Error('setTimeout中的错误'); // 这个错误无法被外层try-catch捕获
}, 1000);

// 正确的异常处理方式
setTimeout(() => {
  try {
    // 可能出错的代码
    throw new Error('内部错误');
  } catch (error) {
    console.error('捕获到错误:', error.message);
  }
}, 1000);

关键理解点:

  1. Promise 定时输出的核心:

    • 使用 Promise 包装 setTimeout 创建延时函数
    • 通过链式调用或 async/await 控制执行顺序
    • 每个步骤都返回 Promise 以保持链式调用
  2. setTimeout 回调函数限制:

    • 参数传递: 可以通过第 3 个参数开始传递给回调函数
    • this 绑定: 普通函数 this 指向全局对象,箭头函数继承外层 this
    • 闭包问题: var 声明的变量在循环中会被共享
    • 最小延时: 浏览器有最小延时限制(通常 4ms)
    • 异常处理: 回调中的异常无法被外层 try-catch 捕获
  3. 实际应用建议:

    • 优先使用 async/await 实现定时逻辑,代码更清晰
    • 注意 setTimeout 的 this 绑定问题
    • 在循环中使用 setTimeout 时注意变量作用域
    • setTimeout 回调中的异常需要内部处理

Promise 核心原则总结

通过以上示例,我们可以总结出 Promise 的核心原则:

  1. 状态不可逆: Promise 的状态一经改变就不能再改变(pending → resolved/rejected)

  2. 链式返回: .then().catch() 都会返回一个新的 Promise

  3. 错误冒泡: .catch() 不管被连接到哪里,都能捕获上层未捕捉过的错误

  4. 自动包装: 在 Promise 中,返回任意一个非 Promise 的值都会被包裹成 Promise 对象,例如 return 2 会被包装为 return Promise.resolve(2)

  5. 多次调用: Promise 的 .then() 或者 .catch() 可以被调用多次,但如果 Promise 内部的状态一经改变并且有了一个值,那么后续每次调用 .then() 或者 .catch() 的时候都会直接拿到该值

  6. 返回 Error 不等于抛出: .then().catch()return 一个 Error 对象并不会抛出错误,所以不会被后续的 .catch() 捕获

  7. 避免循环引用: .then().catch() 返回的值不能是 Promise 本身,否则会造成死循环

  8. 值透传机制: .then() 或者 .catch() 的参数期望是函数,传入非函数则会发生值透传

  9. 双参数处理: .then() 方法能接收两个参数,第一个是处理成功的函数,第二个是处理失败的函数,在某些时候你可以认为 .catch().then() 第二个参数的简便写法

  10. finally 特性: .finally() 方法也是返回一个 Promise,它在 Promise 结束的时候,无论结果为 resolved 还是 rejected,都会执行里面的回调函数

常见误解与陷阱

误解 1: Promise.resolve()会立即执行 then 回调

console.log('开始');
Promise.resolve().then(() => console.log('then回调'));
console.log('结束');

输出顺序:

开始
结束
then回调

Promise.resolve()确实立即创建了一个 resolved 的 Promise,但 then 回调仍然会作为微任务在当前宏任务结束后执行。

误解 2: await 会立即执行后面的代码

async function test() {
  console.log('函数开始');
  await Promise.resolve();
  console.log('await后的代码'); // 这不会立即执行
}

test();
console.log('全局代码');

输出顺序:

函数开始
全局代码
await后的代码

await 后的代码会被转换为 Promise.then 的回调,作为微任务在当前宏任务结束后执行。

误解 3: 连续的 await 会并行执行

async function test() {
  console.time('test');

  // 这两个操作是顺序执行的,不是并行的
  const result1 = await fetch('/api/data1'); // 假设耗时1秒
  const result2 = await fetch('/api/data2'); // 假设耗时1秒

  console.timeEnd('test'); // 约2秒,而不是1秒
}

连续的 await 是顺序执行的,第二个 await 会等待第一个 await 完成后才开始。如果需要并行执行,应该使用 Promise.all 或先创建 Promise 再 await。

误解 4: 浏览器控制台中的 undefined 输出

在浏览器控制台中执行代码时,你可能会注意到除了预期的输出外,还会显示一个额外的 undefined

// 在浏览器控制台中执行
console.log('Hello World');

控制台显示:

Hello World
undefined

机制解析:

这个 undefined 并不是代码执行的一部分,而是浏览器控制台的特性:

  1. 表达式返回值: 浏览器控制台会显示每个执行语句的返回值
  2. console.log()的返回值: console.log() 函数执行后返回 undefined
  3. 控制台显示机制: 控制台会自动显示这个返回值

示例对比:

// 在控制台中执行不同类型的语句

// 1. console.log() - 返回 undefined
console.log('测试');
// 输出: 测试
//      undefined

// 2. 赋值语句 - 返回赋值的值
var x = 5;
// 输出: undefined (var 声明返回 undefined)

let y = 10;
// 输出: undefined (let 声明返回 undefined)

// 3. 表达式 - 返回计算结果
2 + 3;
// 输出: 5

// 4. 函数调用 - 返回函数的返回值
function test() {
  console.log('函数内部');
  return 'function result';
}
test();
// 输出: 函数内部
//      "function result"

重要说明:

  • 这个 undefined 只在浏览器控制台中出现,不会影响实际代码执行
  • 在脚本文件中运行相同代码时,不会有这个额外的 undefined 输出
  • 这是浏览器开发者工具的显示特性,不是 JavaScript 语言本身的行为
  • 理解这一点有助于区分真正的代码输出和控制台的显示机制

总结

理解 JavaScript 事件循环机制的关键是:

  1. 正确划分任务: 以回调任务为单位思考,而不是单行代码
  2. 掌握执行顺序: 当前宏任务 → 所有微任务 → 下一个宏任务
  3. 理解 async/await: 本质是 Promise 的语法糖,await 后的代码作为微任务执行
  4. 避免常见误解: Promise.resolve()不会立即执行 then 回调,连续 await 是顺序执行的

通过分析实际代码示例,我们可以更准确地预测 JavaScript 异步代码的执行顺序,编写更高效、可靠的异步代码。

你可能感兴趣的:(js,javascript,开发语言,ecmascript)