异步

异步方法

  • 回调函数 callback
  • 事件监听
  • 发布/订阅
  • Promise对象
  • Generator函数

Promise

Promise对象有以下两个特点

  1. 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果( resolve/reject 结果是异步的, promise内部其余代码是同步执行),可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

注意,调用resolve或reject并不会终结 Promise 的参数函数的执行。所以,最好在它们前面加上return语句,这样就不会有意外。

Promise.prototype.then() 返回的是另一个Promise实例。

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。返回的是另一个Promise实例, 另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。

1
2
3
4
5
6
7
8
9
10
11
12
const promise = new Promise((resolve, reject) => {
if(/* 异步操作成功 */) {
resolve(value);
} else {
reject(error);
}
});
promise.then((value) => {
// success
}, (error) => {
// failure
});
1
2
3
4
5
6
7
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
1
2
3
4
5
6
7
const p = Promise.all([p1,p2,p3])

p的状态由p1、p2、p3决定,分成两种情况。

1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
1
2
3
const p = Promise.race([p1,p2,p3])

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const p = Promise.allSettled([p1,p2,p3])

只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。
该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例。

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);

const allSettledPromise = Promise.allSettled([resolved, rejected]);

allSettledPromise.then(function (results) {
console.log(results);
});
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]
1
2
3
const p = Promise.any([p1,p2,p3])

只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。该方法目前是一个第三阶段的提案
1
2
3
4
5
6
7
Promise.resolve(value)
Promise.reject(value)

等价于

new Promise(resolve => resolve(value));
new Promise((resolve, reject) => reject(value));
1
2
3
4
5
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
1
2
3
4
5
6
7
8
9
10
11
12
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();
image.onload = function() {
resolve(image);
};
image.onerror = function() {
reject(new Error('Could not load image at ' + url));
};
image.src = url;
});
}
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
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};

getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
})

Iterator

Iterator 的作用有三个:

  • 一是为各种数据结构,提供一个统一的、简便的访问接口;
  • 二是使得数据结构的成员能够按某种次序排列;
  • 三是 ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费

Iterator 的遍历过程是这样的

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

  3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

  4. 不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

原生具备 Iterator 接口的数据结构如下
  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Iterable {
[Symbol.iterator]() : Iterator,
}

interface Iterator {
next(value?: any) : IterationResult,
// 循环中 throw Error 或 break,会调用return方法
?return(): IterationResult
?throw(): any
}

interface IterationResult {
value: any,
done: boolean,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
}
[...myIterable] // [1, 2, 3]

// 或者采用下面的简洁写法

let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};

for (let x of obj) {
console.log(x);
}
// "hello"
// "world"
通过遍历器实现指针结构
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
function Obj(value) {
this.value = value;
this.next = null;
}

Obj.prototype[Symbol.iterator] = function() {
var current = this;

function next() {
if (current) {
var value = current.value;
current = current.next;
return { done: false, value: value };
} else {
return { done: true };
}
}
return { next: next };
}

var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);
one.next = two;
two.next = three;
for (var i of one){
console.log(i); // 1, 2, 3
}
遍历语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for(let i=0;i<srr.length; i++) {}
# 繁琐

arr.forEach((item, index) => {})
# 无法跳出循环

for(let keyString in arr) {}
# 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
# for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
# 某些情况下,for...in循环会以任意顺序遍历键名。

for(let value of arr) {}
# 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
# 不同于forEach方法,它可以与break、continue和return配合使用。
# 提供了遍历所有数据结构的统一操作接口。

Generator

执行Generator函数会返回一个遍历器对象,是一个遍历器对象生成函数,返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态

1
2
3
4
5
function* myGenerator() {
yield 1;
yield 2;
return 'ending';
}
生成的遍历器对象的next方法的运行逻辑如下
  1. 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

  2. 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

  3. 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值,此时返回的对象的done属性值为true

  4. 如果该函数没有return语句,则返回的对象的value属性值为undefined。

需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}

# for...of 循环不包含{done:true}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。
注意,不要混淆遍历器对象的throw方法和全局的throw命令。全局的throw命令抛出的错误只能被函数体外的catch语句捕获。
如果 Generator 函数内部没有部署try…catch代码块,那么throw方法抛出的错误,将被外部try…catch代码块捕获。
如果 Generator 函数内部和外部,都没有部署try…catch代码块,那么程序将报错,直接中断执行。

g.throw()方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。否则不会被内部捕获。
throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。

这种函数体内捕获错误的机制,大大方便了对错误的处理。多个yield表达式,可以只用一个try…catch代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次catch语句就可以了。

Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的catch捕获。

一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};

var i = g();
i.next();

try {
i.throw('a');
i.throw('b'); // 被外部catch后就结束了
i.throw('c'); // 这句不会被执行
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}

var g = gen();
g.next() // a
g.throw() // b
g.next() // c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

ES6 提供了yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
yield*后面的 Generator 函数(没有return语句时),不过是for…of的一种简写形式,完全可以用后者替代前者。反之,在有return语句时,则需要用var value = yield* iterator的形式获取return语句的值。

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
// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}

// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}

// 下面生成二叉树
function make(array) {
// 判断是否为叶节点
if (array.length == 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}

let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);

// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}

result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
Generator函数的this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}

function F() {
return gen.call(gen.prototype);
}

var f = new F();

f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3
Generator是实现状态机的最佳结构
1
2
3
4
5
6
7
8
var clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
Generator与协程

协程(coroutine)是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。

传统的“子例程”(subroutine)采用堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。

从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。

不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。

由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。

Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。

如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield表达式交换控制权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}

function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}

var it = main();
it.next();
1
2
3
4
5
6
7
8
9
10
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。

Thunk函数

Thunk 函数是自动执行 Generator 函数的一种方法。

编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function f(m) {
return m * 2;
}

f(x + 5);

// 等同于

var thunk = function () {
return x + 5;
};

function f(thunk) {
return thunk() * 2;
}

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

1
2
3
4
5
6
7
8
9
10
11
12
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);
简单Thunk函数转换器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ES5版本
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};

// ES6版本
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
生产环境用thunkify

thunkify只允许回调函数执行一次

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
function thunkify(fn){
return function(){
var args = new Array(arguments.length);
var ctx = this;

for(var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}

return function(done){ // done(err, data)
var called;

args.push(function(){ // 将封装后的callback加入到传入参数中
if (called) return;
called = true;
done.apply(null, arguments);
});

try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};
自动执行Generator同步操作
1
2
3
4
5
6
7
8
9
10
11
function* gen() {
// ...
}

var g = gen();
var res = g.next();

while(!res.done){
console.log(res.value);
res = g.next();
}
Thunk函数可用于Generator函数的自动流程管理

使用run函数的前提是每一个异步操作都是thunk函数,需要在异步完成后将执行权交还给Generator函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function run(fn) { // fn为generator
var gen = fn();

function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
// result.value就是thunk函数
// result.value异步完成后将执行权交还给Generator函数
}

next();
}

function* g() {
// ...
}

run(g);
示例
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
function getData(data, cb) {
setTimeout(() => {
console.log(data);
cb(null,data);
},1000);
}

const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};

function run(fn) {
var gen = fn();

function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}

next();
}

const getDataThunk = Thunk(getData);

function* generator() {
let data1 = yield getDataThunk(1);
let data2 = yield getDataThunk(2);
let data3 = yield getDataThunk(3);
console.log(data1, data2, data3)
}

run(generator);
<!--
1
2
3
1 2 3
-->

Thunk 函数并不是 Generator 函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。

co 模块的原理

为什么 co 可以自动执行 Generator 函数?

前面说过,Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

两种方法可以做到这一点。

(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

(2)Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。

co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co。

Promise也可用于Generator函数的自动流程管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function run(fn) {
var gen = fn();

function next(data) {
var result = gen.next(data);
if (result.done) return result.value;
result.value.then(next);
// result.value就是Promise对象
}

next();
}

function* g() {
// ...
}

run(g);
co处理并发的异步操作
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
// 数组的写法
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);

# 并发3个异步操作
co(function* () {
var values = [n1, n2, n3];
yield values.map(function* somethingAsync(x) {
// do something async
return y
});
});

Stream 模式

Node 提供 Stream 模式读写数据,特点是一次只处理数据的一部分,数据分成一块块依次处理,就好像“数据流”一样。这对于处理大规模数据非常有利。Stream 模式使用 EventEmitter API,会释放三个事件。

  • data事件:下一块数据块已经准备好了。
  • end事件:整个“数据流”处理完了。
  • error事件:发生错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const co = require('co');
const fs = require('fs');

const stream = fs.createReadStream('./les_miserables.txt');
let valjeanCount = 0;

co(function*() {
while(true) {
const res = yield Promise.race([
new Promise(resolve => stream.once('data', resolve)),
new Promise(resolve => stream.once('end', resolve)),
new Promise((resolve, reject) => stream.once('error', reject))
]);
if (!res) {
break;
}
stream.removeAllListeners('data');
stream.removeAllListeners('end');
stream.removeAllListeners('error');
valjeanCount += (res.toString().match(/valjean/ig) || []).length;
}
console.log('count:', valjeanCount); // count: 1120
});

Async/Await

async 函数是什么?一句话,它就是 Generator 函数的语法糖。async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

async函数对 Generator 函数的改进,体现在以下四点
  1. 内置执行器

    Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。
    asyncReadFile();

    上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

  2. 更好的语义

    async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

  3. 更广的适用性

    co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

  4. 返回值是 Promise

    async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

    进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

1
2
3
4
5
6
7
8
9
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name);
const stockPrice = await getStockPrice(symbol);
return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
console.log(result);
});
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
// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}

async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try…catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# thenable对象也可以await, await将其视为Promise处理
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(
() => resolve(Date.now() - startTime),
this.timeout
);
}
}

(async () => {
const sleepTime = await new Sleep(1000);
console.log(sleepTime);
})();
// 1000
await异步并发执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 写法一
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);
}
await异步继发执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 写法一
async function dbFuc(db) {
let docs = [{}, {}, {}];

for (let doc of docs) {
await db.post(doc);
}
}

// 写法二
async function dbFuc(db) {
let docs = [{}, {}, {}];

await docs.reduce(async (_, doc) => {
await _;
await db.post(doc);
}, undefined);
}
async函数的执行原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

1
2
3
4
5
6
7
8
9
10
11
async function fn(args) {
// ...
}

// 等同于

function fn(args) {
return spawn(function* () {
// ...
});
}
spawn自动执行器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
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); });
});
}

与其他异步处理方法的比较

需求:假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

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
// Promise 写法
function chainAnimationsPromise(elem, animations) {

// 变量ret用来保存上一个动画的返回值
let ret = null;

// 新建一个空的Promise
let p = Promise.resolve();

// 使用then方法,添加所有动画
for(let anim of animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
});
}

// 返回一个部署了错误捕捉机制的Promise
return p.catch(function(e) {
/* 忽略错误,继续执行 */
}).then(function() {
return ret;
});

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Generator 写法
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
let ret = null;
try {
for(let anim of animations) {
ret = yield anim(elem); // anim返回promise
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
});
}
1
2
3
4
5
6
7
8
9
10
11
12
// async 写法
async function chainAnimationsAsync(elem, animations) {
let ret = null;
try {
for(let anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
}