单线程模式

JavaScript 引擎是单线程同步进行的,即每次仅能处理一个事件。当处理多个事件时,则需要排队等待执行。因此当一个事件在执行时,将阻塞后续的事件执行。
这在实际的浏览器交互中将是极差的用户体验,比如远古时代当用户提交一个表单时,浏览器将等待表单提交动作完成才能进行下一步操作,期间对于用户而言浏览器一直是卡死的一个状态。
同时单线程机制也不适应如今多核CPU的环境,无法充分利用CPU资源,效率低效,程序运行时间长。

为了解决此类问题,JavaScript引入了异步(Asynchronous)的执行模式,比如 ajax 操作。了解异步编程的方式,可以帮助我们编写更加合理出色的JavaScript程序。

回调函数

回调函数是异步操作最基本的方法,也是应用及其广泛的存在。作为 Node 异步编程的直接体现,使用 Node.fs 作为例子:

  1. const fs = require("fs");
  2. const { resolve } = require("path");
  3. const dirPath = resolve(__dirname, "./directory");
  4. function errHandler(err) { /* handle error */ }
  5. fs.mkdir(dirPath, (err, data) => {
  6. if (err) {
  7. errHandler(err);
  8. // return or not
  9. }
  10. fs.writeFile(`${dirPath}/demo.txt`, "Hello World!", "utf8", (err, data) => {
  11. if (err) {
  12. errHandler(err);
  13. // return or not
  14. }
  15. fs.readFile(`${dirPath}/demo.txt`, "utf8", (err, data) => {
  16. if (err) {
  17. errHandler(err);
  18. // return or not
  19. }
  20. console.log(data);
  21. });
  22. });
  23. });
  24. // Hello World!

可以看出回调函数的缺点也很明显:

  • 只能指定一个回调任务,当业务复杂时,不得不写出多个嵌套的回调函数而陷入回调地狱(Callback Hell)
  • 每个回调事件中的错误处理都需要单独判断处理,事件流程控制也显得复杂
  • 嵌套的回调函数不仅代码可读性差,其代码结构也高度耦合。即使我们可以将各个部分封装抽离,对于或者流程追踪也显得异常棘手。

Promise

为了解决回调地狱,社区提出并实现了 Promise 方案,并在ES6中规范化提供原生Promise对象。

  1. const mkdir = function (path) {
  2. return new Promise((resolve, reject) => {
  3. fs.mkdir(path, (err, data) => (err ? reject(err) : resolve(data)));
  4. });
  5. };
  6. const writeFile = function (path, msg, type = 'utf8') {
  7. return new Promise((resolve, reject) => {
  8. fs.writeFile(path, msg, type, (err, data) =>
  9. err ? reject(err) : resolve(data)
  10. );
  11. });
  12. };
  13. const readFile = function (path, type = 'utf8') {
  14. return new Promise((resolve, reject) => {
  15. fs.readFile(path, type, (err, data) => (err ? reject(err) : resolve(data)));
  16. });
  17. };
  18. mkdir(dirPath)
  19. .then(() => writeFile(`${dirPath}/demo.txt`, 'Hello World!'))
  20. .catch(writeErr => errHandler(writeErr));
  21. .then(() => readFile(`${dirPath}/demo.txt`))
  22. .catch(readErr => errHandler(readErr));
  23. .then((data) => console.log(data), err => errHandler(err));
  24. // Hello World!

Promise.then 使用 链式调用 技巧解决了回调地狱,并且对于错误处理,有对应的状态/方法处理,使得异步编程更加清晰。

但是大量的 Promise.then/catch 使代码显得冗余,且当 Promise 处于 pending 状态时,我们无法判断异步事件的执行阶段,更无法中断其执行,不利用控制流程。

Generator + co

在ES6中使用 Generator 实现了协程:一种类似于线程,但交替执行的程序运行方式。

  1. function* gen() {
  2. yield mkdir(dirPath);
  3. yield writeFile(`${dirPath}/demo.txt`, "Hello World!");
  4. yield readFile(`${dirPath}/demo.txt`);
  5. }
  6. const scheduler = gen();
  7. scheduler.next().value.catch((err) => errHandler(err));
  8. scheduler.next().value.then(() => {
  9. const result = scheduler.next();
  10. result.value.then((res) => console.log(res));
  11. });
  12. // Hello World!

在使用 Generator 封装文件操作后,其内部就像是在执行同步操作,并且 Generator 总是返回一个 Iterator 对象,使得我们可以控制流程的执行。内部异步操作仍是 Promise 接口,亦不影响我们进行错误处理。

不过手动单步执行以及嵌套取值的方式并非所愿,我们需要借助 co 库为 Generator 函数做自动执行的封装。
co 模块的原理很简单,利用递归 + 回调函数(体现为Thunk函数)或 Promise 封装异步操作,在异步回调中继续执行下一步异步操作,直到完成 Generator 函数的执行。

  1. // Thunk 函数
  2. function thunkLauncher(gen) {
  3. const g = gen();
  4. function next(data) {
  5. const res = g.next(data);
  6. if (res.done) {
  7. return res.value;
  8. }
  9. res.value(next);
  10. }
  11. next();
  12. }
  13. // Promise
  14. function promiseLauncher(gen) {
  15. const g = gen();
  16. function next(data) {
  17. const res = g.next(data);
  18. if (!res.done) {
  19. return res.value;
  20. }
  21. res.value.then((data) => next(data));
  22. }
  23. next();
  24. }

大致实现如上,基于Promise的支持更广泛且实用性广,也是 co 模块后续重构选择的实现方式。

async / await

由 Generator + co 的异步操作为基础,ES6小版本(ES2017)引入了新标准 async 函数。

It is a stepping stone towards the async/await proposal —————— 引用于 tj/co

在异步处理上,async 函数就是 Generator 函数的语法糖。
async 函数返回一个 Promise 对象,配合 await 命令(Promise.then 的语法糖)执行异步操作:

  1. async function fileScheduler() {
  2. try {
  3. await mkdir(dirPath);
  4. await writeFile(`${dirPath}/demo.txt`, "Hello World!");
  5. const result = await readFile(`${dirPath}/demo.txt`);
  6. console.log(result);
  7. } catch (err) {
  8. errHandler(err);
  9. }
  10. }
  11. (async () => await fileScheduler())();
  12. // Hello World;

async/await 的写法,就相当于只写了 Generator 函数:* => asyncyield => await
且包含了 co 自动执行器的机制,使得异步编程更加方便,代码层面就如同同步操作一般。

除此之外,async/await 方式更加语义化,await 命令作为 Promise.then 语法糖,因此除了例子中统一处理错误的方式,我们还可以针对某个一步操作进行错误处理:

  1. async () => await mkdir(dirPath).catch(err => errHandler(err));