第四章是第三章的"续集"和"解药"。第三章告诉你"异步为什么牛、底层怎么实现的",第四章告诉你"异步这么难写,怎么才能优雅地驾驭它"。朴灵作者把焦点从底层原理转向实践编程模型,系统介绍了从原始回调到现代 async/await 的解决方案演进路径。
这一章的核心思想:控制权反转------把"下一步干什么"交给框架/机制,而不是手动嵌套回调。
我们按小节逐一详细总结,每个细节配解释 + 多例子 + 生动比喻。
4.1 异步编程的优势与难点(第三章3.4已讲,这里略过回顾)
优势:高并发、低资源。
难点:回调地狱、异常难捕获、流程复杂。
生动比喻:回调地狱像"俄罗斯套娃",一层套一层;像"金字塔",越写越斜。
4.2 事件发布/订阅模型(EventEmitter)
核心思想
"一对多"解耦:发布者只管emit事件,订阅者只管on监听,互不干扰。
生动比喻:像广播电台(发布者)喊"新闻来了!",所有收音机(订阅者)都能收到,谁想听就调频。
API 详解
js
const EventEmitter = require('events');
const ee = new EventEmitter();
// 订阅
ee.on('news', (data) => console.log('收到新闻:', data)); // 多次触发
ee.once('alert', () => console.log('紧急警报,只响一次')); // 只一次
// 发布
ee.emit('news', '股市大涨');
ee.emit('news', '天气预报');
ee.emit('alert'); // 只打印一次
// 移除
const handler = () => console.log('移除我');
ee.on('remove', handler);
ee.removeListener('remove', handler);
例子1:文件处理多监听
js
class FileProcessor extends EventEmitter {}
const processor = new FileProcessor();
processor.on('read', (data) => console.log('日志记录:', data.length));
processor.on('read', (data) => console.log('缓存更新'));
processor.on('read', (data) => console.log('响应客户端'));
processor.emit('read', bigBuffer); // 三个监听都执行
例子2:错误约定
js
ee.on('error', (err) => console.error('出错了:', err)); // 必须监听!
ee.emit('error', new Error('boom')); // 不监听会崩溃进程
优势:解耦、灵活、可组合(Stream、http、net底层都用它)。
4.3 Promise/Deferred模式
核心思想
把嵌套回调变成链式,错误统一catch。
生动比喻:回调地狱是"横着爬山",Promise是"顺着绳子往下跳",一步接一步。
Promise 三状态
Pending → Fulfilled / Rejected(不可逆)
当时Deferred模式(第三方库如Q/Bluebird)
js
function readFile(filename) {
const deferred = Q.defer();
fs.readFile(filename, (err, data) => {
err ? deferred.reject(err) : deferred.resolve(data);
});
return deferred.promise;
}
链式使用
js
readFile('a.txt')
.then(data => {
console.log('a:', data);
return readFile('b.txt'); // 返回新Promise,继续链
})
.then(data => {
console.log('b:', data);
throw new Error('出错');
})
.catch(err => console.error('统一捕获:', err)); // 任何一层错误都到这里
例子1:并行Promise.all
js
Promise.all([readFile('a'), readFile('b'), readFile('c')])
.then(([a, b, c]) => console.log('三个文件都读完'))
.catch(err => console.error('任何一个失败就进来'));
例子2:值穿透
js
Promise.resolve(1)
.then(() => '字符串') // 返回非Promise,直接传下一个
.then(str => console.log(str)); // '字符串'
优势:扁平化、错误冒泡、并行控制。
4.4 流程控制库
4.4.1 尾触发与nextTick
同步尾触发会栈溢出,异步用nextTick推到队列尾。
例子:
js
function tail() {
process.nextTick(() => tail()); // 不会栈溢出
}
4.4.2 async库(最经典,至今流行)
- series:串行
- parallel:并行
- waterfall:串行且传值
- auto:复杂依赖
例子1:waterfall串行传值
js
async.waterfall([
cb => fs.readFile('a.txt', cb),
(dataA, cb) => {
console.log(dataA);
fs.readFile('b.txt', cb);
},
(dataB, cb) => cb(null, '最终结果')
], (err, result) => console.log(result));
例子2:parallel并行
js
async.parallel([
cb => fs.readFile('a.txt', cb),
cb => fs.readFile('b.txt', cb)
], (err, [a, b]) => console.log(a, b));
4.4.3 Step库(线性写法)
js
Step(
function() { fs.readFile('a.txt', this); },
function(err, data) {
console.log(data);
fs.readFile('b.txt', this);
}
);
4.4.4 Wind库(编译器风格,接近async/await)
js
var read = eval(Wind.compile("async", function() {
var a = $await(fs.readFileAsync('a.txt'));
var b = $await(fs.readFileAsync('b.txt'));
console.log(a, b);
}));
read().start();
4.5 现代补充:async/await(终极形态)
所有方案的集大成:
js
async function main() {
try {
const a = await readFile('a.txt');
const b = await readFile('b.txt');
console.log(a, b);
} catch (err) {
console.error(err); // 统一捕获
}
}
main();
生动比喻:
- 回调:横着爬山(地狱)
- EventEmitter:广播喊人
- Promise:顺绳子跳(链式)
- async/await:直接走平路(像同步)
总结与收获
第四章告诉你:异步难写,但有层层递进的解决方案。从事件解耦,到Promise链式,再到流程库控制,最后async/await让代码重回"同步美感"。