我所知道的JavaScript异步编程
前言
没有搞定异步编程的JS开发者不是称职的开发者
。
入门JS算是一年了,从当时直接使用回调到后来开始大量使用async库,期间冒出的promise
、generator
都完全没有去管它。然后然后最近就被鄙视了一番(哭泣。。。。)。所以趁着刚被人鄙视完,奋发图强,好好把Js的异步编程学起来,以后去鄙视别人(哈哈哈。。。。)。
这篇文章将以代码为主,辅以解释,demo代码地址:js-async-program-demo
demo介绍
该demo是基于Express服务器框架,然后异步编程都是体现在服务器端。我们在route/index.js
中处理来自客户端的请求,然后在后台中去异步请求一些信息,之后返回这些信息。然后我将使用目前JS所有原生的异步编程方式来实现这个目的,从代码中也许可以找到你想学习的信息。
在最后一个版本中使用到了async/await
,但是因为Nodejs不支持,所以需要Babel的转译,于是便引进了babel-register
来实时编译,更多关于babel
的学习可以参考Babel6的学习新姿势
1、callback
首先是最古老最原始的异步方式-回调。作为目前最传统的方式,这种异步编程方式仍存在于大家的代码中,毕竟太过经典。但是为了更加优雅地写出漂亮的异步代码,为了展示你高逼格的代码,请在以后的异步编程中弃用它吧,否则你就可能写出demo中类似的代码:
router.get('/', function(req, res, next) {
const finalRes = res;
request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
return finalRes.render('index', {title: 'express', result: body});
});
});
});
});
});
});
所以传统的回调方式缺点有:
- 回调地狱,也就是层层嵌套,难以书写出漂亮的代码
- 难以调试追踪
- 难以处理某一个回调错误
2、promise
2.1、简短的历史
promise这个概念并不是JS特有的,很早之前C++就有这个概念,之后Python也有了。JS第一次出现Promise的概念可以追溯到2007年,在一个JS库里-MochiKit
。然后Dojo
引用了它,之后jquery
也开始使用了。然后CommonJS组织提出了Promises/A+
规范。在其早期形成时,NodeJs开始使用它了。如今promises已经成为了ES6的标准,V8引擎早已经原生地实现了。
2.2、概念
顾名思义,它的中文翻译是承诺
,那就代表它会在当前或者未来的某一时刻做出它承诺的事情来,至于做出什么来便是取决于你给其提供的resolved
或rejected
。
在MDN中的定义是:
**Promise**是一个代理一个不需要知道值的对象。它允许你去关联操作到一个异步操作上的成功或者失败行为。这样可以像同步操作那样返回值,不过返回的值依然是一个promise对象,而且是在未来的某个时刻。
Promise
是一个对象,其对象结构如图:
Promise
的三大状态就不细说了,我们可以借助浏览器大致看一下其三大状态的变化:(记住是不可逆的)
代码如下:
状态变化如下:
在一个promise中pending
之后的状态要么是resolved,要么rejected。当任意一个条件满足的时候,通过调用then
方法可以将相关操作移出队列并执行。此时即使你不调用then
方法的话,其结果都会存在而不会消失(除非你destroy了),好比是promise帮你暂存了信息一样。
2.3、实例演示
在demo中,我们改写之前的回调写法,使用promise来实现异步获取URL:
router.get('/promise', function(req, res, next) {
let reqApi = new Promise((resolve, reject) => {
request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
if (err){
reject(err);
} else{
resolve(body);
}
});
});
reqApi
.then((body) => reqApi)
.then((body) => reqApi)
.then((body) => reqApi)
.then((body) => reqApi)
.then((body) => {
return res.render('index', {title: 'express', result: body});
})
.catch(err => {
console.log(err)
})
});
我们异步多次请求URL,不过这样写的话算是“异步中的同步”,因为下一个URL获取是建立在上一个URL获取的前提下。当这些请求无关的时候我们可以使用promise.all
方法:
router.get('/promiseAll', function(req, res, next) {
let reqApi = new Promise((resolve, reject) => {
request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
if (err){
reject(err);
} else{
resolve(body);
}
});
});
Promise.all([reqApi, reqApi, reqApi, reqApi, reqApi])
.then((body) => {
return res.render('index', {title: 'express', result: body});
})
.catch(err => {
console.log(err)
})
});
这个方法是让所有的promise都并行进行,然后如果所有的promise都被resolved了的话才返回resolved,如果只要任意一个rejected,那么就会立即返回rejected。成功的话按照给定参数的列表顺序返回对应的结果数组。
Promise里还有一个API叫做Promise.race
,顾名思义就是竞争赛跑的意思,也就是说只要在所有的promise中有一个resolved或者rejected,那么就立刻返回,所以有点争上游的意思。可以参考这段代码理解一下:
router.get('/promiseRace', function(req, res, next) {
let reqApi = new Promise((resolve, reject) => {
request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
if (err){
reject(err);
} else{
resolve(body);
}
});
});
Promise.race([reqApi, reqApi, reqApi, reqApi, reqApi])
.then((body) => {
return res.render('index', {title: 'express', result: body});
})
.catch(err => {
console.log(err)
})
});
2.4、总结
promise总体上对于异步编程的友好性来说更上一层楼的,可以更好地控制我们的代码流。但是事物是有两面性的,Promise依然有一些缺点:
- 代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚
- 当异步变多的时候会有很多的then 方法,整块代码会充满Promise 的方法,而不是业务逻辑本身
- 每一个then 方法内部是一个独立的作用域,要是想共享数据,就要将部分数据暴露在最外层,在then 内部赋值一次。
3、Generator
与Promise一起作为ES6标准出来的还有Generator。generator的设计允许对你隐藏异步的实现细节然后提供给你单线程,类同步风格的代码。这可以让我们以非常自然的方式表达我们程序的步骤。
3.1、概念
定义一个generator函数
可以返回一个generator对象g
(假设是这个名称),该对象遵循了迭代协议和迭代器协议。换句话说就是可以使用Array.from(g)
, [...g]
或for value of g
来进行迭代循环。
Generator函数允许你声明一个特殊类型的迭代器,每一次迭代就代表你的代码的一次休眠(也就是将执行权交还)。
generator函数
的定义都必须在function后面加个*
以示区别,比如:
此时g
的对象结构如图:
Tips
Symbol.iterator:这个也是ES6的新特性,简单说这个相当于迭代器的接口,只有对象里有这个symbol的属性,才可以认为此对象是可迭代的。只要一个对象实现了正确的 Symbol.iterator 方法,那么它就可以被 for in 所遍历。
@@iterator:等价于Symbol.iterator
.next()
每次都返回一个done
属性和value
值,前者指示是否已经迭代完成,后者指示当前结果。
如图:
这里注意有两个结果需要区分:一个是next()执行的结果另外一个是yield表达式返回的结果。在后面的例子中我们会细说一下区别。
另外值得注意的是上下文在休眠和恢复的时候都会被保存着,这意味着generators是有状态的。
3.2、如何使用它来实现异步编程
那么根据generator的使用方法应该来设计一个异步操作呢?根据异步的返回类型我们设计出两个demo:
3.2.1、callback版本的generator
router.get('/generator', function(req, res, next) {
const finalRes = res;
function* generator(){
let val = [];
function request_g(url){
request(url, (err, res, body) => {
if (err) {
g.throw(err)
}
g.next(body);
});
}
val[0] = yield request_g('https://api.douban.com/v2/user/linxiaowu');
val[1] = yield request_g('https://api.douban.com/v2/user/linxiaowu');
val[2] = yield request_g('https://api.douban.com/v2/user/linxiaowu');
val[3] = yield request_g('https://api.douban.com/v2/user/linxiaowu');
val[4] = yield request_g('https://api.douban.com/v2/user/linxiaowu');
return finalRes.render('index', {title: 'express', result: val});
}
var g = generator();
g.next(); // ---return is { value: undefined, done: false }
})
request_g
帮助我们封装了request这个异步操作,确保它在回调的时候可以调用到generator的迭代器next()
方法。
3.2.1.1、异步流程解析
- 首先我们获得generator对象
g
,然后执行.next()
的方法,它就开始执行generator函数里面的语句直到遇到了第一个yield
表达式 - 然后开始执行
request_g
这个函数并立即返回undefined
值给next
,于是如我们代码注释的g.next()
返回得到的东西是request_g
执行的结果。 - 休眠generator函数,执行权交还给CPU。
- 在某个时间点request请求成功执行回调,于是在回调中就发现有
.next
方法,接着唤醒generator,并且带着request请求回来的body
参数赋值给了val[0] - 接着从刚开始暂停的地方继续执行,同样的过程再继续重复,直到最后一个yield表达式。
可以看到它通过隐藏在yield
中的pause的能力,然后将generator的resume能力分离到另外一个函数(request_g
)来实现了异步,这样我们的代码就看起来就像是同步一样了。
上面的写法对于简单的异步操作还能够胜任,可是遇到复杂的异步就不行不行的,这个时候我们就需要更加强大的机制来使用generator,于是就有下面的版本Promise + generator。
3.2.2、promise版本的generator
我们可以使用刚才的request来封装一个promise对象,也可以使用现成的request-promise
。
前者可以是:
function request-p(url) {
// Note: returning a promise now!
return new Promise( function(resolve,reject){
request( url, resolve );
} );
}
后者使用request-promise
,如下:
function *generator(){
yield request_promise({
url: 'https://api.github.com/repos/linxiaowu66/react-table-demo',
method: 'get',
headers: {
'User-Agent': 'request'
}
})
/*you can Add more Async operation*/
}
router.get('/generator-promise', function(req, res, next) {
const g = generator();
let val = [];
(function iterator(){
let next = g.next();
if (next.done){
return res.render('index', {title: 'express', result: val});
}
next.value.then( data => {
val.push(data);
iterator();
}).catch( err => {
console.log(err);
return res.render('index', {title: 'express', result: err});
})
})()
})
3.2.2.1、异步流程解析
以后者的实现来说:
- 首先我们获得generator对象
g
,然后执行一个立即执行函数
--iterator()
。 - 立即函数内部执行
.next()
操作,它便开始执行generator的第一个yield
表达式,该表达式直接返回一个promise对象给next.value
。 - 休眠generator函数,执行权交还给CPU。
- CPU继续执行,判断返回的
next.done
是否为true
,如果是的话表明所有的异步操作已经完成,于是返回网页 - 为返回的promise注册
resolved
和rejected
的操作; - 在未来的某个时间点,request请求成功返回,调用
.then
的操作,保存结果然后递归调用iterator()
。请求失败的话调用.catch
的操作,返回网页并显示错误消息。
结论
可以看到,虽然Generator函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)
4、Async/Aswait
JavaScript不断地演进,到了ES7又有了新的异步编程方式,就好比class是原型的语法糖一样,Async/Await也是为了提供给开发者一个更加简单、清楚的语法(不需要你像generator那样不断地使用.next控制执行过程,而是全都自动化,只需要你定义好async函数以及await表达式)。因为简单,所以我们一目了然地就能看懂整个代码。立马上code:
async function getUrl (){
let reqApi = new Promise((resolve, reject) => {
request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
if (err){
reject(err);
} else{
resolve(body);
}
});
});
try{
let result = await Promise.all([reqApi, reqApi, reqApi, reqApi]);
return result;
} catch(err){
console.log(err)
}
}
router.get('/async', function(req, res, next) {
getUrl().then(data => {
res.render('index', {title: 'express', result: data});
})
.catch(err => {
res.render('index', {title: 'express', result: err});
});
});
Tips:
-
一个Async函数总是返回一个
Promise
,该Promise
如果是捕捉到错误的时候是rejected,否则总是resolved。这样的话我们就可以调用一个async函数并且将其结合到我们正常使用的Promise的链式调用中 -
我们在使用
await
的时候一般都是认为“等待”promise
,但并没有真的等待哈。很多人其实都不知道async/await的整个基础其实就是promise。实际上你写的每一个async函数都会返回一个promise,你每一个单独等待的函数都会是一个promise的(如果awaited函数不是promise,它会强转为promise格式)。
参考:
公众号关注一波~
网站源码:linxiaowu66 · 豆米的博客
Follow:linxiaowu66 · Github
关于评论和留言
如果对本文 我所知道的JavaScript异步编程 的内容有疑问,请在下面的评论系统中留言,谢谢。