Javascript 异步编程

同步 & 异步

  • 单线程同步编程:每个任务按照先后顺序依次执行(阻塞)
  • 多线程模型:每一个任务由独立的线程控制,这些线程由操作系统管理。在一个多处理器或多核环境中可以真正实现多线程;在单处理器中,通过任务交替执行实现并发(非阻塞)

由于一些比较古老的原因,Javascript 是单线程的,也就是说在同一时刻,浏览器进程中只有一个 Javascript 的线程在执行,并且阻塞其他任务执行
为什么 Javascript 是单线程的:

  • 轻量
  • 简化并发模型,无死锁
  • 没有线程切换开销

但是如果碰到耗时比较长的任务,比如读取文件内容,会导致后面的任务无法继续进行下去。Javascript 在后期的发展中采用异步非阻塞编程模式解决多线程并行的问题,一个异步过程可以表述如下:

  • 主线程发起一个异步请求,相应的 Worker 接收请求并告知主线程已收到请求(异步函数返回)
  • 主线程可以继续执行后面的代码,同时 Worker 执行异步任务
  • Worker 完成工作后,通知主线程
  • 主线程收到通知后,执行一定的动作(调用回调函数)

异步的实现

现在,我们定义一个函数,用来读取指定目录下的全部文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 阻塞方式
const readDirSync = () => {
const fileList = fs.readdirSync(publicPath);
const content = [];
fileList.forEach((fileName) => {
console.log('in loop:', fileName);
content.push(fs.readFileSync(path.resolve(publicPath, fileName), 'utf8').replace('\n', ''));
});
console.log('done:', content.join(', '));
};

// in loop: text.txt
// in loop: text2.txt
// done: This is a text file, This is a text file 2

回调函数

下面用回调函数的方式实现读取指定目录下的全部文件内容,回调函数将耗时的执行推迟到了后面执行实现了异步,没有阻塞后面函数的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const readDirCallback = () => {
fs.readdir(publicPath, (err, fileList) => {
if (err) throw new Error(`Read Dir '${publicPath}' Error`);
fileList.forEach((fileName) => {
const filePath = path.resolve(publicPath, fileName);
fs.readFile(filePath, 'utf8', (error, file) => {
if (error) throw new Error(`Read File '${filePath}' Error`);
console.log(file.replace('\n', ''));
});
});
});
};

// This is a text file
// This is a text file 2

但是,上面的回调函数仅仅嵌套了两层,假设我们要实现更为复杂的功能,那么也许就会出现 fn1(fn2(fn3(fn4(...)))); 这样的情况,传说中的 ‘Callback Hell’ 就会由此产生。回调函数最大的问题是函数之间高度耦合,维护困难

优点:

  • 简单
  • 利于理解、部署

缺点:

  • 可读性差
  • 可维护性差
  • 无法统一处理异常
  • 无法灵活控制流程

事件监听

DOM 事件的异步方式

1
2
3
4
const btn = document.querySelector('#button');
btn.addEventListener('click', () => { console.log('Button Clicked') });

// Button Clicked

订阅/发布

Node.js 提供 events 模块,通过 EventEmitter 实例来触发事件,绑定在该事件上的函数被同步调用,所以需要确保事件的正确排序且避免竞争条件或逻辑错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const readPubSub = (ctx) => {
const emitter = new events.EventEmitter();
fs.readdir(publicPath, (err, fileList) => {
if (err) throw new Error(`Read Dir '${publicPath}' Error`);
console.log(fileList);
emitter.emit('readFile', fileList);
});
emitter.on('readFile', (fileList) => {
fileList.forEach((fileName) => {
const filePath = path.resolve(publicPath, fileName);
fs.readFile(filePath, 'utf8', (err, file) => {
if (err) throw new Error(`Read File '${filePath}' Error`);
console.log(file.replace('\n', ''));
});
});
});
};

// [ 'text.txt', 'text2.txt' ]
// This is a text file 2
// This is a text file

监听器函数可以使用 setImmediate()process.nextTick() 方法切换到异步操作模式:

1
2
3
4
5
6
7
const myEmitter = new MyEmitter();
myEmitter.on('event', (a, b) => {
setImmediate(() => {
console.log('这个是异步发生的');
});
});
myEmitter.emit('event', 'a', 'b');

🍭更多关于 EventEmitter

Promise

用 Promise 实现读取指定目录下文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const readPromise = () => {
const getFileList = () => new Promise((resolve, reject) => {
fs.readdir(publicPath, (err, fileList) => (err ? reject(err) : resolve(fileList)));
});
const getFileContent = filePath => new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, content) => (err ? reject(err) : resolve(content.replace('\n', ''))));
});
getFileList().then(fileList => Promise.all(fileList.map((fileName) => {
const filePath = path.resolve(publicPath, fileName);
return getFileContent(filePath);
}))).then((result) => {
console.log(result.join(', '));
}).catch((err) => {
throw err;
});
};

// This is a text file, This is a text file 2

与回调函数方式实现的异步相比,这段代码在视觉上清晰了许多,Promise 是典型的 Monad 风格,采用链式调用,每次产生新的 thenable 对象
但是,Promise 并没有真正消除 callback,只是利用 then() 延迟了 callback 的绑定

优点:

  • 关注点分离
  • 一次性
  • 可以单独处理异常,也可以统一处理

缺点:

  • “贪心”
  • 无法取消
  • 无法访问链式调用内部的值

Genertor

Generator 函数可以彻底消除 callback,看起来 Generator 与订阅/发布模式有些类似,都是通过主动调用的方式,告诉程序什么时候该去执行下一步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const readGenerator = () => {
function* ReadFile(fileList) {fileList.forEach((fileName) => {
const filePath = path.resolve(publicPath, fileName);
fs.readFile(filePath, 'utf8', (err, file) => {
if (err) throw new Error(`Read File '${filePath}' Error`);
console.log(file.replace('\n', ''));
});
});
}
function* ReadDir() {
yield fs.readdir(publicPath, (err, fileList) => {
if (err) throw new Error(`Read Dir '${publicPath}' Error`);
console.log(fileList);
const readFile = ReadFile(fileList);
readFile.next();
});
}
const readDir = ReadDir();
readDir.next();
};

// [ 'text.txt', 'text2.txt' ]
// This is a text file
// This is a text file 2

🍭Generator MDN 文档

Generator 离不开🍭co 函数库,核心代码:

1
2
3
4
5
6
7
8
co(function* () {
var result = yield Promise.resolve(true);
return result;
}).then(function(value) {
console.log(value);
}, function(err) {
console.error(err.stack);
});

Async/Await

终于轮到 Async/Await 了,Async/Await 可以说是 javascript 异步编程最为优雅的一种实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const readAsync = async () => {
const getFileList = async () => new Promise((resolve, reject) => {
fs.readdir(publicPath, (err, fileList) => (err ? reject(err) : resolve(fileList)));
});
const getFileContent = async filePath => new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, content) => (err ? reject(err) : resolve(content.replace('\n', ''))));
});
const fileList = await getFileList();
const content = await Promise.all(fileList.map(async (fileName) => {
const filePath = path.resolve(publicPath, fileName);
try {
return await getFileContent(filePath);
}
catch (err) {
console.log(err);
return err;
}
}));
console.log(content.join(', '));
};

// This is a text file, This is a text file 2

Async/Await 看上去与 Promise 及其相似,不同的是 Async/Await 帮助我们处理了 then 链,同时,也不需要像 Generator 一样需要自己去维护内部 yield 的执行

优点:

  • 代码结构清晰
  • 语义化友好
  • 有利于代码维护