viviier

ECMAScript6 generator co async

关于异步

在执行IO操作时,javascript代码无需等待,而是传入回调函数后,继续执行后续的代码。

1
2
3
4
5
$.getJSON('http://example/ajax', function(err, data) {
if(err) throw err
console.log('等待结果返回')
})
console.log('不等待结果返回执行')

而同步的IO操作则需要等待结果完成后才能继续执行后面的代码。在等待时间内,无法响应其他任何事件。

1
var data = getJSONSync('http://example.com/ajax');

通过回调函数异步读取文件的简单例子:

1
2
3
4
fs.readFile('nico.txt', (err, data) => {
if(err) throw err
console.log(data)
})

Promise

将回调函数的横向加载改为纵向加载,解决回调函数多层嵌套问题。

1
2
3
4
5
fs.readFile(fileA, (err, data) => {
fs.readFile(fileB, (err, data) => {
// ...
})
})

如果依次读取多个文件,就会出现多重嵌套,代码就会无法管理。采用Promise,连续读取多个文件,就可以解决回调函数带来的问题。

1
2
3
4
5
6
7
const readFile = require('fs-redfile-promise')
readFile(fileA)
.then(data => console.log(data.toString()))
.then(() => readFile(fileB))
.then(data => console.log(data.toString))
.catch(err => console.log(err))

Promise提供then方法加载回调函数,catch捕获错误。但是Promis只是回调函数的改进,代码含有有大量的then,语义不清晰,而且代码冗余。那么,有没有更好的解决方法呢?

Generator

关于Generator

交出函数的执行权(暂停执行)。

1
2
3
4
function* gen(x) {
var y = yield x + 2;
return y;
}

以上代码就是一个Generator函数。整个generator函数就是一个封装的异步函数,异步操作需要暂停的地方,都用yield语句注明。Generator函数的执行方法如下。

1
2
3
var g = gen(1);
g.next(); // { value: 3, done: false }
g.next(); // { value: undefined, done: true }

执行generator函数返回的是指针对象。调用指针的next方法,会移动内部指针,指向第一个遇到的yield语句上,上例是执行到 x + 2 为止。

generator传值与错误处理

next方法返回值的value属性,是generator函数向外输出数据。next方法还可以接受参数,向generator函数体内输入数据。

1
2
3
4
5
6
7
8
function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next(); // { value: 3, done: false }
g.next(2); // { value: 2, done: true }

上面代码中,第一个next方法的value属性,返回表达式 x + 2的值。第二个next方法带有参数2,作为上个阶段异步任务的返回值,被函数体变量个y接收。因此这一步的value属性,返回的就是2。

Generator函数内部还可以处理错误代码,捕获函数体外抛出的错误。

1
2
3
4
5
6
7
8
9
10
11
12
function* gen(x) {
try {
var y = yield x + 2;
} catch (e) {
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了'); // 出错了

generator函数体外,throw抛出的错误可以被函数体内的try …catch捕获。也就是出错的代码和处理错误的代码,实现了时间和空间的分离。

Generator实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let fetch = require('node-fetch');
function* gen() {
let url = 'http://api.github.com/github';
let result = yield fetch(url);
console.log(result.bio);
}
let g = gen();
let result = g.next();
result.value
.then( data => data.json() )
.then( data => g.next(data) );

上面代码中,首先执行generator获取指针对象,然后使用next方法执行异步的第一阶段,得到了没有jsonparse的result,因为fetch返回的是promise对象,所有使用then调用下一个next方法。先将得到的result给JSON.parse(),然后把parse后的值作为上个阶段异步任务返回的值传到函数体内(此时函数体内的result成了第二个then传入进的data值,也就是parse后的result),然后执行console.log(result.bio);

基于promise对象的自动执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
var fs = require('fs')
var readFile = function(fileName) {
return new Promise(function(resolve, reject){
fs.readFile(fileName, function(err, data){
if(err) reject(err)
resolve(data)
})
})
}
var gen = function* () {
var f1 = yield readFile('./nihao.txt')
console.log('我被执行了吗?')
var f2 = yield readFile('./package.json')
console.log('我被输出了吗/')
console.log(f1.toString())
console.log('我是粉丝')
console.log(f2.toString())
}
var g = gen()
g.next().value.then(function(data) {
// g.next() // { value: 'hini', done: false}
console.log('woshi' + data)
// g.next(data) // { value: 'package.json' , done: false}
g.next(data).value.then(function(data){
console.log('我是什么时候执行的啊' + data)
// g.next(data) // { value: undefined, done: true}
g.next(data)
console.log('我呢?')
})
})
// 'woshihini'
//
/*
* g.next()的时候,返回了{ value: 'hini', done: false}
.value也就是'hini'因为是promise对象,所以用then方法,将hini作为data传入
console.log('woshi' data) // woshihini
然后再次执行g.next(data)
f1 = 'nihi'
console.log('我被执行了吗?')
console.log('我是什么时候执行的啊' + data)
返回了{ value: 'package.json', done: false}
再次执行g.next(data)
f2 = 'package.json'
console.log('我被输出了吗?')
console.log(f1.toString)
console.log('我是粉丝')
console.log(f2.toString)
然后返回{ value: undefined, done: true}
console.log('我呢')
tips: next参数会被当作上一个yield语句的返回值。
执行第一个g.next() 返回值是{ value: 'hini', done: false}
然后因为是promise对象,所以使用then方法,将.value最为data参数
然后再次执行g.next(data) 这个时候data为'hini' 把这个data最为上一个yield语句的返回值,然后执行下面的语句
也就是 f1 = data 然后next到下一个yield
然后获取到下一个yield的值也就是'package.json',作为promise对象的data传入,
执行g.next(data)的时候,执行的时候将data作为上一个yield的返回值,也就是执行
var f2 = 'package.json'
然后执行下面的console.log
*/

上面代码是通过then方法层层添加回调,手动执行。相对这点,就可以写出一个自动执行器函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
run(gen);
// 第一步g.next(data) 返回{ value: 'hini', done: false} 指针指到var f1 = yiedl
// 第二步next(data) g.next(data) 把data也就是result.value: 'hini'当作上一个阶段的返回值,然后执行下面的语句
// var f1 = 'hini' 然后执行到下一个yield 返回 { value: 'package.json', done: false}
// 第三步next(data) g.next(data) 把data也就是result.value: 'package.json'当作上一个阶段的返回值,然后执行下面的语句
// var f2= 'package.json' console.log(...) console.log(...)
// 但是这个时候done还是false,所以继续g.next()一次,返回 undefined

上面代码中,只要generator函数没有执行到最后一步,next函数就调用自身,实现自动执行。

Co

使用co的前提条件是,generator函数的yield命令后面,只能是thunk函数或promise对象。

关于co

co是上面自动执行函数的扩展,接收generator函数作为参数,返回一个promise对象,然后通过co自身的next反复调用。

基于co的自动执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const fs = require('fs')
const co = require('co')
const readFile = fileName => new Promise((resolve, reject) => {
fs.readFile(fileName, (err, data) => {
if(err) reject(err)
resolve(data)
})
})
const gen = function* (){
let f1 = yield readFile('./nihao.txt')
let f2 = yield readFile('package.json')
console.log(f1.toString())
console.log(f2.toString())
}
co(gen)

并发的异步操作

co支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。这时,要把并发的操作都放在数组或对象里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 数组的写法
co(function* (){
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
]
console.log(res)
}).catch(onerror)
// 对象的写法
co(function* (){
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2)
}
console.log(res)
}).catch(onerror)

Async

async函数就是generator函数的语法糖。

关于async

1
2
3
4
5
6
7
8
const asyncReadFile = async function (){
let f1 = await readFile('nihao.txt')
let f2 = await readFile('package.json)
console.log(f1.toString())
console.log(f2.toString())
}
asyncReadFile()

把之前的readFile写成async函数就是这样。比较之后发现,async函数就是将generator函数的星号(*)替换成async,将yield替换成了await。generator函数的执行必须靠执行器,才有了co函数库,而async函数自带执行器,与普通函数一摸一样,而且async的语义更好,适用性更广,在async函数的await命令后面,可以跟Promise对象和原始类型的值。

async例子

1
2
3
4
5
6
7
8
9
10
11
12
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
async function asyncprint(value, ms) {
await timeout(ms)
console.log(value)
}
asyncprint('hello', 5000)

上面的代码指定5s后,输出’hello’。

同generator函数一样,async函数返回一个promise对象,可以使用then方法添加回调函数。

1
2
3
getStockPriceByName('goog').then(function (result){
console.log(result);
});

另外要注意的是,await命令后面的promise对象,运行结果可能是rejected,所以最好把await命令放在try…catch代码块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function myFunction() {
try {
await sonethingPromise()
} catch (err) {
console.log(err)
}
}
// 另一种写法
async function myFunction() {
await sonethingPromise().catch(function (err){
console.log(err)
})
}

如果希望多个请求并发执行,可以使用Promise.all方法

------本文结束感谢阅读------