在 JavaScript 中,异步编程是我们绕不开的话题。
最早,我们习惯使用 回调函数 来处理异步逻辑,但一旦嵌套过深,就会陷入"回调地狱(callback hell) "。
1. 从回调到 Promise
来看一个最简单的例子:读取两个文件。
javascript
// 回调写法(callback hell)
readFile(fileA, function (err, dataA) { // 调用 readFile,传入 fileA 和回调函数(Node 风格:err 为第一个参数)
if (err) throw err; // 如果发生错误(err 非空),抛出异常(或可替换为更好的错误处理)
console.log(dataA.toString()); // 输出读取到的 dataA 的内容(Buffer -> 以字符串形式显示)
readFile(fileB, function (err, dataB) { // 在第一个回调内再次调用 readFile 读取 fileB ------ 导致嵌套回调
if (err) throw err; // 如果读取 fileB 时出错,抛出异常
console.log(dataB.toString()); // 输出读取到的 dataB 的内容
}); // 结束第二个回调函数
}); // 结束第一个回调函数调用
这段代码一旦文件读取多起来,就会不断向右缩进,难以维护。
于是,Promise
出现了:
javascript
readFile(fileA) // 假设 readFile 返回一个 Promise(或已封装为 Promise 风格)
.then(dataA => { // 当第一个 Promise resolve 时进入 then 回调,dataA 是文件内容
console.log(dataA.toString()); // 打印文件 A 的内容(Buffer -> 字符串)
return readFile(fileB); // 返回另一个 Promise(读取 fileB),以便链式继续
})
.then(dataB => { // 当读取 fileB 的 Promise resolve 时进入这里
console.log(dataB.toString()); // 打印文件 B 的内容
})
.catch(err => console.log(err)); // 捕获上面任一步骤中的拒绝或抛出错误,并打印错误
相比回调,Promise
解决了"回调地狱"的层层嵌套问题,逻辑更清晰。
但如果链条很长,then
的调用依然很多,依旧显得冗余。
2. Generator 让异步更像同步
假设我们期望写出这样的"同步代码":
javascript
function* bar() { // 定义一个 Generator 函数,调用后返回一个 iterator(不会立即执行函数体)
console.log("hi~"); // 第一次调用 iterator.next() 时,会执行到第一个 yield 之前的同步代码并打印 'hi~'
const res1 = yield new Promise(resolve => { // yield 暂停执行,并把右侧的 Promise 交给外部(iterator.next() 的调用者)
setTimeout(() => resolve("hello world!"), 1000); // 创建一个 Promise:1s 后 resolve("hello world!")
}); // 当外部把上面 Promise 的结果传回(iterator.next(result))时,res1 被赋值为该结果
const res2 = yield new Promise(resolve => { // 第二个 yield,同理:暂停并把第二个 Promise 交给外部
setTimeout(() => resolve("yzl!"), 1000); // 1s 后 resolve("yzl!")
}); // 当外部把第二个 Promise 的结果传回时,res2 被赋值为该结果
console.log(res1); // hello world! // 当上面两个 yield 都被外部 resume 后,打印 res1(期望 "hello world!")
console.log(res2); // yzl! // 打印 res2(期望 "yzl!")
} // Generator 函数结束
看起来就像同步的逻辑:一步接一步执行。
但是,问题来了 ------ 如何自动执行这个 Generator 函数?
3. 手动执行 Generator
我们知道 yield
会"暂停"函数执行,把控制权交回调用者。
于是我们可以这样写:
vbnet
const iterator = bar(); // 调用 Generator 函数得到 iterator(此时函数体未执行),iterator 有 next()/throw() 等方法
let it1 = iterator.next(); // 启动 generator 的执行,运行直到第一个 yield 停下
// iterator.next() 返回 { value, done },value 是 yield 表达式右侧的值(这里是第一个 Promise)
it1.value.then(res1 => { // 给第一个 Promise 注册 then ------ 当第一个 Promise resolve 时执行此回调
let it2 = iterator.next(res1); // 把 Promise 的结果 res1 传回 generator:上一个 yield 表达式的返回值就是这个 res1
// iterator.next(res1) 会继续执行 generator,直到第二个 yield 并返回它的 value(第二个 Promise)
it2.value.then(res2 => { // 给第二个 Promise 注册 then ------ 当第二个 Promise resolve 时执行此回调
iterator.next(res2); // 把第二个 Promise 的结果 res2 传回 generator,resume 并完成后续逻辑(如打印)
});
});
这样,res1
和 res2
就能传回到 bar()
里面,输出预期结果。
但这种写法依旧有问题:
- 只能处理固定的
Promise
数量 - 写起来依旧冗余
于是我们希望:能不能写一个自动执行 Generator 的函数?
4. generatorRunner:自动执行 Generator
我们可以写一个简单的执行器,递归调用 next
:
vbnet
const generatorRunner = fn => { // 定义一个执行器:接受一个 generator 函数(fn)作为参数
let iterator = fn(); // 执行 generator 函数以获取 iterator(注意:fn() 可能会执行到第一个 yield 之前的同步代码)
const next = (res = undefined) => { // 定义递归函数 next,用于在 Promise resolve 后继续驱动 generator
let result = iterator.next(res); // 继续执行 generator,把上一步的结果 res 传进去;result = { value, done }
if (result.done) return; // 如果 generator 已完成(done 为 true),直接结束递归
result.value.then(res => next(res)); // 假设 result.value 是 Promise:当它 resolve 时,把 resolve 的值传回 next -> iterator.next(res)
// 注意:这个简化版本没有处理 reject,也没有处理非 Promise 的 yield 值
};
next(); // 启动递归,从第一个 yield 开始
};
generatorRunner(bar); // 使用执行器运行我们前面定义的 generator(bar)
效果 ✅:bar()
中的 Promise
可以依次自动执行。
这种"自动执行 Generator"的模式,就是 Thunk 函数 的思路。
5. Co 库的实现
实际开发中,我们不会自己手写这种执行器,而是使用成熟的库,例如 tj/co。
来看它的简化实现:
javascript
function co(gen) { // co:主入口,支持传入 generator 函数或一个 iterator(generator 对象)
var ctx = this; // 保留当前上下文(可用于绑定 this)
var args = [].slice.call(arguments, 1); // 如果调用 co(gen, arg1, arg2...),把额外参数收集成数组传给 gen
return new Promise(function (resolve, reject) { // co 返回一个 Promise,外部可以用 then/catch 得到结果或错误
if (typeof gen === "function") gen = gen.apply(ctx, args); // 若传入的是 generator 函数,则执行它得到 iterator;并绑定 ctx 与额外参数
if (!gen || typeof gen.next !== "function") return resolve(gen); // 如果不是 iterator(例如普通值),直接 resolve(透明通过)
onFulfilled(); // 开始执行流程:以 onFulfilled 作为第一次驱动(相当于模拟 iterator.next())
function onFulfilled(res) { // 当前步骤成功(或首次启动)要执行的函数;res 是上一个 yield 返回的值
let ret;
try {
ret = gen.next(res); // 把 res 传入 generator,继续执行到下一个 yield,ret = { value, done }
} catch (e) {
return reject(e); // 如果 gen.next 抛错,reject 外层 Promise
}
next(ret); // 统一把 ret 交给 next 处理(会检查 done / 转换 value 等)
return null; // 返回 null 只是为了和某些实现保持一致,不是必须
}
function onRejected(err) { // 当当前 Promise 被 reject 时调用,把错误抛回 generator 内部
let ret;
try {
ret = gen.throw(err); // 将错误抛到 generator 内,让 generator 有机会用 try/catch 捕获
} catch (e) {
return reject(e); // 如果 generator 没有捕获错误,外层 Promise reject
}
next(ret); // 如果 generator 捕获并继续,继续处理返回值 ret
}
function next(ret) { // 处理 gen.next()/gen.throw() 返回的 ret({ value, done })
if (ret.done) return resolve(ret.value); // 如果 generator 完成,resolve 外层 Promise 并返回最终值
let value = toPromise.call(ctx, ret.value); // 把 ret.value 转成一个 Promise(支持多种类型:Promise/Generator/Thunk/Array/Object)
if (value && isPromise(value)) // 如果转换后的 value 看起来是 Promise/thenable
return value.then(onFulfilled, onRejected); // 当 value resolve 时继续 onFulfilled,reject 时走 onRejected
// 如果不能转换为 Promise(不被支持的 yield 类型),则报类型错误并调用 onRejected
return onRejected(
new TypeError("You may only yield a function, promise, generator, array, or object")
);
}
});
}
Co 的核心:
- 自动执行 Generator
- 支持多种 yield 类型(Promise、数组、对象等)
- 异常捕获,避免未处理的错误
这比我们的 generatorRunner
更强大、更通用。
6. 从 Co 到 Async/Await
实际上,async/await
就是 Generator + co
的语法糖。
我们把 bar
改成 async
函数:
javascript
async function barAsync() { // 声明一个 async 函数,调用后立即返回一个 Promise(函数体内部可以使用 await)
console.log("hi~"); // 同步执行:打印 'hi~'(在遇到第一个 await 之前会执行的同步代码)
const res1 = await new Promise((resolve) => { // await 接受 Promise/thenable,等待其 resolve,然后把结果赋值给 res1
setTimeout(() => resolve("hello world!"), 1000); // 创建一个 1s 后 resolve 的 Promise
}); // 当上面的 Promise resolve 后,res1 = "hello world!"
const res2 = await new Promise((resolve) => { // 同理,等待第二个 Promise resolve,并把结果赋值给 res2
setTimeout(() => resolve("yzl!"), 1000); // 创建一个 1s 后 resolve 的 Promise
}); // 当上面的 Promise resolve 后,res2 = "yzl!"
console.log(res1); // hello world! // 打印第一个 await 的结果
console.log(res2); // yzl! // 打印第二个 await 的结果
} // async 函数结束(如果函数有返回值,会作为 resolve 的值返回)
js
barAsync() // 调用 async 函数,返回一个 Promise
.then(() => { // 当 async 函数执行成功(或返回值)时进入 then
// 成功回调
})
.catch(err => { // 如果 async 内部抛错或某个 await 的 Promise reject,会走这里
console.error(err); // 打印或处理错误
});
对比 Generator
版本:
- 不再需要
yield
/next
手动驱动 - 语法更简洁、更直观
- 错误处理也可以直接用
try/catch
这就是为什么 async/await 在 ES2017 正式成为主流方案 的原因。
7. 总结
整个异步编程的演进链路是:
- 回调函数(callback hell,难维护)
- Promise (扁平化代码,但
then
链依旧繁琐) - Generator + Thunk/Co(让异步写法接近同步)
- Async/Await(语法糖,最简洁优雅)
最终,现代 JS 开发中我们大多数场景直接使用 async/await
,而理解 Promise、Generator 和 Co 的原理,可以帮助我们深入掌握 JavaScript 异步的底层机制。