在前端开发里,异步是一种关键的编程模式,用于处理可能耗时较长的操作,避免阻塞主线程,从而提升应用程序的响应能力和性能。下面从异步产生的原因、实现方式、应用场景和注意事项几个方面详细介绍:
异步产生的原因
JavaScript 是单线程的,这意味着同一时间只能执行一个任务。如果执行一个耗时的操作(如网络请求、文件读取)采用同步方式,后续代码会被阻塞,页面会出现卡顿,影响用户体验。而异步操作可以让程序在等待耗时操作完成的同时继续执行其他任务。
异步的实现方式
回调函数
回调函数是最基本的异步处理方式,将一个函数作为参数传递给另一个函数,当异步操作完成后调用该回调函数。
function fetchData(callback) {
setTimeout(() => {
const data = { message: '数据已获取' };
callback(data);
}, 1000);
}
fetchData((result) => {
console.log(result.message);
});
缺点:回调地狱问题,代码嵌套层次过深,难以维护和调试。
Promise
Promise 是一种更优雅的异步处理方式,避免了回调地狱。它有三种状态:pending
(进行中)、fulfilled
(已成功)和 rejected
(已失败)。
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
const data = { message: '数据已获取' };
resolve(data);
} else {
reject(new Error('数据获取失败'));
}
}, 1000);
});
}
fetchData()
.then((result) => {
console.log(result.message);
})
.catch((error) => {
console.error(error);
});
优点:链式调用使代码更清晰,便于错误处理。
async/await
async/await
是基于 Promise 的语法糖,让异步代码看起来更像同步代码,提高了代码的可读性和可维护性。
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
const data = { message: '数据已获取' };
resolve(data);
} else {
reject(new Error('数据获取失败'));
}
}, 1000);
});
}
async function main() {
try {
const result = await fetchData();
console.log(result.message);
} catch (error) {
console.error(error);
}
}
main();
优点:代码结构更简洁,易于理解和调试。
异步的应用场景
- 网络请求:如使用
fetch
或axios
进行 API 调用,避免页面在等待响应时卡顿。 - 定时器:
setTimeout
和setInterval
用于实现延迟执行或周期性执行任务。 - 文件读取:在浏览器或 Node.js 中读取文件时,使用异步方式避免阻塞主线程。
异步编程的注意事项
- 错误处理:在异步操作中,错误处理尤为重要。使用
try...catch
块捕获async/await
中的错误,使用.catch()
方法处理 Promise 中的错误。 - 执行顺序:异步操作的执行顺序可能与代码编写顺序不同,需要注意控制异步操作的执行顺序。
代码问题分析1
console.log(1);
setTimeout(function() {
console.log(2);
}, 0);
new Promise(function(resolve) {
console.log(3);
// 调用 resolve 方法,将 Promise 状态变为 resolved
resolve();
}).then(function() {
console.log(4);
});
console.log(5);
代码执行流程
-
同步代码执行:
console.log(1)
立即执行,输出1
。setTimeout
是异步函数,其回调函数被放入宏任务队列,等待当前调用栈清空后执行。new Promise
的构造函数同步执行,console.log(3)
输出3
,然后调用resolve()
方法将Promise
状态变为resolved
。console.log(5)
立即执行,输出5
。
-
微任务执行:
Promise
的.then
回调函数被放入微任务队列。在同步代码执行完毕后,JavaScript 引擎会优先处理微任务队列,因此console.log(4)
执行,输出4
。
-
宏任务执行:
- 最后,JavaScript 引擎处理宏任务队列,执行
setTimeout
的回调函数,输出2
。
- 最后,JavaScript 引擎处理宏任务队列,执行
最终输出顺序
1
3
5
4
2
代码问题分析2
console.log(1);
setTimeout(function() {
console.log(2);
}, 0);
new Promise(function(resolve, reject) {
console.log(3);
// 拒绝 Promise
reject(new Error('Promise 被拒绝'));
})
.then(function() {
console.log(4);
})
.catch(function(error) {
// 处理 Promise 被拒绝的情况
console.error('捕获到错误:', error.message);
});
console.log(5);
代码执行流程解释
-
同步代码执行:
console.log(1)
是同步代码,会立即执行,输出1
。setTimeout
是异步函数,其回调函数会被放入宏任务队列,等待当前调用栈清空后执行。尽管延迟时间设置为0
毫秒,它还是会在同步代码之后执行。new Promise
的构造函数是同步执行的,其中的console.log(3)
会立即执行,输出3
。接着调用reject()
方法,将Promise
的状态变为已拒绝。console.log(5)
是同步代码,会立即执行,输出5
。
-
微任务和错误处理:
- 由于
Promise
被拒绝,.then()
方法中的回调函数不会执行。因为.then()
方法只处理Promise
成功(fulfilled
)的情况。同时,由于没有使用.catch()
方法来捕获拒绝错误,会在控制台抛出一个未处理的拒绝错误。
- 由于
-
宏任务执行:
- 最后,JavaScript 引擎会检查宏任务队列,执行其中的任务。
setTimeout
的回调函数会被执行,输出2
。
- 最后,JavaScript 引擎会检查宏任务队列,执行其中的任务。
输出顺序
1
3
5
捕获到错误: Promise 被拒绝
2
代码问题分析3
async function async1() {
console.log('async1 start')
await async2()
// await后面的代码可以理解为promise.then(function(){ 回调执行的 })
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
async1()
console.log('script end')
1. 同步代码执行
console.log('script start')
:这是同步代码,会立即执行,输出script start
。setTimeout
:这是一个异步函数,它的回调函数会被放入宏任务队列,等待当前调用栈清空后执行,所以不会立即输出setTimeout
。async1()
:调用async1
函数,async1
函数内部开始执行。console.log('async1 start')
:同步代码,立即执行,输出async1 start
。await async2()
:调用async2
函数。console.log('async2')
:async2
函数内部的同步代码,立即执行,输出async2
。await
会暂停async1
函数后续代码的执行,将async1
函数剩余代码放入微任务队列。
console.log('script end')
:同步代码,立即执行,输出script end
。
2. 微任务执行
此时同步代码执行完毕,JavaScript 引擎会先检查微任务队列。async1
函数中 await
后面的代码 console.log('async1 end')
在微任务队列中,所以会执行该代码,输出 async1 end
。
3. 宏任务执行
微任务队列清空后,JavaScript 引擎会检查宏任务队列。setTimeout
的回调函数在宏任务队列中,所以会执行该回调函数,输出 setTimeout
。
输出顺序
script start
async1 start
async2
script end
async1 end
setTimeout