异步编程
单线程模式
JavaScript 引擎是单线程同步进行的,即每次仅能处理一个事件。当处理多个事件时,则需要排队等待执行。因此当一个事件在执行时,将阻塞后续的事件执行。 这在实际的浏览器交互中将是极差的用户体验,比如远古时代当用户提交一个表单时,浏览器将等待表单提交动作完成才能进行下一步操作,期间对于用户而言浏览器一直是卡死的一个状态。 同时单线程机制也不适应如今多核 CPU 的环境,无法充分利用 CPU 资源,效率低效,程序运行时间长。
为了解决此类问题,JavaScript 引入了异步(Asynchronous)的执行模式,如 ajax 操作。了解异步编程的方式,可以帮助我们编写更加合理出色的 JavaScript 程序。
回调函数
回调函数是异步操作最基本的方法,也是应用及其广泛的存在。在此借用 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()
}
// Promise
function 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
It is a stepping stone towards the async/await proposal —————— 引用于 tj/co
由 Generator + co
的异步操作为基础,ES6 小版本(ES2017)引入了新标准 async 函数。
在异步处理上, 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
方式更加语义化,除了例子中统一处理错误的方式,我们还可以针对某个一步操作进行错误处理:
;async () => await mkdir(dirPath).catch(err => errHandler(err))