JavaScript 的事件循环机制是理解异步编程的关键。本文通过实际代码示例和详细解析,帮助你掌握事件循环的工作原理,准确预测代码执行顺序。
JavaScript 是单线程语言,通过事件循环处理异步操作。事件循环由以下几个关键部分组成:
事件循环遵循以下规则:
console.log('1 - 开始');
setTimeout(() => {
console.log('2 - 定时器回调');
}, 0);
Promise.resolve().then(() => {
console.log('3 - Promise回调');
});
console.log('4 - 结束');
输出顺序:
1 - 开始
4 - 结束
3 - Promise回调
2 - 定时器回调
解析:
1 - 开始
和4 - 结束
3 - Promise回调
2 - 定时器回调
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
2
4
宏任务 2(setTimeout 回调):
timerStart
timerEnd
微任务队列(在宏任务 2 之后执行):
success
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
解析:
script start
和script end
promise1
promise2
setTimeout
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
解析:
script start
async1 start
async2
script end
async1 end
在分析事件循环时,我们应该以"回调任务"为单位思考,而不是简单地看单行代码。一个回调函数内的所有同步代码会作为一个整体执行,不会被其他任务打断。
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 回调也不会立即执行,而是要等到当前同步代码执行完毕后才会作为微任务执行。
setTimeout(() => {
console.log('定时器开始');
Promise.resolve().then(() => {
console.log('定时器内的Promise');
});
console.log('定时器结束');
}, 0);
Promise.resolve().then(() => {
console.log('外部Promise');
});
console.log('主脚本结束');
输出顺序:
主脚本结束
外部Promise
定时器开始
定时器结束
定时器内的Promise
解析:
主脚本结束
外部Promise
定时器开始
定时器结束
定时器内的Promise
async/await 是 Promise 的语法糖,它的本质仍然是基于 Promise 的微任务机制。
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
console.log(2)
)作为微任务4
2
console.log(3)
)作为新的微任务3
// 使用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 后的代码作为微任务执行。
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
解析:
Promise 状态确定: Promise 构造函数中先调用 reject("error")
,Promise 状态变为 rejected,后续的 resolve("success2")
不会执行(Promise 状态一旦确定就不能改变)
链式调用执行流程:
.then()
被跳过,因为 Promise 是 rejected 状态.then()
也被跳过,继续寻找错误处理器.catch()
捕获错误,输出 catch: error
.catch()
处理错误后返回一个 resolved 状态的 Promise(返回值为 undefined).then()
接收到 resolved 状态的 Promise,执行并输出 then3: undefined
Promise 链式调用规则:
.catch()
成功处理错误后,会返回一个 resolved 状态的 Promise.catch()
没有显式返回值,默认返回 undefined
.then()
会正常执行,因为错误已被处理重要概念:
.catch()
不仅用于捕获错误,还用于从错误中恢复,让 Promise 链继续执行.catch()
成功执行后,Promise 链的状态从 rejected 转为 resolved.catch()
后面的 .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
解析:
Promise 状态共享:
.then()
.then()
回调都会在 Promise resolve 时执行执行时机:
timer
resolve('success')
,Promise 状态变为 resolved.then()
回调都被加入微任务队列.then()
回调时间差异:
.then()
回调几乎同时执行(在同一个微任务队列中)与链式调用的区别:
// 链式调用 - 每个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()
创建的是独立的回调,不是链式关系.then()
注册的顺序执行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()
,但实际上:
return vs throw 的区别:
return new Error()
- 返回一个 Error 对象作为正常值,Promise 状态为 resolvedthrow new Error()
- 抛出异常,Promise 状态为 rejected执行流程:
.then()
返回 new 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抛出
});
关键理解:
new Error()
只是创建了一个 Error 类型的对象throw
、Promise.reject()
或其他异常机制才能改变 Promise 状态.then()
中 return
任何值(包括 Error 对象)都会被当作正常的 resolved 值throw
来抛出异常最佳实践:
// ❌ 错误写法 - 容易误解
.then(() => {
if (someCondition) {
return new Error('出错了'); // 这不会触发catch
}
})
// ✅ 正确写法 - 明确抛出异常
.then(() => {
if (someCondition) {
throw new Error('出错了'); // 这会触发catch
}
})
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log);
输出顺序:
1
解析:
这个示例展示了 Promise 的值透传机制:
非函数参数的处理:
.then(2)
- 传入数字 2
,不是函数.then(Promise.resolve(3))
- 传入 Promise 对象,不是函数值透传规则:
.then()
和 .catch()
的参数期望是函数.then()
Promise.resolve(1)
的值 1
直接透传到最后一个 .then(console.log)
等价写法:
// 原代码等价于:
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
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()
的重要区别:
执行流程分析:
Promise.resolve()
创建一个 resolved 状态的 Promise.then()
的第一个参数 success
函数success
函数中抛出异常 throw new Error('error!!!')
.then()
的第二个参数 fail1
不会执行.catch()
捕获,执行 fail2
函数为什么 fail1 不执行:
.then(onFulfilled, onRejected)
中的 onRejected
只处理当前 Promise 的 rejected 状态Promise.resolve()
,状态为 resolvedsuccess
函数执行时抛出的异常会创建一个新的 rejected PromisePromise 链的状态传递:
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,状态独立管理最佳实践:
// ❌ 容易混淆的写法
promise.then(success, fail1).catch(fail2);
// ✅ 推荐的清晰写法
promise
.then(success)
.catch(fail1) // 专门处理错误
.then(...) // 继续后续处理
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()
方法的特殊执行机制:
链式调用的微任务执行机制:
关键理解: 链式调用后面的内容需要等前一个调用执行完才会执行,不是所有回调都会在初始阶段就加入微任务队列。
初始阶段:
Promise.resolve('1')
创建 resolved 状态的 Promise.then()
回调加入微任务队列:[then1]
Promise.resolve('2')
创建 resolved 状态的 Promise.finally()
回调加入微任务队列:[then1, finally2]
.finally()
和第二个 .then()
还没有加入队列,因为它们要等前面的微任务执行完微任务队列执行过程:
then1
→ 输出 1
→ 执行完后将第一个 .finally()
加入队列 → 微任务队列:[finally2, finally1]
finally2
→ 输出 finally2
→ 执行完后将第二个 .then()
加入队列 → 微任务队列:[finally1, then2]
finally1
→ 输出 finally
→ 微任务队列:[then2]
then2
→ 输出 finally2后面的then函数 2
→ 微任务队列:[]
执行顺序: then1
→ finally2
→ finally1
→ then2
核心原理:
.then()
和 .finally()
等方法返回新的 Promise,链式后面的回调要等当前回调执行完才会加入微任务队列finally() 的关键特性:
.finally()
不会改变 Promise 的值,即使有 return
语句.finally()
不会改变 Promise 的状态(resolved/rejected).finally()
都会执行返回值处理:
.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() 的执行规则:
.finally()
中抛出异常或返回 rejected Promise,会改变 Promise 链的状态实际应用场景:
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(); // 无论成功失败都隐藏加载状态
});
}
// 模拟异步任务
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的订单列表']
解析:
.then()
才会执行Promise.all()
立即 rejectfunction 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 (推荐)
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' }
]
参数传递的关键理解:
函数返回 Promise 模式: 最灵活,可以传递不同参数
Promise.all([fetchData(param1), fetchData(param2), fetchData(param3)]);
立即执行 Promise: 参数在创建时就固定了
const promise1 = fetchData(param1); // 立即开始执行
const promise2 = fetchData(param2); // 立即开始执行
Promise.all([promise1, promise2]);
动态创建: 使用数组方法批量创建
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]);
}
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
解析:
这是一道经典的陷阱题,开发者经常会错误地认为会输出更多内容。
执行流程分析:
script start
async1()
函数async1
中输出 async1 start
await new Promise(...)
,Promise 构造函数同步执行,输出 promise1
resolve()
,Promise 永远处于 pending 状态await
会一直等待这个 Promise resolve,但它永远不会 resolvescript end
为什么后续代码不执行:
console.log('async1 success')
永远不会执行,因为 await 在等待一个永远不会 resolve 的 Promisereturn 'async1 end'
永远不会执行async1().then(res => console.log(res))
中的 then 回调永远不会执行,因为 async1 函数永远不会返回常见误解:
开发者可能认为会输出:
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: 忘记在条件分支中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。
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 中错误处理的关键机制:
执行流程分析:
async1()
函数async1
中遇到 await async2()
async2()
函数,输出 async2
async2()
返回一个 rejected 状态的 Promiseawait
遇到 rejected 的 Promise 会抛出异常async1
函数终止执行,后续代码不会执行不会执行的代码:
console.log('async1')
不会执行,因为 await 抛出了异常return 'async1 success'
不会执行async1().then(res => console.log(res))
中的 then 回调不会执行,因为 async1 返回 rejected 状态的 Promise错误传播机制:
async2()
返回 rejected Promiseawait async2()
将 rejection 转换为异常抛出async1()
函数因为未捕获的异常而返回 rejected Promiseasync1().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: 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 导致程序意外终止。
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 函数在遇到错误后继续执行:
执行流程分析:
async1()
函数await Promise.reject('error!!!')
error!!!
async1
Promise.resolve('async1 success')
script start
async1 success
与未处理错误的对比:
// 未处理错误的版本
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
try-catch 的作用机制:
另一种优雅的错误处理方式 - 链式 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 在 async/await 中的强大作用,让错误处理变得更加灵活和可控。
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、状态管理和事件循环的多重交互:
同步代码执行阶段:
first()
函数3
p
,执行其构造函数,输出 7
resolve(1)
,状态变为 resolved,值为 1resolve(2)
,状态变为 resolved,值为 2p.then()
回调到微任务队列first().then()
回调到微任务队列4
微任务队列执行:
p.then()
回调,输出 1
(内层 Promise 的 resolved 值)first().then()
回调,输出 2
(外层 Promise 的 resolved 值)宏任务执行:
5
resolve(6)
,但 Promise p
已经是 resolved 状态,这次调用无效p
的状态,显示 Promise { : 1 }
关键理解点:
Promise 状态不可逆:
p
先调用 resolve(1)
,状态确定为 resolved,值为 1resolve(6)
调用无效,不会改变 Promise 状态嵌套 Promise 的独立性:
p
是两个独立的 Promise 实例执行时机:
.then()
回调是异步执行的(微任务)微任务队列顺序:
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状态
常见误解:
开发者可能认为:
resolve(6)
会改变 Promise 的值 ❌p
的最终值是 6 ❌实际情况:
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 状态管理的复杂性和事件循环中同步/异步代码的执行顺序。
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 异步编程概念:
同步代码执行阶段:
script start
async1()
函数async1
中输出 async1
await new Promise(resolve => { console.log('promise1') })
promise1
async1
函数后续代码永远不会执行script end
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)
宏任务执行:
timer2
timer1
重要说明: 虽然 timer1 在代码中先遇到,但 setTimeout 的执行顺序是按延时时间决定的,不是按代码遇到的顺序!timer2 延时 1 秒,timer1 延时 2 秒,所以 timer2 先执行。
不会执行的代码:
console.log('async1 end')
- 永远不会执行return 'async1 success'
- 永远不会执行async1().then(res => console.log(res))
- then 回调永远不会执行关键知识点:
async/await 陷阱:
// 陷阱代码
await new Promise((resolve) => {
console.log('promise1'); // 只输出,不resolve
});
// 后续代码永远不会执行
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)
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 和事件循环理解程度的优秀题目。
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 的核心概念:
Promise 状态不可逆性:
resolve('resovle1')
,Promise 状态立即变为 resolved,值为'resovle1'
resolve('resolve2')
调用无效,因为 Promise 状态已经确定resolve('resolve3')
也无效,Promise 状态不能再次改变链式调用的执行流程:
'resovle1'
.then()
回调执行,输出resovle1
.then()
回调函数没有显式 return,JavaScript 会自动返回undefined
.then()
只要正常执行完成(没有抛出异常),就会返回一个resolved 状态的 Promise.then()
也会返回Promise.resolve(undefined)
.finally()
接收到的是.then()
返回的 Promise,状态为 resolved,值为undefined
finally 的特殊性质:
finally(res => {...})
中的res
参数是迷惑项!.finally()
的回调函数实际上不接收任何参数,res
始终为undefined
.finally()
不会改变 Promise 的值,会透传上一个 Promise 的结果.finally()
返回一个新的 Promise,值为上一步的结果(这里是undefined
)变量 p1 的真实含义:
p1
不是指向最初的 Promise,而是指向整个链式调用的最终结果p1
实际上是.finally()
返回的 Promise.then()
返回undefined
,.finally()
透传这个值,所以p1
的值是undefined
执行时序:
.then()
回调,输出resovle1
,设置 1 秒定时器.finally()
回调,输出finally undefined
timer1
Promise { : undefined }
关键理解点:
.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
finally 参数陷阱:
// ❌ 错误理解:认为finally能接收Promise的结果
.finally(res => {
console.log('finally', res); // res永远是undefined
})
// ✅ 正确理解:finally不接收参数
.finally(() => {
console.log('finally执行,但不知道Promise的结果');
})
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 透传规则总结:
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('出错了');
});
关键区别:
resolve()
或 reject()
,否则 Promise 永远处于 pending 状态return
自动 resolve,通过 throw
自动 reject,不需要显式调用.then()
中的返回值会被自动包装成 Promise 状态链式调用的返回值:
// 原始情况:没有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()的返回值)
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 高级特性的优秀示例。
需求: 使用 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完成'));
具体区别:
代码格式:
return
语句return
关键字可读性:
调试友好性:
return
语句处设置断点,便于调试团队协作:
推荐使用场景:
功能上完全等价: 两个方法的执行结果、性能、内存占用都完全相同,只是代码风格的差异。
特殊写法分析:
// 这种写法实际上也能工作!
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,原因如下:
.then()
接收到 Promise 对象时,会等待这个 Promise resolve.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 确实需要等到链式调用执行时才会被处理,但是:
为什么所有 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 启动
为什么仍然有序输出:
.then()
链仍然确保了顺序等待.then()
链传递.then()
链关键疑问解答:为什么特殊写法中的 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);
})
);
核心区别:
特殊写法:p.then(new Promise(...))
new Promise(...)
是立即执行的表达式const promise = new Promise(...)
,然后 p.then(promise)
标准写法:p.then(() => new Promise(...))
() => new 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());
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 只是传递函数引用,不执行
参数求值规则:JavaScript 在调用函数前,必须先计算所有参数的值
构造函数特性:new Promise(...)
的构造函数会立即执行
执行顺序:
// 当执行这行代码时:
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 构造函数也延迟执行
new Promise(...)
虽然在 p.then()
的参数位置,但它在 .then()
调用时就立即执行了setTimeout
几乎同时启动,因为所有的 Promise 构造函数都在 reduce 迭代时立即执行了.then()
链实现有序输出性能差异:
这个细节很重要,理解了参数传递机制就能明白执行时机的差异。
推荐建议:
虽然特殊写法能工作,但推荐使用标准写法:
// ✅ 推荐:清晰明确的函数式写法
arr.reduce(
(p, x) => p.then(() => new Promise((r) => setTimeout(() => r(console.log(x)), 1000))),
Promise.resolve()
);
原因:
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 能实现串行执行:
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);
关键理解点:
Promise 定时输出的核心:
setTimeout 回调函数限制:
实际应用建议:
通过以上示例,我们可以总结出 Promise 的核心原则:
状态不可逆: Promise 的状态一经改变就不能再改变(pending → resolved/rejected)
链式返回: .then()
和 .catch()
都会返回一个新的 Promise
错误冒泡: .catch()
不管被连接到哪里,都能捕获上层未捕捉过的错误
自动包装: 在 Promise 中,返回任意一个非 Promise 的值都会被包裹成 Promise 对象,例如 return 2
会被包装为 return Promise.resolve(2)
多次调用: Promise 的 .then()
或者 .catch()
可以被调用多次,但如果 Promise 内部的状态一经改变并且有了一个值,那么后续每次调用 .then()
或者 .catch()
的时候都会直接拿到该值
返回 Error 不等于抛出: .then()
或 .catch()
中 return
一个 Error 对象并不会抛出错误,所以不会被后续的 .catch()
捕获
避免循环引用: .then()
或 .catch()
返回的值不能是 Promise 本身,否则会造成死循环
值透传机制: .then()
或者 .catch()
的参数期望是函数,传入非函数则会发生值透传
双参数处理: .then()
方法能接收两个参数,第一个是处理成功的函数,第二个是处理失败的函数,在某些时候你可以认为 .catch()
是 .then()
第二个参数的简便写法
finally 特性: .finally()
方法也是返回一个 Promise,它在 Promise 结束的时候,无论结果为 resolved 还是 rejected,都会执行里面的回调函数
console.log('开始');
Promise.resolve().then(() => console.log('then回调'));
console.log('结束');
输出顺序:
开始
结束
then回调
Promise.resolve()确实立即创建了一个 resolved 的 Promise,但 then 回调仍然会作为微任务在当前宏任务结束后执行。
async function test() {
console.log('函数开始');
await Promise.resolve();
console.log('await后的代码'); // 这不会立即执行
}
test();
console.log('全局代码');
输出顺序:
函数开始
全局代码
await后的代码
await 后的代码会被转换为 Promise.then 的回调,作为微任务在当前宏任务结束后执行。
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。
在浏览器控制台中执行代码时,你可能会注意到除了预期的输出外,还会显示一个额外的 undefined
:
// 在浏览器控制台中执行
console.log('Hello World');
控制台显示:
Hello World
undefined
机制解析:
这个 undefined
并不是代码执行的一部分,而是浏览器控制台的特性:
console.log()
的返回值: console.log()
函数执行后返回 undefined
示例对比:
// 在控制台中执行不同类型的语句
// 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 异步代码的执行顺序,编写更高效、可靠的异步代码。