1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-28 13:02:21 +00:00

edit async

This commit is contained in:
Ruan Yifeng 2015-07-17 08:03:45 +08:00
parent 01340b7e41
commit 89fc7766fb
4 changed files with 522 additions and 368 deletions

View File

@ -5,13 +5,11 @@
Array.from方法用于将两类对象转为真正的数组类似数组的对象array-like object和可遍历iterable的对象包括ES6新增的数据结构Set和Map Array.from方法用于将两类对象转为真正的数组类似数组的对象array-like object和可遍历iterable的对象包括ES6新增的数据结构Set和Map
```javascript ```javascript
let ps = document.querySelectorAll('p'); let ps = document.querySelectorAll('p');
Array.from(ps).forEach(function (p) { Array.from(ps).forEach(function (p) {
console.log(p); console.log(p);
}); });
``` ```
上面代码中querySelectorAll方法返回的是一个类似数组的对象只有将这个对象转为真正的数组才能使用forEach方法。 上面代码中querySelectorAll方法返回的是一个类似数组的对象只有将这个对象转为真正的数组才能使用forEach方法。
@ -19,42 +17,34 @@ Array.from(ps).forEach(function (p) {
Array.from方法可以将函数的arguments对象转为数组。 Array.from方法可以将函数的arguments对象转为数组。
```javascript ```javascript
function foo() { function foo() {
var args = Array.from( arguments ); var args = Array.from( arguments );
} }
foo( "a", "b", "c" ); foo( "a", "b", "c" );
``` ```
任何有length属性的对象都可以通过Array.from方法转为数组。 任何有length属性的对象都可以通过Array.from方法转为数组。
```javascript ```javascript
Array.from({ 0: "a", 1: "b", 2: "c", length: 3 }); Array.from({ 0: "a", 1: "b", 2: "c", length: 3 });
// [ "a", "b" , "c" ] // [ "a", "b" , "c" ]
``` ```
对于还没有部署该方法的浏览器可以用Array.prototyp.slice方法替代。 对于还没有部署该方法的浏览器可以用Array.prototyp.slice方法替代。
```javascript ```javascript
const toArray = (() => const toArray = (() =>
Array.from ? Array.from : obj => [].slice.call(obj) Array.from ? Array.from : obj => [].slice.call(obj)
)(); )();
``` ```
Array.from()还可以接受第二个参数作用类似于数组的map方法用来对每个元素进行处理。 Array.from()还可以接受第二个参数作用类似于数组的map方法用来对每个元素进行处理。
```JavaScript ```JavaScript
Array.from(arrayLike, x => x * x); Array.from(arrayLike, x => x * x);
// 等同于 // 等同于
Array.from(arrayLike).map(x => x * x); Array.from(arrayLike).map(x => x * x);
``` ```
下面的例子将数组中布尔值为false的成员转为0。 下面的例子将数组中布尔值为false的成员转为0。
@ -77,11 +67,9 @@ function countSymbols(string) {
Array.of方法用于将一组值转换为数组。 Array.of方法用于将一组值转换为数组。
```javaScript ```javaScript
Array.of(3, 11, 8) // [3,11,8] Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3] Array.of(3) // [3]
Array.of(3).length // 1 Array.of(3).length // 1
``` ```
这个方法的主要目的是弥补数组构造函数Array()的不足。因为参数个数的不同会导致Array()的行为有差异。 这个方法的主要目的是弥补数组构造函数Array()的不足。因为参数个数的不同会导致Array()的行为有差异。
@ -124,11 +112,9 @@ console.log("found:", found);
数组实例的findIndex方法的用法与find方法非常类似返回第一个符合条件的数组成员的位置如果所有成员都不符合条件则返回-1。 数组实例的findIndex方法的用法与find方法非常类似返回第一个符合条件的数组成员的位置如果所有成员都不符合条件则返回-1。
```javascript ```javascript
[1, 5, 10, 15].findIndex(function(value, index, arr) { [1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9; return value > 9;
}) // 2 }) // 2
``` ```
这两个方法都可以接受第二个参数用来绑定回调函数的this对象。 这两个方法都可以接受第二个参数用来绑定回调函数的this对象。
@ -150,13 +136,11 @@ console.log("found:", found);
fill()使用给定值,填充一个数组。 fill()使用给定值,填充一个数组。
```javascript ```javascript
['a', 'b', 'c'].fill(7) ['a', 'b', 'c'].fill(7)
// [7, 7, 7] // [7, 7, 7]
new Array(3).fill(7) new Array(3).fill(7)
// [7, 7, 7] // [7, 7, 7]
``` ```
上面代码表明fill方法用于空数组的初始化非常方便。数组中已有的元素会被全部抹去。 上面代码表明fill方法用于空数组的初始化非常方便。数组中已有的元素会被全部抹去。
@ -201,38 +185,32 @@ for (let [index, elem] of ['a', 'b'].entries()) {
Array.protypeto.includes方法返回一个布尔值表示某个数组是否包含给定的值。该方法属于ES7。 Array.protypeto.includes方法返回一个布尔值表示某个数组是否包含给定的值。该方法属于ES7。
```javascript ```javascript
[1, 2, 3].includes(2); // true [1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false [1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true [1, 2, NaN].includes(NaN); // true
``` ```
该方法的第二个参数表示搜索的起始位置默认为0。 该方法的第二个参数表示搜索的起始位置默认为0。
```javascript ```javascript
[1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true [1, 2, 3].includes(3, -1); // true
``` ```
下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。 下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。
```javascript ```javascript
const contains = (() => const contains = (() =>
Array.prototype.includes Array.prototype.includes
? (arr, value) => arr.includes(value) ? (arr, value) => arr.includes(value)
: (arr, value) => arr.some(el => el === value) : (arr, value) => arr.some(el => el === value)
)(); )();
contains(["foo", "bar"], "baz"); // => false contains(["foo", "bar"], "baz"); // => false
``` ```
## 数组推导 ## 数组推导
数组推导array comprehension提供简洁写法允许直接通过现有数组生成新数组。这项功能没有被列入ES6而是推迟到了ES7 数组推导array comprehension提供简洁写法允许直接通过现有数组生成新数组。这项功能本来是要放入ES6的但是TC39委员会想继续完善这项功能让其支持所有数据结构内部调用iterator对象不像现在只支持数组所以就把它推迟到了ES7。Babel转码器已经支持这个功能
```javascript ```javascript
var a1 = [1, 2, 3, 4]; var a1 = [1, 2, 3, 4];
@ -265,7 +243,6 @@ var years = [ 1954, 1974, 1990, 2006, 2010, 2014 ];
数组推导可以替代map和filter方法。 数组推导可以替代map和filter方法。
```javascript ```javascript
[for (i of [1, 2, 3]) i * i]; [for (i of [1, 2, 3]) i * i];
// 等价于 // 等价于
[1, 2, 3].map(function (i) { return i * i }); [1, 2, 3].map(function (i) { return i * i });
@ -273,7 +250,6 @@ var years = [ 1954, 1974, 1990, 2006, 2010, 2014 ];
[for (i of [1,4,2,3,-8]) if (i < 3) i]; [for (i of [1,4,2,3,-8]) if (i < 3) i];
// 等价于 // 等价于
[1,4,2,3,-8].filter(function(i) { return i < 3 }); [1,4,2,3,-8].filter(function(i) { return i < 3 });
``` ```
上面代码说明模拟map功能只要单纯的for...of循环就行了模拟filter功能除了for...of循环还必须加上if语句。 上面代码说明模拟map功能只要单纯的for...of循环就行了模拟filter功能除了for...of循环还必须加上if语句。
@ -281,7 +257,6 @@ var years = [ 1954, 1974, 1990, 2006, 2010, 2014 ];
在一个数组推导中还可以使用多个for...of结构构成多重循环。 在一个数组推导中还可以使用多个for...of结构构成多重循环。
```javascript ```javascript
var a1 = ["x1", "y1"]; var a1 = ["x1", "y1"];
var a2 = ["x2", "y2"]; var a2 = ["x2", "y2"];
var a3 = ["x3", "y3"]; var a3 = ["x3", "y3"];
@ -295,7 +270,6 @@ var a3 = ["x3", "y3"];
// y1x2y3 // y1x2y3
// y1y2x3 // y1y2x3
// y1y2y3 // y1y2y3
``` ```
上面代码在一个数组推导之中使用了三个for...of结构。 上面代码在一个数组推导之中使用了三个for...of结构。
@ -305,11 +279,9 @@ var a3 = ["x3", "y3"];
由于字符串可以视为数组,因此字符串也可以直接用于数组推导。 由于字符串可以视为数组,因此字符串也可以直接用于数组推导。
```javascript ```javascript
[for (c of 'abcde') if (/[aeiou]/.test(c)) c].join('') // 'ae' [for (c of 'abcde') if (/[aeiou]/.test(c)) c].join('') // 'ae'
[for (c of 'abcde') c+'0'].join('') // 'a0b0c0d0e0' [for (c of 'abcde') c+'0'].join('') // 'a0b0c0d0e0'
``` ```
上面代码使用了数组推导,对字符串进行处理。 上面代码使用了数组推导,对字符串进行处理。

View File

@ -466,9 +466,217 @@ run(gen);
Thunk函数并不是Generator函数自动执行的唯一方案。因为自动执行的关键是必须有一种机制自动控制Generator函数的流程接收和交还程序的执行权。回调函数可以做到这一点Promise 对象也可以做到这一点。 Thunk函数并不是Generator函数自动执行的唯一方案。因为自动执行的关键是必须有一种机制自动控制Generator函数的流程接收和交还程序的执行权。回调函数可以做到这一点Promise 对象也可以做到这一点。
## co函数库 ## co模块
如果并发执行异步操作可以将异步操作都放入一个数组跟在yield语句后面。 ### 基本用法
[co模块](https://github.com/tj/co)是著名程序员TJ Holowaychuk于2013年6月发布的一个小工具用于Generator函数的自动执行。
比如有一个Generator函数用于依次读取两个文件。
```javascript
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
```
co模块可以让你不用编写Generator函数的执行器。
```javascript
var co = require('co');
co(gen);
```
上面代码中Generator函数只要传入co函数就会自动执行。
co函数返回一个Promise对象因此可以用then方法添加回调函数。
```javascript
co(gen).then(function (){
console.log('Generator 函数执行完成');
})
```
上面代码中等到Generator函数执行结束就会输出一行提示。
### co模块的原理
为什么co可以自动执行Generator函数
前面说过Generator就是一个异步操作的容器。它的自动执行需要一种机制当异步操作有了结果能够自动交回执行权。
两种方法可以做到这一点。
1回调函数。将异步操作包装成Thunk函数在回调函数里面交回执行权。
2Promise 对象。将异步操作包装成Promise对象用then方法交回执行权。
co模块其实就是将两种自动执行器Thunk函数和Promise对象包装成一个模块。使用co的前提条件是Generator函数的yield命令后面只能是Thunk函数或Promise对象。
上一节已经介绍了基于Thunk函数的自动执行器。下面来看基于Promise对象的自动执行器。这是理解co模块必须的。
### 基于Promise对象的自动执行
还是沿用上面的例子。首先把fs模块的readFile方法包装成一个Promise对象。
```javascript
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
```
然后手动执行上面的Generator函数。
```javascript
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
})
```
手动执行其实就是用then方法层层添加回调函数。理解了这一点就可以写出一个自动执行器。
```javascript
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);
```
上面代码中只要Generator函数还没执行到最后一步next函数就调用自身以此实现自动执行。
### co模块的源码
co就是上面那个自动执行器的扩展它的源码只有几十行非常简单。
首先co函数接受Generator函数作为参数返回一个 Promise 对象。
```javascript
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
});
}
```
在返回的Promise对象里面co先检查参数gen是否为Generator函数。如果是就执行该函数得到一个内部指针对象如果不是就返回并将Promise对象的状态改为resolved。
```javascript
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
});
}
```
接着co将Generator函数的内部指针对象的next方法包装成onFulefilled函数。这主要是为了能够捕捉抛出的错误。
```javascript
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}
```
最后就是关键的next函数它会反复调用自身。
```javascript
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
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) + '"'));
}
```
上面代码中next 函数的内部代码,一共只有四行命令。
- 第一行,检查当前是否为 Generator 函数的最后一步,如果是就返回。
- 第二行,确保每一步的返回值,是 Promise 对象。
- 第三行,使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。
- 第四行,在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected从而终止执行。
### 处理并发的异步操作
co支持并发的异步操作即允许某些操作同时进行等到它们全部完成才进行下一步。
这时要把并发的操作都放在数组或对象里面跟在yield语句后面。
```javascript
// 数组的写法
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);
```
下面是另一个例子。
```javascript ```javascript
co(function* () { co(function* () {
@ -483,3 +691,308 @@ function* somethingAsync(x) {
``` ```
上面的代码允许并发三个somethingAsync异步操作等到它们全部完成才会进行下一步。 上面的代码允许并发三个somethingAsync异步操作等到它们全部完成才会进行下一步。
## async函数
### 含义
async 函数是什么一句话async函数就是Generator函数的语法糖。
前文有一个Generator函数依次读取两个文件。
```javascript
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
```
写成 async 函数,就是下面这样。
```javascript
var asyncReadFile = async function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
```
一比较就会发现async函数就是将Generator函数的星号*替换成async将yield替换成await仅此而已。
async 函数对 Generator 函数的改进,体现在以下三点。
1内置执行器。Generator函数的执行必须靠执行器所以才有了co模块而async 函数自带执行器。也就是说async函数的执行与普通函数一模一样只要一行。
```javascript
var result = asyncReadFile();
```
2更好的语义。async和await比起星号和yield语义更清楚了。async表示函数里有异步操作await 表示紧跟在后面的表达式需要等待结果。
3更广的适用性。 co模块约定yield命令后面只能是Thunk函数或Promise对象而async函数的await命令后面可以跟Promise对象和原始类型的值数值、字符串和布尔值但这时等同于同步操作
### async函数的实现
async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
```javascript
async function fn(args){
// ...
}
// 等同于
function fn(args){
return spawn(function*() {
// ...
});
}
```
所有的 async 函数都可以写成上面的第二种形式,其中的 spawn 函数就是自动执行器。
下面给出 spawn 函数的实现,基本就是前文自动执行器的翻版。
```javascript
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
```
async 函数是非常新的语法功能,新到都不属于 ES6而是属于 ES7。目前它仍处于提案阶段但是转码器 Babel 和 regenerator 都已经支持,转码后就能使用。
### async 函数的用法
同Generator函数一样async函数返回一个Promise对象可以使用then方法添加回调函数。当函数执行的时候一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
下面是一个例子。
```javascript
async function getStockPriceByName(name) {
var symbol = await getStockSymbol(name);
var stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result){
console.log(result);
});
```
上面代码是一个获取股票报价的函数函数前面的async关键字表明该函数内部有异步操作。调用该函数时会立即返回一个Promise对象。
下面的例子,指定多少毫秒后输出一个值。
```javascript
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value)
}
asyncPrint('hello world', 50);
```
上面代码指定50毫秒以后输出"hello world"。
### 注意点
await命令后面的Promise对象运行结果可能是rejected所以最好把await命令放在try...catch代码块中。
```javascript
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise().catch(function (err){
console.log(err);
};
}
```
await命令只能用在async函数之中如果用在普通函数就会报错。
```javascript
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
}
```
上面代码会报错因为await用在普通函数之中了。但是如果将forEach方法的参数改成async函数也有问题。
```javascript
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 可能得到错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
```
上面代码可能不会正常工作,原因是这时三个`db.post`操作将是并发执行也就是同时执行而不是继发执行。正确的写法是采用for循环。
```javascript
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
```
如果确实希望多个请求并发执行,可以使用 Promise.all 方法。
```javascript
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的写法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
```
ES6将await增加为保留字。使用这个词作为标识符在ES5是合法的在ES6将抛出SyntaxError。
### 与Promise、Generator的比较
我们通过一个例子来看Async函数与Promise、Generator函数的区别。
假定某个DOM元素上面部署了一系列的动画前一个动画结束才能开始后一个。如果当中有一个动画出错就不再往下执行返回上一个成功执行的动画的返回值。
首先是Promise的写法。
```javascript
function chainAnimationsPromise(elem, animations) {
// 变量ret用来保存上一个动画的返回值
var ret = null;
// 新建一个空的Promise
var p = Promise.resolve();
// 使用then方法添加所有动画
for(var anim in animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
})
}
// 返回一个部署了错误捕捉机制的Promise
return p.catch(function(e) {
/* 忽略错误,继续执行 */
}).then(function() {
return ret;
});
}
```
虽然Promise的写法比回调函数的写法大大改进但是一眼看上去代码完全都是Promise的APIthen、catch等等操作本身的语义反而不容易看出来。
接着是Generator函数的写法。
```javascript
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
var ret = null;
try {
for(var anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
});
}
```
上面代码使用Generator函数遍历了每个动画语义比Promise写法更清晰用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于必须有一个任务运行器自动执行Generator函数上面代码的spawn函数就是自动执行器它返回一个Promise对象而且必须保证yield语句后面的表达式必须返回一个Promise。
最后是Async函数的写法。
```javascript
async function chainAnimationsAsync(elem, animations) {
var ret = null;
try {
for(var anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
}
```
可以看到Async函数的实现最简洁最符合语义几乎没有语义不相关的代码。它将Generator写法中的自动执行器改在语言层面提供不暴露给用户因此代码量最少。如果使用Generator写法自动执行器需要用户自己提供。

View File

@ -4,7 +4,7 @@ ECMAScript 6以下简称ES6是JavaScript语言的下一代标准已经
ES6的目标是使得JavaScript语言可以用来编写大型的复杂的应用程序成为企业级开发语言。 ES6的目标是使得JavaScript语言可以用来编写大型的复杂的应用程序成为企业级开发语言。
标准的制定者计划以后每年发布一次标准使用年份作为标准的版本。因为当前版本的ES6是在2015年发布的所以又称ECMAScript 2015。 标准的制定者计划以后每年发布一次标准使用年份作为标准的版本。因为当前版本的ES6是在2015年发布的所以又称ECMAScript 2015。
## ECMAScript和JavaScript的关系 ## ECMAScript和JavaScript的关系
@ -330,3 +330,5 @@ ES7可能包括的功能有
4**Traits**它将是“类”功能class的一个替代。通过它不同的对象可以分享同样的特性。 4**Traits**它将是“类”功能class的一个替代。通过它不同的对象可以分享同样的特性。
其他可能包括的功能还有更精确的数值计算、改善的内存回收、增强的跨站点安全、类型化的更贴近硬件的低级别操作、国际化支持Internationalization Support、更多的数据结构等等。 其他可能包括的功能还有更精确的数值计算、改善的内存回收、增强的跨站点安全、类型化的更贴近硬件的低级别操作、国际化支持Internationalization Support、更多的数据结构等等。
本书对于那些明确的、或者很有希望列入ES7的功能尤其是那些Babel已经支持的功能都将予以介绍。

View File

@ -428,7 +428,6 @@ p.then(null, function (s){
使用Generator函数管理流程遇到异步操作的时候通常返回一个Promise对象。 使用Generator函数管理流程遇到异步操作的时候通常返回一个Promise对象。
```javascript ```javascript
function getFoo () { function getFoo () {
return new Promise(function (resolve, reject){ return new Promise(function (resolve, reject){
resolve('foo'); resolve('foo');
@ -446,10 +445,10 @@ var g = function* () {
function run (generator) { function run (generator) {
var it = generator(); var it = generator();
function go(result) { function go(result) {
if (result.done) return result.value; if (result.done) return result.value;
return result.value.then(function (value) { return result.value.then(function (value) {
return go(it.next(value)); return go(it.next(value));
}, function (error) { }, function (error) {
@ -461,345 +460,13 @@ function run (generator) {
} }
run(g); run(g);
``` ```
上面代码的Generator函数g之中有一个异步操作getFoo它返回的就是一个Promise对象。函数run用来处理这个Promise对象并调用下一个next方法。 上面代码的Generator函数g之中有一个异步操作getFoo它返回的就是一个Promise对象。函数run用来处理这个Promise对象并调用下一个next方法。
## async函数 ## async函数
### 概述
async函数与Promise、Generator函数一样是用来取代回调函数、解决异步操作的一种方法。它本质上是Generator函数的语法糖。async函数并不属于ES6而是被列入了ES7但是traceur、Babel.js、regenerator等转码器已经支持这个功能转码后立刻就能使用。 async函数与Promise、Generator函数一样是用来取代回调函数、解决异步操作的一种方法。它本质上是Generator函数的语法糖。async函数并不属于ES6而是被列入了ES7但是traceur、Babel.js、regenerator等转码器已经支持这个功能转码后立刻就能使用。
下面是一个Generator函数依次读取两个文件 async函数的详细介绍请看《异步操作》一章
```javascript
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
```
上面代码中readFile函数是`fs.readFile`的Promise版本。
写成async函数就是下面这样。
```javascript
var asyncReadFile = async function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
```
一比较就会发现async函数就是将Generator函数的星号*替换成async将yield替换成await仅此而已。
async函数对Generator函数的改进体现在以下三点。
1内置执行器。Generator函数的执行必须靠执行器而async函数自带执行器。也就是说async函数的执行与普通函数一模一样只要一行。
```javascript
var result = asyncReadFile();
```
2更好的语义。async和await比起星号和yield语义更清楚了。async表示函数里有异步操作await表示紧跟在后面的表达式需要等待结果。
3更广的适用性。co函数库约定yield命令后面只能是Thunk函数或Promise对象而async函数的await命令后面可以跟Promise对象和原始类型的值数值、字符串和布尔值但这时等同于同步操作
### 实现
async函数的实现就是将Generator函数和自动执行器包装在一个函数里。
```javascript
async function fn(args){
// ...
}
// 等同于
function fn(args){
return spawn(function*() {
// ...
});
}
```
所有的async函数都可以写成上面的第二种形式其中的spawn函数就是自动执行器。
下面给出spawn函数的实现基本就是前文自动执行器的翻版。
```javascript
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
```
### 用法
同Generator函数一样async函数返回一个Promise对象可以使用then方法添加回调函数。当函数执行的时候一旦遇到await就会先返回等到触发的异步操作完成再接着执行函数体内后面的语句。
下面是一个例子。
```javascript
async function getStockPriceByName(name) {
var symbol = await getStockSymbol(name);
var stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result){
console.log(result);
});
```
上面代码是一个获取股票报价的函数函数前面的async关键字表明该函数内部有异步操作。调用该函数时会立即返回一个Promise对象。
上面的例子用Generator函数表达就是下面这样。
```javascript
function getStockPriceByName(name) {
return spawn(function*(name) {
var symbol = yield getStockSymbol(name);
var stockPrice = yield getStockPrice(symbol);
return stockPrice;
});
}
```
上面的例子中spawn函数是一个自动执行器由JavaScript引擎内置。它的参数是一个Generator函数。async...await结构本质上是在语言层面提供的异步任务的自动执行器。
下面是一个更一般性的例子,指定多少毫秒后输出一个值。
```javascript
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value)
}
asyncPrint('hello world', 50);
```
上面代码指定50毫秒以后输出“hello world”。
### 注意点
await命令后面的Promise对象运行结果可能是rejected所以最好把await命令放在try...catch代码块中。
```javascript
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise().catch(function (err){
console.log(err);
};
}
```
await命令只能用在async函数之中如果用在普通函数就会报错。
```javascript
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
}
```
上面代码会报错因为await用在普通函数之中了。但是如果将forEach方法的参数改成async函数也有问题。
```javascript
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 可能得到错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
```
上面代码可能不会正常工作原因是这时三个db.post操作将是并发执行也就是同时执行而不是继发执行。正确的写法是采用for循环。
```javascript
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
```
如果确实希望多个请求并发执行可以使用Promise.all方法。
```javascript
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的写法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
```
ES6将await增加为保留字。使用这个词作为标识符在ES5是合法的在ES6将抛出SyntaxError。
### 与Promise、Generator的比较
我们通过一个例子来看Async函数与Promise、Generator函数的区别。
假定某个DOM元素上面部署了一系列的动画前一个动画结束才能开始后一个。如果当中有一个动画出错就不再往下执行返回上一个成功执行的动画的返回值。
首先是Promise的写法。
```javascript
function chainAnimationsPromise(elem, animations) {
// 变量ret用来保存上一个动画的返回值
var ret = null;
// 新建一个空的Promise
var p = Promise.resolve();
// 使用then方法添加所有动画
for(var anim in animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
})
}
// 返回一个部署了错误捕捉机制的Promise
return p.catch(function(e) {
/* 忽略错误,继续执行 */
}).then(function() {
return ret;
});
}
```
虽然Promise的写法比回调函数的写法大大改进但是一眼看上去代码完全都是Promise的APIthen、catch等等操作本身的语义反而不容易看出来。
接着是Generator函数的写法。
```javascript
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
var ret = null;
try {
for(var anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
});
}
```
上面代码使用Generator函数遍历了每个动画语义比Promise写法更清晰用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于必须有一个任务运行器自动执行Generator函数上面代码的spawn函数就是自动执行器它返回一个Promise对象而且必须保证yield语句后面的表达式必须返回一个Promise。
最后是Async函数的写法。
```javascript
async function chainAnimationsAsync(elem, animations) {
var ret = null;
try {
for(var anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
}
```
可以看到Async函数的实现最简洁最符合语义几乎没有语义不相关的代码。它将Generator写法中的自动执行器改在语言层面提供不暴露给用户因此代码量最少。如果使用Generator写法自动执行器需要用户自己提供。