单线程模式
JavaScript 引擎是单线程同步进行的,即每次仅能处理一个事件。当处理多个事件时,则需要排队等待执行。因此当一个事件在执行时,将阻塞后续的事件执行。
这在实际的浏览器交互中将是极差的用户体验,比如远古时代当用户提交一个表单时,浏览器将等待表单提交动作完成才能进行下一步操作,期间对于用户而言浏览器一直是卡死的一个状态。
同时单线程机制也不适应如今多核CPU的环境,无法充分利用CPU资源,效率低效,程序运行时间长。
为了解决此类问题,JavaScript引入了异步(Asynchronous)的执行模式,比如 ajax 操作。了解异步编程的方式,可以帮助我们编写更加合理出色的JavaScript程序。
回调函数
回调函数是异步操作最基本的方法,也是应用及其广泛的存在。作为 Node 异步编程的直接体现,使用 Node.fs 作为例子:
const fs = require("fs");const { resolve } = require("path");const dirPath = resolve(__dirname, "./directory");function errHandler(err) { /* handle error */ }fs.mkdir(dirPath, (err, data) => {if (err) {errHandler(err);// return or not}fs.writeFile(`${dirPath}/demo.txt`, "Hello World!", "utf8", (err, data) => {if (err) {errHandler(err);// return or not}fs.readFile(`${dirPath}/demo.txt`, "utf8", (err, data) => {if (err) {errHandler(err);// return or not}console.log(data);});});});// Hello World!
可以看出回调函数的缺点也很明显:
- 只能指定一个回调任务,当业务复杂时,不得不写出多个嵌套的回调函数而陷入回调地狱(Callback Hell)
- 每个回调事件中的错误处理都需要单独判断处理,事件流程控制也显得复杂
- 嵌套的回调函数不仅代码可读性差,其代码结构也高度耦合。即使我们可以将各个部分封装抽离,对于或者流程追踪也显得异常棘手。
Promise
为了解决回调地狱,社区提出并实现了 Promise 方案,并在ES6中规范化提供原生Promise对象。
const mkdir = function (path) {return new Promise((resolve, reject) => {fs.mkdir(path, (err, data) => (err ? reject(err) : resolve(data)));});};const writeFile = function (path, msg, type = 'utf8') {return new Promise((resolve, reject) => {fs.writeFile(path, msg, type, (err, data) =>err ? reject(err) : resolve(data));});};const readFile = function (path, type = 'utf8') {return new Promise((resolve, reject) => {fs.readFile(path, type, (err, data) => (err ? reject(err) : resolve(data)));});};mkdir(dirPath).then(() => writeFile(`${dirPath}/demo.txt`, 'Hello World!')).catch(writeErr => errHandler(writeErr));.then(() => readFile(`${dirPath}/demo.txt`)).catch(readErr => errHandler(readErr));.then((data) => console.log(data), err => errHandler(err));// Hello World!
Promise.then 使用 链式调用 技巧解决了回调地狱,并且对于错误处理,有对应的状态/方法处理,使得异步编程更加清晰。
但是大量的 Promise.then/catch 使代码显得冗余,且当 Promise 处于 pending 状态时,我们无法判断异步事件的执行阶段,更无法中断其执行,不利用控制流程。
Generator + co
在ES6中使用 Generator 实现了协程:一种类似于线程,但交替执行的程序运行方式。
function* gen() {yield mkdir(dirPath);yield writeFile(`${dirPath}/demo.txt`, "Hello World!");yield readFile(`${dirPath}/demo.txt`);}const scheduler = gen();scheduler.next().value.catch((err) => errHandler(err));scheduler.next().value.then(() => {const result = scheduler.next();result.value.then((res) => console.log(res));});// Hello World!
在使用 Generator 封装文件操作后,其内部就像是在执行同步操作,并且 Generator 总是返回一个 Iterator 对象,使得我们可以控制流程的执行。内部异步操作仍是 Promise 接口,亦不影响我们进行错误处理。
不过手动单步执行以及嵌套取值的方式并非所愿,我们需要借助 co 库为 Generator 函数做自动执行的封装。
co 模块的原理很简单,利用递归 + 回调函数(体现为Thunk函数)或 Promise 封装异步操作,在异步回调中继续执行下一步异步操作,直到完成 Generator 函数的执行。
// Thunk 函数function thunkLauncher(gen) {const g = gen();function next(data) {const res = g.next(data);if (res.done) {return res.value;}res.value(next);}next();}// Promisefunction promiseLauncher(gen) {const g = gen();function next(data) {const res = g.next(data);if (!res.done) {return res.value;}res.value.then((data) => next(data));}next();}
大致实现如上,基于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 的语法糖)执行异步操作:
async function fileScheduler() {try {await mkdir(dirPath);await writeFile(`${dirPath}/demo.txt`, "Hello World!");const result = await readFile(`${dirPath}/demo.txt`);console.log(result);} catch (err) {errHandler(err);}}(async () => await fileScheduler())();// Hello World;
async/await 的写法,就相当于只写了 Generator 函数:* => async,yield => await
且包含了 co 自动执行器的机制,使得异步编程更加方便,代码层面就如同同步操作一般。
除此之外,async/await 方式更加语义化,await 命令作为 Promise.then 语法糖,因此除了例子中统一处理错误的方式,我们还可以针对某个一步操作进行错误处理:
async () => await mkdir(dirPath).catch(err => errHandler(err));
