前置知识:事件循环与微任务机制
- 宏任务(MacroTask):
setTimeout,setInterval,requestAnimationFrame,postMessage。 - 微任务(MicroTask):
Promise回调:then、catch、finally,MutationObserver,queueMicrotask,async/await,await后的代码相当于微任务。
| 特性 | 宏任务(Macrotask) | 微任务(Microtask) |
|---|---|---|
| 执行时机 | 每次事件循环处理一个宏任务 | 当前宏任务结束后立即清空所有微任务 |
| 队列处理 | 单次处理一个任务 | 一次性处理全部任务 |
| 优先级 | 低 | 高 |
| 常见 API | setTimeout、DOM 事件、I/O | Promise、MutationObserver |
1. 回调函数时代
- JavaScript早期的异步操作(如AJAX请求、定时任务)完全依赖回调函数实现。其本质是通过函数参数传递,将异步任务完成后的逻辑封装为函数,由事件循环触发执行。
fs.readFile('file1.txt', (err, data1) => {
fs.readFile('file2.txt', (err, data2) => {
fs.writeFile('result.txt', data1 + data2, (err) => {
if (err) throw err;
console.log('Done!');
});
});
});
缺点
- 嵌套结构:回调函数支持链式调用,但多层嵌套会导致“回调地狱”(Callback Hell),代码可读性和可维护性极差。
- 错误处理缺陷:错误需手动传递,缺乏统一机制,易导致未捕获异常
- 典型应用:
setTimeout、XMLHttpRequest等API均采用回调模式。
2. Promise 的诞生(ES6 / ES2015)
- ES6引入Promise,通过状态机模型(Pending/Fulfilled/Rejected)统一管理异步任务,解决回调地狱问题。
- 通过链式调用
.then()和.catch()扁平化异步流程。
readFile('file1.txt')
.then(data1 => readFile('file2.txt'))
.then(data2 => writeFile('result.txt', data1 + data2))
.then(() => console.log('Done!'))
.catch(err => console.error(err));
缺陷
- 冗余语法:频繁使用
.then()导致代码冗余,语义不够直观。 - 无法取消:Promise一旦创建即执行,缺乏中断机制(需额外封装)。
3. 生成器
Generator 对象由生成器函数返回并且它符合可迭代协议和迭代器协议。
- Generator 通过
yield关键字暂停函数执行,并能在外部恢复,使得异步代码可以 以同步的方式编写。
function* infinite() {
let index = 0;
while (true) {
yield index++;
}
}
const generator = infinite(); // "Generator { }"
console.log(generator.next().value); // 0
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
// generator+yeild实现同步执行异步任务
function* fetchUser() {
const response = yield fetch('/api/user'); // 暂停,等待 fetch 完成
const user = yield response.json(); // 再次暂停,等待解析 JSON
return user;
}
// 执行器函数(如 co 库)
function run(generator) {
const iterator = generator();
function handle(result) {
if (result.done) return result.value;
return result.value.then(data => {
return handle(iterator.next(data)); // 恢复执行并传递数据
});
}
return handle(iterator.next());
}
run(fetchUser).then(user => console.log(user));
Generator 实例的 next() 方法返回一个包含属性 done 和 value 的对象。你也可以通过向 next 方法传入一个参数来向生成器传一个值。
function* gen() {
while (true) {
const value = yield;
console.log(value);
}
}
const g = gen();
g.next(1); // 返回 { value: undefined, done: false }
// 这一步不会有输出:通过 `next` 发送的第一个值会被丢弃
g.next(2); // 返回 { value: undefined, done: false }
// 打印 2
4. async/await 的成熟(ES2017)
异步函数的函数体可以被看作是由零个或者多个 await 表达式分割开来的。从顶层代码直到(并包括)第一个 await 表达式(如果有的话)都是同步运行的。因此,不包含 await 表达式的异步函数是同步运行的。然而,如果函数体内包含 await 表达式,则异步函数就一定会异步完成。
async function processFiles() {
try {
const data1 = await readFile('file1.txt');
const data2 = await readFile('file2.txt');
await writeFile('result.txt', data1 + data2);
console.log('Done!');
} catch (err) {
console.error(err);
}
}
// 等价于
function processFiles(){
return new Promise((resolve, reject)=>{
readFile('file1.txt').then(data1=>{
readFile('file2.txt').then(data2=>{
writeFile('result.txt', data1 + data2).then(res=>{
resolve()
})
})
})
})
}
每个 await 表达式之后的代码可以被认为存在于 .then 回调中。通过这种方式,可以通过函数的每个可重入步骤来逐步构建 promise 链。而返回值构成了链中的最后一个环。
如果给定的值是一个 promise,异步函数会返回一个不同的引用,而
Promise.resolve会返回相同的引用,
const p = new Promise((res, rej) => {
res(1);
});
async function asyncReturn() {
return p;
}
function basicReturn() {
return Promise.resolve(p);
}
console.log(p === basicReturn()); // true
console.log(p === asyncReturn()); // false
promise 链不是一次就构建好的,相反,promise 、链是随着控制权依次在异步函数中交出并返回而分阶段构建的。
async/await捕获错误
function getProcessedData(url) {
return downloadData(url) // 返回一个 promise
.catch((e) => downloadFallbackData(url)) // 返回一个 promise
.then((v) => processDataInWorker(v)); // 返回一个 promise
}
// try...catch
async function getProcessedData(url) {
let v;
try {
v = await downloadData(url);
} catch (e) {
v = await downloadFallbackData(url);
}
return processDataInWorker(v);
}
async function getProcessedData(url) {
const v = await downloadData(url).catch((e) => downloadFallbackData(url));
return processDataInWorker(v);
}
未来趋势
- Top-level await:在模块顶层直接使用
await(ES2022)。 - 异步迭代器:
for await...of处理流式数据。 - Web Workers:多线程异步任务,避免阻塞主线程。