前言
熟悉前端的同学对JavaScript的第一印象是什么?不论是弱类型、脚本语言、异步、原型...但用过的同学都对一个特性又爱又恨,那就是异步。本文会首先从异步的原理开始,介绍一些异步编程的方法,从jQuery中的异步到Promise、Generator,再到async/wait,一步一步讲解,内容尽量通俗易懂,用最简单的例子。
开发中总是会遇到各种异步问题,今天粗略的说下JS的异步,抛块砖,讲下异步发展,并没有太深入挖掘,本篇幅稍微有点长,需要有点耐心,如果比较了解的同学可以直接跳过,内容有错误或不恰当的地方请不吝指出,欢迎大家指正、交流。
1、异步
1.1 为什么要有异步
在浏览器中,JS是单线程、异步执行的。单线程,就是同一时刻JS引擎只能执行一段代码,浏览器是直接面对用户的,而且往往一个页面会有很多请求,如果所有请求都是同步的,那体验就太糟了,所以请求采用异步,避免用户长时间的等待。
而Node中,“一切皆异步”的思想,更是指出了异步的重要性,目的也是让猿儿们编写高效的程序,不因为请求或DB操作而阻塞了服务。
1.2 异步原理
先来说下同步,同步就是事件1干完,再干事件2,事件1干完之前,事件2只能傻傻的等待。如排队上厕所,前面那个人完事出来你才能进去舒服一下,否则...自己脑补吧,画面太美不敢看。
// doSomething1
var s = Date.now();
for (var i = 0; i < 100000000; i++) {
// ...
}
console.log(Date.now() - s); // 约330ms
// doSomething2
上面的为同步,在doSomething2开始之前,必须等待350ms才能开始doSomething2,因为浏览器在执行for的时候,干不了别的。
异步,是为了解决“傻傻的等待”的问题,还是上厕所问题,但是这次加了一步,先拿号,然后等待叫号,等轮到你的号通知你之后,直接去厕所就行了,而在这等待期间,你完全可以来两局王者农药,不耽误你干别的。
// 1、异步1
setTimeout(function () {
// doSomething
}, 1000)
// 2、异步2
$.ajax({
url: '/test/data.json',
success: function() {
console.log('success');
}
});
像上面这种不立即执行,而是等待有了结果之后,再去执行的函数,称为callback,即回调函数。
异步的原理就是将callback作为参数传递给异步执行的函数,等有结果之后再去调用callback执行。
1.3 常见异步
开发中常见的异步操作有:
网络请求,如ajax,request
IO操作,如fs.readFile,DB的CRUD
定时函数,如setTimeout, setInterval
事件监听,如$btn.on('click', callback)
1.4 结束语
异步是一开始就有的,但是怎么把异步从回调地狱中解放出来,则是一步一步发展的。jQuery大家都很熟悉,基本是个前端同学都必用过的,下面先说下它中的异步解决方案。
2、jQuery异步解决方案
jQuery中异步很多,本文主要讲述 $.ajax的变化。
2.1 v1.5版本之前的异步
在1.5版本之前,ajax主要是通过回调函数的写法来实现的:
var ajax = $.ajax({
url: '/test/data.json',
success: function() {
console.log('success');
},
err: function() {
console.log('err')
}
})
console.log(ajax); // 返回的是一个XHR对象
这种是传统的callback的写法,单看这一层还好,如果有三层甚至更多,那么会看到代码呈“>”状,一层层的缩进,代码阅读很糟,也不便于维护。
2.2 v1.5版本以及之后的异步
2011年1月31日,jQuery v1.5发布,重写了ajax的API,ajax写法如下:
var ajax = $.ajax('/test/data.json')
.done(function() {
console.log('done');
})
.fail(function() {
console.log('fail');
})
.always(function() {
console.log('finished');
});
console.log(ajax); // 返回的是一个deferred对象
可以看到,这次采用了链式调用的写法,返回的是一个deferred对象,何为deferred对象,请移驾此处:deferred对象。
链式写法的好处,不用把所有请求都丢到callback里,明确了成功就放入done,失败放入fail,如果成功后有很多步骤,可以写很多done,然后链式起来就行了。熟悉Promise的同学是不是觉得有一点熟悉,如果看不出来,那么上面的写法还可以像这么写:
var ajax = $.ajax('/test/data.json')
.then(function() {
console.log('success');
}, function() {
console.log('err');
})
.then(function() {
console.log('success');
}, function() {
console.log('err');
})
就是用then来代替done、fail,then两个参数,第一个是doneCallback,第二个是failCallback。是不是与Promise更像了。
2.3 结束语
在这章节说下jQuery的变化,也是为了说明JS异步发展的一个过程,由callback到链式的写法,jQuery从一开始的callback到之后的then链式调用,其实也为之后的Promise奠定了基础,下面先讲下async的处理方式,然后就轮到Promise。
3、Async.js
讲Promise之前,先说下Async.js,像Promise,它是需要学习成本的,有人不想用Promise,但是多层回调嵌套又确实很恶心人,所有就有了async、then等库的诞生,这些库并没有用到Promise,能以优美的方式去书写异步,只是callback的语法糖,但是也可以一定程度逃离“回调地狱”了。
下面简单介绍下Async,它可以用在browser跟node端,它的方法很多,具体可移驾Github Async.js,挑出几个常用的,看看它的写法。
- async.series(tasks, [callback]): 顺序执行数组或集合内的函数,执行完一个就执行下一个,错误可在callback中获得
var async = require('async');
async.series([
function (callback) {
callback(null, 'ok1'); // 为了方便,直接返回字符串
},
function (callback) {
callback(null, 'ok2');
}
], function (err, data) {
console.log(data); // ['ok1', 'ok2']
});
- async.parallel(tasks, [callback]): 并行执行数组、集合内的函数
async.parallel([
function (callback) {
callback(null, 'ok1');
},
function (callback) {
callback(null, 'ok2');
}
], function (err, data) {
console.log(data); // ['ok1', 'ok2']
});
- async.waterfall(tasks, [callback]): 瀑布流方式,任务依次执行,前一个函数的回调,会作为后一个函数的参数
async.waterfall([
function (callback) {
callback(null, 'ok1', 'ok2');
},
function (arg1, arg2, callback) {
// 此处 arg1='ok1', arg2='ok2'
callback(null, 'ok3');
}
], function (err, data) {
console.log(data); // 'ok3'
});
可以看出,对于连续多个任务或请求,利用async库可以轻易的把他们放到同一级(数组或集合)来执行,避免了callback的多层嵌套。所以这个库很受欢迎,githut的star将近23k,但是如果想顺应时代发展,特别是ES6,甚至ES7,Promise还是有必要学下的,接下来是Promise。
4、Promise
callback一层嵌一层的回调,导致了金字塔问题的出现,也即callhack hell,写个代码都不能愉快的写了。所有新兴事物的快速发展一定是戳中了原来的一些痛点。
在开发者的千呼万唤中,终于,2015年6月份,ES2015规范正式发布,也是JavaScript的20周年,ES6的发布,也标志着JS开始升级为企业级大型应用的开发语言。Promise也正式加入到ES6,成为一个原生对象,可以直接用。
4.1 什么是Promise
Promise是一个拥有then方法的函数或对象,一个Promise对象可以理解为一次将要执行的操作,主要是异步操作,之后可以用一种链式调用的方式来组织代码。目前Promise的规范是Promise/A+规范,核心内容如下:
I
状态:一个Promise只有3种状态:pending(等待), fulfilled(已完成)或rejected(已拒绝),且必须在其中状态之一。
状态只能从 pending-->fulfilled,或者 pending-->rejected,不能逆向转换,fulfilled、rejected也不能相互转换。
then方法:一个Promise必须提供一个then方法来获取其值,而且then必须返回一个Promise,以供链式调用。
then方法接收两个可选参数,promise.then(onFulfilled, onRejected)
onFulfilled:pending-->fulfilled时调用,onRejected:pending-->rejected时调用。
4.2 基本用法
- 先来看一个fs的异步读取文件的方法:
var fs = require('fs');
var read = function(fileName) {
fs.readFile(fileName, function (err, data) {
if (err) {
console.log(err);
} else {
console.log(data.toString());
}
});
}
然后用Promise对fs.readFile进行封装:
var fs = require('fs');
// readPromise这个方法以后会多次使用
var readPromise = function(fileName) {
// 把fs.readFile用Promise包装一层
var promise = new Promise(function (resolve, reject) {
fs.readFile(fileName, function (err, data) {
if (err) {
reject(err); // 失败就reject出去
} else {
resolve(data.toString()); // 成功就resolve出去
}
});
})
return promise; // 最后返回一个promise对象
}
大家注意看程序中注释的部分,Promise的callback中有两个非常重要的参数:resolve 和 reject。
resolve方法:使Promise对象状态变化 pending-->fulfilled,即等待状态变为已完成,表示成功,resolve方法的参数用于成功之后的操作,此处就是获得的文件的内容。
reject方法:使Promise对象状态变化 pending-->rejected,即等待状态变为已拒绝,表示失败,reject方法的参数用于失败之后的操作,此处就是失败的原因。
通过上小节的规范可以知道,Promise对象都有then方法,所以readPromise方法可以这么用:
readPromise('./test.txt')
.then(function (data) {
console.log(data); // 上面代码中的resolve回的值
}, function (err) {
console.log(err); // 上面代码中的reject回的值
});
then有两个参数,第一个是成功之后的callback,第二个是失败之后的callback,而参数分别是上步包装的resolve与reject函数的参数。
上面还有种写法,就是then只接受一个参数,表示成功之后的操作,后续跟上catch方法,捕获reject的异常:
readPromise('./test.txt')
.then(function (data) {
console.log(data); // 上面代码中的resolve回的值
})
.catch(function (err) {
console.log(err); // 上面代码中的reject回的值
});
上面这种写法更清晰点。
4.3 参数传递
理解Promise的参数传递是很重要的,这样才能得到自己想要的数据。上面已经讲了,resolve的数据会在第一个then接收,reject的数据会在catch接收。因为then返回的还是Promise,所以then可以链式调用,如想对上面的test.txt的数据进行处理,则可以继续then下去:
readPromise('./test.txt')
.then(function (data) {
console.log(data); // resolve回的值
return data; // 此处return的data,将在下个then的参数处获得
})
.then(function (data) {
console.log(data + " 数据已经处理了~"); // 此处的data就是上个then里return回的数据
})
.catch(function (err) {
console.log(err); // 上面代码中的reject回的值
});
then链式操作中返回的值,将会在下个步骤处获得,而如果返回的是一个Promise,那么下个then处获得的就是Promise的第一个then的值。这句话怎么理解,来看个例子,我想读取test1.txt之后,再读取test2.txt,传统callback处理以及Promise处理对比:
// 普通回调,层层嵌套
fs.readFile('./test1.txt',function (err1, data1) {
if (err1) {
console.log(err1);
} else {
console.log(data1);
// 然后再读取第二个文件
fs.readFile('./test1.txt', function (err2, data2) {
if (err2) {
console.log(err2);
} else {
console.log(data2);
}
});
}
});
// Promise方式
var read1 = readPromise('./test1.txt');
var read2 = readPromise('./test2.txt');
read1.then(function (data1) {
console.log(data1); // 此处是test1.txt的内容
return read2; // 此处返回的是read2,一个Promise对象
})
.then(function (data2) {
console.log(data2); // 此处是上一步返回的read2的then,所以打印的是test2.txt的内容
})
对比可以发现,Promise方式更优雅,也更容易看懂,这只是读取2个文件,如果读取三个甚至更多,那用Promise就更方便了,当然如果不需要读取的有依赖关系,则可用Promise对象的all或race方法。
如果想读取test1.txt, text2.txt的内容,读完再做其他操作,则可以如下:
var read1 = readPromise('./test1.txt');
var read2 = readPromise('./test2.txt');
Promise.all([read1, read2])
.then(function (datas) {
console.log(datas[0]); // test1.txt的内容
console.log(datas[1]); // test2.txt的内容
});
如果想读取test1.txt, text2.txt的内容,但是只要有一个返回就可以做其他操作,谁执行的快就用谁,则可以如下:
var read1 = readPromise('./test1.txt');
var read2 = readPromise('./test2.txt');
Promise.race([read1, read2])
.then(function (data) {
console.log(data); // 先读取完那个文件的内容
});
有人说还看到过Promise.resolve,它的作用是把一个thenable对象转换为Promise对象,如下:
// thenable对象,有then属性,且属性值如下
var thenable = {
then: function (resolve, reject) {
resolve('success');
}
}
// 把thenable对象转换为Promise对象
var thenToPromise = Promise.resolve(thenable);
// 然后就可以这么用了
thenToPromise.then(function (data) {
console.log(data); // 'success'
});
4.4 相关库
实际开发中,使用原生的Promise当然可以,不过市面上有现成的第三方库,而且很好用,比较流行的是Q、Bluebird等。他们都可用于浏览器端以及node端,并且可以在不支持Promise的环境中使用,至于用那个,则看个人爱好了,bluebird号称Promise库里最快的,比原生的Promise都快,其实原生的Promise比传统的callback慢不少。
这里介绍Q.js一些基本的用法,引用官网的一个例子,再次体验下传统回调与Promise库之间的对比:
// 传统回调
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
// 用Q
Q.fcall(promisedStep1)
.then(promisedStep2)
.then(promisedStep3)
.then(promisedStep4)
.then(function (value4) {
// Do something with value4
})
.catch(function (err) {
// Handle any err from all above steps
})
.done();
可以看到,传统回调方式也不错嘛,也有美感,but,这只是简写,如果加上各种异常判断,还有其他操作,那么维护起来很麻烦,也容易出错。而用Q,则清晰了很多,一步完成之后继续下一步,比较符合人的思维,这里看到一个Q的用法:Q.fcall,常用的方法有:Q.fcall, Q.nfcall, Q.nfapply, Q.defer, Q.all, Q.any等。用法都放到一段代码里:
var Q = require('q');
var fs = require('fs');
// Q.fcall: 接收函数或defer实例,返回一个Promise对象
var promiseFcall = Q.fcall(function () {
return 'hello';
});
// Q.nfcall: Node function call, 处理callback是这种形式的:function(err, result),可以直接封装成Promise
var promiseNfcall = Q.nfcall(fs.readFile, './test.txt', 'utf-8');
// Q.nfapply: 与Q.nfcall类似,只是参数不一样,很像js的call与apply用法
var promiseNfapply = Q.nfapply(fs.readFile, ['./test.txt', 'utf-8']);
// Q.defer: 可以定义Promise生成器,如果浏览器不支持Promise,则比较有用,很像原生Promise的写法
var promiseDefer = function(fileName) {
var defer = Q.defer();
fs.readFile(fileName, function (err, data) {
if (err) {
defer.reject(err);
} else {
defer.resolve(data.toString());
}
})
}
// Q.all: 与Promise.all类似
var read1 = Q.nfcall(fs.readFile, './test1.txt', 'utf-8');
var read2 = Q.nfcall(fs.readFile, './test2.txt', 'utf-8');
Q.all([read1, read2], function (data) {
console.log(data[0]);
console.log(data[1]);
});
// Q.any: 与Promise.race类似
Q.any([read1, read2])
.then(function (data) {
console.log(data);
});
以上只是简单介绍了最基本的用法,具体可以自行去github上看下。
4.5 结束语
到此,Promise差不多介绍完了,当然Promise还有很多用法,就不一一列举了,那么Promise有没有改变callback的本质?并没有,Promise只是换了种对异步的写法,优化了对代码的可读性,其实还是依赖callback,获得的数据,还是在then的callback里获取到的。上面看到需要的数据,还是在callback中获得的,还没有真正像同步那样的写法,如果用Generator配合Promise,则写法就完全不同了,接下来进入Generator。
5、Generator
5.1 协程
介绍Generator前,先讲下协程,协程最初诞生是为了解决低速IO与高速的CPU之间协作问题,协程是指多个线程交互协作,完成异步任务,大概流程如下:
协程A开始运行
执行到某处,暂停,然后执行权交给协程B
一段时间后,协程B交换执行权给协程A
协程A恢复执行
还以读取文件为例,代码表示如下:
function asyncFunction() {
// doSomething1
yield readFile('./test.txt');
// doSomething2
}
上面函数asyncFunction就是一个协程,一开始执行doSomething1,当遇到yield后,自身先暂停,执行权移交给readFile,当readFile执行完之后,执行权又交还回asyncFunction,然后接着执行doSomething2。
5.2 什么是Generator
Generator(生成器)可以说是协程在ES6中的实现,它最大的特点是:可以交出执行权,暂停执行。先看一个简单的Generator写法:
function* gen() {
yield 'hello';
yield 'world';
return 'ok';
}
var g = gen();
g.next(); // {value: "hello", done: false}
g.next(); // {value: "world", done: false}
g.next(); // {value: "ok", done: true}
g.next(); // {value: undefined, done: true}
这看上去像是一个函数,所以也可以称为Generator函数,但是要明白,Generator并不是函数,它与普通函数有几点区别:
以function* 开始,注意这个*
内部有一个 yield 关键字,跟return有点像,不同是yield可以有多个
Generator返回的其实是一个Iterator对象,下面先说下Iterator迭代器。
5.3 Iterator迭代器
在讲Iterator之前,先说下ES6新引入的一个基本类型:Symbol。
ES6之前JS有6个基本数据类型:string, object, null, boolean, undefined, number。现在增加一个:Symbol,表示独一无二的值。
Symbol不能用new关键字,因为是一个原始类型的值,不是对象,所以也不能添加属性,可理解为类似字符串数据类型。它可以接收一个字符串参数,主要是为了控制台显示或转换为字符串时容易区分:
var s1 = Symbol();
var s2 = Symbol();
s1 == s2 // false
s1 = Symbol('foo');
s2 = Symbol('foo');
s1 // Symbol(foo);
typeof s1 // 'symbol'
s1 == s2 // false
Symbol也可以作为对象的属性key来使用:
var obj = {
a: 'foo',
[Symbol.iterator]: 'foo2'
}
console.log(obj); // {a: "foo", Symbol(Symbol.iterator): "foo2"}
Symbol有个iterator属性,指向该对象的默认遍历器方法。在ES6中有些原生就具有[Symbol.iterator]属性,如数组、Set、Map(也是ES6新引进的)、arguments对象等,这些对象有个特点,就是可以用for...of循环遍历:
var arr = ['foo1', 'foo2', 'foo3'];
for (var i of arr) {
console.log(i); // 'foo1' 'foo2' 'foo3' 这里注意i为value,并不是对应的key
}
Iterator对象:具有[Symbol.iterator]属性的数据,都可以生成一个Iterator对象,而怎么使用Iterator对象,有两种方式: next(), for...of。以数组举例:
var arr = ['foo1', 'foo2', 'foo3'];
var it = arr[Symbol.iterator](); // 生成arr的iterator对象
// next
it.next(); // {value: "foo1", done: false}
it.next(); // {value: "foo2", done: false}
it.next(); // {value: "foo3", done: false}
it.next(); // {value: undefined, done: true}, done=true表示获取完成
// for...of,这种用法不会遍历到return的数据
for (var i of it) {
console.log(i); // 'foo1' 'foo2' 'foo3'
}
而Generator,就是天生的Iterator对象,所以才有next(),也可以用for...of遍历,针对一开始的例子,现详细解释一下:
function* gen() {
yield 'hello';
yield 'world';
return 'ok';
}
var g = gen();
g.next(); // {value: "hello", done: false}
g.next(); // {value: "world", done: false}
g.next(); // {value: "ok", done: true}
g.next(); // {value: undefined, done: true}
首先定义Generator gen,注意声明用function*
var g = gen()这步生成Generator对象,但是并没有立即执行代码,处于暂停状态
第一个g.next()会激活状态,开始执行代码,直到遇到第一个yield,此时返回yield之后的数据,再次进入暂停状态
第二个g.next()与之前的类似,最后返回结果,进入暂停
第三个g.next()也是先激活,但是遇到了return,所以就结束了,返回return的数据,已经结束了,此时done=true
第四个g.next(),此时因为已经结束,所以只能返回value=undefined, done=true
注意,每次next返回的数据,都是{value:xxx, done:xxx}格式。
5.4 yield、next
上面其实已经用到yield、next了,这儿再详细说下。
- yield* : yield 可以返回一个值或一个表达式,但还可以 yield* 这么用,在Generator里面再套一个Generator:
function* gen() {
yield 'a';
yield 'b';
}
function* gen2() {
yield 'c';
yield* gen();
yield 'd';
}
var g = gen2();
g.next(); // {value: "c", done: false}
g.next(); // {value: "a", done: false}
g.next(); // {value: "b", done: false}
g.next(); // {value: "d", done: true}
- next: next也可以向yield传递参数:
function* gen() {
var a = yield 'a';
console.log(a); // 100
var b = yield 'b';
console.log(b); // 200
yield 'c';
}
var g = gen();
g.next(); // {value: "a", done: false}
g.next(100); // {value: "b", done: false}
g.next(200); // {value: "c", done: false}
g.next(100)是将100传递给上一个已经执行完了的yield的变量,请各位自己先看下是否能准确判断a,b的值以及每个next的返回值。
第一个next返回当然是value='a'
第二个next,传递100给a变量,所以console.log(a)打印100,然后next返回的是'b'
第三个next同上,200传递给b变量,打印200,然后next返回'c'
讲了这么多,还没到Generator怎么跟异步联系,马上了,下面先说下Thunk函数,已经怎么把Thunk与异步联系起来。
5.5 Thunk
其实Thunk函数并不是Generator的一部分,这节重在介绍Generator,所以放在此处。
- Thunk函数:将多参数函数替换成单参数函数,只接受一个参数,并且参数是回调函数。任何函数,只要含有回调函数,都可以写成Thunk函数形式。
看一个例子,以fs.readFile为例:
// 1、多参数函数
fs.readFile(fileName, calback);
// 2、定义一个fs的thunk转换器
var thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
}
}
// readFileThunk为Thunk函数,用的时候,只传入callback就行
var readFileThunk = thunk(fileName);
readFileThunk(callback);
看着是不是又复杂了...是的,不过现在的复杂是为以后的简单准备的,具体后续会讲。手动写Thunk方法比较麻烦,所以出现了第三方库:thunkify。
- thunkify: 封装了thunk转换器,可以简化写法:
var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
var readFileThunk = readFile(fileName);
readFileThunk(callback);
- Thunk配合Generator
先看个例子:
var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
var gen = function* () {
var data1 = yield readFile('./test1.txt');
console.log(data1.toString()); // test1.txt的内容
var data2 = yield readFile('./test2.txt');
console.log(data2.toString()); // test2.txt的内容
}
看上面的代码,获取data1, data2跟同步写法是否基本一样?想读取那个文件,直接按顺序写就行,不用callback里获得结果,或者then中获得结果,只是前面多了一个yield关键字,是不是很爽?
上面说过,yield会把程序的执行权移出gen函数,但是怎么交换回来呢?这就是Thunk函数的妙用了,它可以用于Generator函数的自动流程管理。下面是一个基于Thunk函数的Generator执行器:
// 执行器
function run(generator) {
var gen = generator();
// 这个next其实就是Thunk函数的回调函数
function next(err, data) {
var res = gen.next(data); // 类似{value: Thunk函数, done: false}
if (res.done) {
return;
}
res.value(next); // res.value是一个Thunk函数,而参数next就是一个callback
}
next();
}
var gen = function* () {
var data1 = yield readFile('./test1.txt');
var data2 = yield readFile('./test2.txt');
}
// run执行Generator函数
run(gen);
其实,要用Generator解决回调地狱问题,需要首先处理一下调用的函数,使函数正确执行后能够自动执行next方法,并且传递执行完方法后的结果。
- Promise配合Generator
yield 后能跟Thunk函数,也可以跟Promise对象,所以Promise也可以配合Generator解决回调地狱问题。下面是一个基于Promise的Generator执行器:
// 执行器
function run(generator) {
var gen = generator();
function go(res) {
// res类似{value: Promise对象, done: false}
if (res.done) {
return res.value;
}
// Promise对象有then方法,两个参数为doneCallback,failCallback
return res.value.then(function (data) {
return go(gen.next(data));
}, function (err) {
return go(err);
})
}
go(gen.next());
}
var gen = function* () {
var data1 = yield readPromise('./test1.txt');
var data2 = yield readPromise('./test2.txt');
}
// run执行Generator函数
run(gen);
每次写生成器函数很麻烦,所以TJ大神写了一个co库,下面介绍。
5.6 co
co库可以自动执行Generator函数,其实就是类似上面的run方法,Generator是一个异步操作容器,自动执行需要交换执行权给Generator,有两种方法可以做到:
回调函数,将异步操作包装成Thunk函数,在回调函数里执行交换执行权
Promise对象,将异步操作包装成Promise对象,用then里执行交换执行权
co库就是将两种执行器包装成一个库,所以使用co的时候,yield后面只能是Thunk函数或Promise对象。co现在返回的是一个Promise对象(之前版本返回的是Thunk函数),co非常好用,把刚才的代码重新,将会非常简单:
var co = require('co');
var gen = function* () {
var data1 = yield readPromise('./test1.txt');
var data2 = yield readPromise('./test2.txt');
}
// co执行Generator函数
co(gen);
5.7 结束语
Generator终于讲完了,配合Thunk函数或Promise对象,确实比之前的callback或then链式调用“顺畅”了很多,很像同步的写法,已经很符合人的顺序执行的思维了。其实Generator的本质是“暂停”,有了这个,才能让程序到一个地方先暂停,执行异步,然后执行完了再继续执行程序,这样就可以把操作连起来了。
可以看到Generator的异步处理,学习成本比较高,Generator、Thunk、Promise...等,都需要时间去学习,这显然还不够友好,所以在ES7中基于Promise实现了一套异步处理方案:async/await,这个才是最终的方案。
6、async/await
async/await是在ES7中实现的,目前好多浏览器不支持,node从7.0.0开始就支持使用--harmony-async-await来支持此功能,另外babel也已经支持async的transform了,使用的时候引入babel就行。
6.1 基本用法
先介绍下async/await:
基于Promise实现的,不适用于普通的回调函数
非阻塞的
函数声明用async function, 遇到异步用await,且await只能放在async函数中
先看一段使用async/await的代码:
// 定义async函数,注意async关键字
var readAsync = async function() {
var data1 = await readPromise('./test1.txt'); // 注意await关键字
var data2 = await readPromise('./test2.txt');
return 'ok'; // 返回值可以在调用处通过then拿到
}
// 执行
readAsync();
// 或者
readAsync().then(function (data) {
console.log(data); // 'ok'
});
是不是非常简单,无需用co,直接执行就行。需要注意一点,await后只能跟Promise对象、字符串,数值等,不能跟Thunk函数,也暗示Promise可能是解决异步的最终方案。同时,async函数也默认返回的是一个Promise对象,函数最后可以return一个值,最后调用时在then里获取。
6.2 与Generator的对比
上面也看到,async/await与Generator的解决方案很像,区别如下:
声明,async function 代替 function*
await 代替 yield
内置执行器,可以直接运行,不需要co这种第三方库
6.3 结束语
其实async与Generator很像,是因为async相当于把Generator跟执行器进行了包装,是Generator的语法糖,但是也方便了很多,目前很多人认为async就是异步的最终方案。
小结
讲完了,里面可能有些例子不恰当,也参考了官网或别人的一些例子,总之尽量用简单的例子去铺开。
由于浏览器的特殊性,JS只能采用异步解决请求,从而性能也比较好,但是也带了了各种麻烦,所以人们从一开始就寻找它的同步写法,试图摆脱恶心的callback-hell,从一开始callback,到Promise对象,到Generator,再到async,异步方案是越来越好,也越来越优雅,随着ES7的普及,其实直接用async就好,不过技术发展总有一些过程,了解这些过程对我们的眼界扩展以及对这门语音会有更好的认识。
希望讲了这么多能帮助一些同学理解异步,有错误也轻及时指正,谢谢~最后再总览下法中中的几种写法,体验异步发展过程:
callback方式:
fs.readFile('./test1.txt', function (err1, data1) {
fs.readFile('./test2.txt', function (err2, data2) {
fs.readFile('./test2.txt', function (err2, data2) {
});
});
});
Promise方式:
readPromise('./test1.txt', function (data1) {
return readPromise('./test2.txt');
})
.then(function (data2) {
return readPromise('./test3.txt');
})
.then(function (data3) {
});
Generator方式:
var gen = function* () {
var data1 = yield readPromise('./test1.txt');
var data2 = yield readPromise('./test2.txt');
var data3 = yield readPromise('./test3.txt');
}
co(gen);
async/await方式:
var readAsync = async function() {
var data1 = await readPromise('./test1.txt');
var data2 = await readPromise('./test2.txt');
var data3 = await readPromise('./test3.txt');
}
readAsync();