JavaScript 异步编程
date
Mar 10, 2018
slug
js-async-programming
status
Published
tags
JavaScript
summary
type
Post
1. JavaScript 天生异步
你说我一个浏览器写写表单验证的,怎么就突然成为如今最流行的编程语言了呢?
JavaScript 设计之初是用于浏览器端 GUI 编程,这就决定了这门语言是单线程、非阻塞的。而 JavaScript 正是通过异步执行任务来实现非阻塞。
关于 JavaScript 异步机制和 Event loop 详细可见:Help, I'm stuck in an event-loop
2. 异步函数的类型
JavaScript 环境本身提供的异步函数通常可以分为两大类:
- I/O 函数
- 计时函数
如果想在应用中自定义复杂的异步任务,就需要在两类异步函数上构建。
3. 异步解决方案
3.1 回调
一开始,JS 中的异步是通过回调实现的。如果想让某段代码将来执行,可以将它放在一个回调函数中。例如下面的 node 代码,只有在文件读取完毕后,
'finished'
才会被打印。const fs = require('fs');
fs.readFile('/etc/passwd', (err, result) => {
console.log('finished');
})
但是随着应用变得复杂,我们有许多异步事件需要处理,并且需要数据从一个事件传递到下一个事件,那么回调函数就会变得这样
step1(function(result1) {
step2(function(result2) {
step3(function(result3) {
// ...
});
});
});
这样的代码被称为
Callback Hell
(回调地狱),回调地狱主要以下有以下几大罪状- 代码丑陋,不符合人类阅读习惯
- 异常难以捕获
try/catch
是同步代码,上面的 step 函数运行时,try/catch
已经执行完毕,异常并不能被捕获。- 代码容易产生冗余
假设我们还有一个不同的操作需要在 step1 之后完成,那么得再来一段
Callback Hell
了……3.2 Pub/Sub
Pub/Sub(发布/订阅)模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
我觉得 Pub/Sub 模式和蝴蝶效应很像:某个事件被触发,整个应用都受到影响。
Pub/Sub 模式可以很好的解决回调地狱产生的代码冗余的问题。
DOM 事件就是很典型的 Pub/Sub 模式。例如下面的点击事件。
button.addEventListener('click', () => {
console.log('the button is clicked');
}, false);
这里我们相当于订阅了
button
上面的点击事件,当用户点击之后,这个按钮就会向订阅者发布这个消息。3.3 Promise
事件(click, keyup)和 Pub/Sub 模式对于同一对象上发生多次的事情非常有用,但是关系到异步事件执行的成功或者失败,Pub/Sub 模式没有提供一个好的解决方案。Promise 很好的解决了这个问题:
// 假设`ready()`返回一个 Promise.
img.ready().then((result)=> { console.log('success!'); }, (err) => { console.log('failed!'); })
当然,Promise 在异步事件执行方面的优点不仅于此。
Promise 最早由社区提出和实现,常见的 Promise 的第三方库有
而官方则在 ES6 正式支持 Promise,并采用了 Promises/A+ 规范。
Promise 为什么叫 Promise 呢,我觉得 MDN 上面关于 Promise 的中文“翻译”很好的解释了这一点🙃:
**Promise **对象用于一个异步操作的最终完成(或失败)及其结果值的表示。(简单点说就是处理异步请求。我们经常会做些承诺,如果我赢了你就嫁给我,如果输了我就嫁给你之类的诺言。这就是promise的中文含义:诺言,一个成功,一个失败。)
原文:The Promise object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.
Promise 通过链接多个
then()
来处理多个异步操作,比回调地狱优雅很多:aPromiseStuff().then((result) => {
return doPromiseStuff();
}).then((result) => {
return doAnotherPromiseStuff();
}).catch((err) => {
console.log(err);
});
关于 Promise 的更多内容可以查看 MDN 上面的教程:Promise
还有两篇关于 Promise 的文章很值得一读:
3.4 Generator
Generator Function 和 Generator 也是 ES6 引入的新特性。
function*
这种声明方式用来定义一个 Generator Function,后者会返回一个 Generator 对象。当一个 Generator Function 被调用时并不会马上执行;相反,它会返回一个 Generator 对象。每次调用 Generator 对象的
next()
方法将会执行函数至下一个yield
表达式,并返回一个符合迭代器协议的对象,包含value
和done
两个属性。value
的值为yield
表达式的运行结果,函数运行结束时其值为undefined
;done
的值表示函数是否运行结束。function* simpleGenerator(){
yield "first";
yield "second";
yield "third";
for (var i = 0; i < 3; i++)
yield i;
}
var g = simpleGenerator();
console.log(g.next()); // { value: "first", done: false }
console.log(g.next()); // { value: "second", done: false }
console.log(g.next()); // { value: "third", done: false }
console.log(g.next()); // { value: 0, done: false },
console.log(g.next()); // { value: 1, done: false },
console.log(g.next()); // { value: 1, done: false },
console.log(g.next()); // { value: undefined, done: true },
Generator Function 这种可以暂停执行和恢复执行的特性,使它能够有处理异步任务的能力。可是光有一个 Generator Function 还不够,它还需要有一个执行器来执行它所封装的异步任务:
function doSomething() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 500);
});
}
function* handleAsynchronousStuff() {
try {
const val = yield doSomething();
console.log(val);
} catch(e) {
console.log(e);
}
}
万事具备,只欠一个co:
co:Generator based control flow goodness for nodejs and the browser, using promises, letting you write non-blocking code in a nice-ish way.
const co = require('co');
co(handleAsynchronousStuff) // 1
我们再来看看 co 做了什么事:
co 的功能其实不算复杂,总共也就 200 多行代码。它不断递归拆解 generator function 中的 yield 表达式,并返回一个 Promise。它做的事情就是执行用 Generator 封装好的异步任务。
Generator 错误处理
generator 对象有一个 throw 方法,可以在 generator function 外面抛出异常,并且能够在 generator function 中使用
try/catch
捕获异常,详细内容可见 MDN 文档:Generator.prototype.throw()那么,使用 Generator 处理异步任务可以优雅的捕获异常吗?答案是肯定的,我们再来啃一啃 co 的核心函数的代码:
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see <https://github.com/tj/co/issues/180>
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
// generator function 执行完毕,Promise 状态变为 resolve
if (ret.done) return resolve(ret.value);
// value => Promise
var value = toPromise.call(ctx, ret.value);
// 如果 value 成功转变为 Promise,则通过`Promise.then()`继续拆解 generator function,并为Promise 添加`onFulfilled`和`onRejected`
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
可以看到,它对 generator function 的异常处理封装在了
onRejected()
函数当中:如果发生错误,则将返回的 Promise 的状态变为reject
,再调用next()
继续拆解 generator function。通过 co 的处理,异步函数中的异常成功通过
gen.throw()
抛出,那么我们就可以跟同步代码一样使用try/catch
捕获异常了:function doSomething() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('something is wrong')
}, 500);
});
}
function* handleAsynchronousStuff() {
try {
const val = yield doSomething();
console.log(val);
} catch(e) {
console.log('Error: ', e);
}
}
co(handleAsynchronousStuff); // Error: something is wrong
嗯……不得不说 Generator 使异步处理的过程更加优雅了。但是 Generator 本身并不是专门用来处理异步任务的,而且在使用 Generator 这种方案时,还得引入第三方模块 co,总觉得有点变扭。
ES7:那就来个语法糖把它们包装一下吧!
async/await
ES7 正式引入
async/await
,它本质上就是 Generator 解决方案的语法糖,并且内置了执行器,上面的handleAsynchronousStuff()
可以这么写:async function handleAsynchronousStuff() {
try {
const val = await doSomething();
console.log(val);
} catch(e) {
console.log('Error: ', e);
}
}
handleAsynchronousStuff();
我觉得
async/await
相较于 Promise 的最直观的优点就是代码的可阅读性大大的提高了。在过去,我们需要链式地写then()
来处理Promise()
的resolve
值,逻辑一复杂,嵌套的代码就越来越多,而async/await
则让我们可以像写同步代码一样来写异步代码。关于
async/await
与Promise
更详细的对比,可以见这篇文章:小结
说到底,
async/await
就是基于 Promise
和Generator
的,要用好async/await
,就必须先理解Promise
和Generator
。这篇文章正是在阐述这样一个观点:在使用
async/await
之前,先理解Promise
说来惭愧,再还没有真正理解
Promise
和Generator
之前我就已经在跟风使用async/await
了,如今正是在恶补 JavaScript 异步解决方案的发展历程。现在再来看async/await
,发现 JavaScript 这门语言的发展有很大一部分都是依赖于开源社区的贡献,不得不感叹开源社区的力量之强大。参考资料
- 《JavaScript 异步编程:设计快速响应的网络应用》
- MDN