Promise
是什么
Promise 是异步编程的一种解决方案。Promise 对象表示了异步操作的最终状态(完成或失败)和返回的结果。
其实我们在 jQuery 的 ajax 中已经见识了部分 Promise 的实现,通过 Promise,我们能够将回调转换为链式调用,也起到解耦的作用。
怎么用
Promise 接口的基本思想是让异步操作返回一个 Promise 对象
三种状态和两种变化途径
Promise 对象只有三种状态。
- 异步操作“未完成”(pending)
- 异步操作“已完成”(resolved,又称 fulfilled)
- 异步操作“失败”(rejected)
这三种的状态的变化途径只有两种。
- 异步操作从“未完成”到“已完成”
- 异步操作从“未完成”到“失败”。
这种变化只能发生一次,一旦当前状态变为“已完成”或“失败”,就意味着不会再有新的状态变化了。因此,Promise 对象的最终结果只有两种。
异步操作成功,Promise 对象传回一个值,状态变为 resolved。
异步操作失败,Promise 对象抛出一个错误,状态变为 rejected。
生成 Promise 对象
通过 new Promise 来生成 Promise 对象:
var promise = new Promise(function(resolve, reject) {
// 异步操作的代码
if (/* 异步操作成功 */){
resolve(value)
} else {
reject(error)
}
})
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve 会将 Promise 对象的状态从 pending 变为 resolved,reject 则是将 Promise 对象的状态从 pending 变为 rejected。
Promise 构造函数接受一个函数后会立即执行这个函数
var promise = new Promise(function () {
console.log('Hello World');
});
// Hello World
then 和 catch 回调
Promise 对象生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。then 方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为 resolved 时调用,第二个回调函数是 Promise 对象的状态变为 rejected 时调用。第二个函数是可选的。分别称之为成功回调和失败回调。成功回调接收异步操作成功的结果为参数,失败回调接收异步操作失败报出的错误作为参数。
var promise = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve('成功');
}, 3000);
});
promise.then(function (data) {
console.log(data);
});
// 3s后打印'成功'
catch 方法是 then(null, rejection)的别名,用于指定发生错误时的回调函数。
var promise = new Promise(function (resolve, reject) {
setTimeout(function () {
reject('失败');
}, 3000);
});
promise.catch(function (data) {
console.log(data);
});
// 3s后打印'失败'
Promise.all()
Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
var p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all 方法接受一个数组作为参数,p1、p2、p3 都是 Promise 对象的实例,如果不是,就会先调用下面讲到的 Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。(Promise.all 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
p 的状态由 p1、p2、p3 决定,分成两种情况。
(1)只有 p1、p2、p3 的状态都变成 resolved,p 的状态才会变成 resolved,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
(2)只要 p1、p2、p3 之中有一个被 Rejected,p 的状态就变成 Rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。
Promise.race()
与 Promise.all()类似,不过是只要有一个 Promise 实例先改变了状态,p 的状态就是它的状态,传递给回调函数的结果也是它的结果。所以很形象地叫做赛跑。
Promise.resolve()和 Promise.reject()
有时需要将现有对象转为 Promise 对象,可以使用这两个方法。
Generator(生成器)
是什么
生成器本质上是一种特殊的迭代器(参见本文章系列二之 Iterator)。ES6 里的迭代器并不是一种新的语法或者是新的内置对象(构造函数),而是一种协议 (protocol)。所有遵循了这个协议的对象都可以称之为迭代器对象。生成器对象由生成器函数返回并且遵守了迭代器协议。具体参见 MDN。
怎么用
执行过程
生成器函数的语法为 function*,在其函数体内部可以使用 yield 和 yield*关键字。
function* gen(x) {
console.log(1);
var y = yield x + 2;
console.log(2);
return y;
}
var g = gen(1);
当我们像上面那样调用生成器函数时,会发现并没有输出。这就是生成器函数与普通函数的不同,它可以交出函数的执行权(即暂停执行)。yield 表达式就是暂停标志。
之前提到了生成器对象遵循迭代器协议,所以其实可以通过 next 方法执行。执行结果也是一个包含 value 和 done 属性的对象。
遍历器对象的 next 方法的运行逻辑如下。
(1)遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值。
(2)下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式。
(3)如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值。
(4)如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined。
需要注意的是,yield 表达式后面的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行。
g.next();
// 1
// { value: 3, done: false }
g.next();
// 2
// { value: undefined, done: true }
for…of 遍历
生成器部署了迭代器接口,因此可以用 for…of 来遍历,不用调用 next 方法
function* foo() {
yield 1;
yield 2;
yield 3;
return 4;
}
for (let v of foo()) {
console.log(v);
}
// 1
// 2
// 3
yield*表达式
从语法角度看,如果 yield 表达式后面跟的是一个遍历器对象,需要在 yield 表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为 yield表达式。yield后面只能跟迭代器,yield*的功能是将迭代控制权交给后面的迭代器,达到递归迭代的目的
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
for (let v of bar()) {
console.log(v);
}
// x
// a
// b
// y
自动执行
下面是使用 Generator 函数执行一个真实的异步任务的例子:
var fetch = require('node-fetch');
function* gen() {
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。这段代码非常像同步操作,除了加上了 yield 命令。
执行这段代码的方法如下
var g = gen();
var result = g.next();
result.value
.then(function (data) {
return data.json();
})
.then(function (data) {
g.next(data);
});
上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用 next 方法(第二行),执行异步任务的第一阶段。由于 Fetch 模块返回的是一个 Promise 对象,因此要用 then 方法调用下一个 next 方法。
可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。
那么如何自动化异步任务的流程管理呢?
Generator 函数就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
两种方法可以做到这一点。
回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。
Thunk 函数
本节很简略,可能会看不太明白,请参考Thunk 函数的含义和用法
Thunk 函数的含义:编译器的”传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。
任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式,可以通过一个 Thunk 函数转换器来转换。
Thunk 函数真正的威力,在于可以自动执行 Generator 函数。我们可以实现一个基于 Thunk 函数的 Generator 执行器,然后直接把 Generator 函数传入这个执行器即可。
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
function* g() {
// ...
}
run(g);
Thunk 函数并不是 Generator 函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。
基于 Promise 对象的自动执行
首先,将方法包装成一个 Promise 对象(fs 是 nodejs 的一个内置模块)。
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 函数。
var g = gen();
g.next().value.then(function (data) {
g.next(data).value.then(function (data) {
g.next(data);
});
});
观察上面的执行过程,其实是在递归调用,我们可以用一个函数来实现:
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 模块是 nodejs 社区著名的 TJ 大神写的一个小工具,用于 Generator 函数的自动执行。
下面是一个 Generator 函数,用于依次读取两个文件
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
var co = require('co');
co(gen);
co 模块可以让你不用编写 Generator 函数的执行器。Generator 函数只要传入 co 函数,就会自动执行。co 函数返回一个 Promise 对象,因此可以用 then 方法添加回调函数。
co(gen).then(function () {
console.log('Generator 函数执行完成');
});
co 模块的原理:其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co(co v4.0 版以后,yield 命令后面只能是 Promise 对象,不再支持 Thunk 函数)。
async(异步)函数
是什么
async 函数属于 ES7。目前,它仍处于提案阶段,但是转码器 Babel 和 regenerator 都已经支持。async 函数可以说是目前异步操作最好的解决方案,是对 Generator 函数的升级和改进。
怎么用
1)语法
async 函数声明定义了异步函数,它会返回一个 AsyncFunction 对象。和普通函数一样,你也可以定义一个异步函数表达式。
调用异步函数时会返回一个 promise 对象。当这个异步函数成功返回一个值时,将会使用 promise 的 resolve 方法来处理这个返回值,当异步函数抛出的是异常或者非法值时,将会使用 promise 的 reject 方法来处理这个异常值。
异步函数可能会包括 await 表达式,这将会使异步函数暂停执行并等待 promise 解析传值后,继续执行异步函数并返回解析值。
注意:await 只能用在 async 函数中。
前面依次读取两个文件的代码写成 async 函数如下:
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。
2)async 函数的改进
async 函数对 Generator 函数的改进,体现在以下三点。
(1)内置执行器。Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
var result = asyncReadFile();
(2)更好的语义。async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
3)基本用法
同 Generator 函数一样,async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
function resolveAfter2Seconds(x) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function add1(x) {
var a = resolveAfter2Seconds(20);
var b = resolveAfter2Seconds(30);
return x + (await a) + (await b);
}
add1(10).then((v) => {
console.log(v);
});
// 2s后打印60
async function add2(x) {
var a = await resolveAfter2Seconds(20);
var b = await resolveAfter2Seconds(30);
return x + a + b;
}
add2(10).then((v) => {
console.log(v);
});
// 4s后打印60
4)捕获错误
可以使用.catch 回调捕获错误,也可以使用传统的 try…catch。
async function myFunction () {
try {
await somethingThatReturnsAPromise()
} catch (err) {
console.log(err)
}
}
// 另一种写法
async function myFunction () {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err)
}
}
5)并发的异步操作
let foo = await getFoo();
let bar = await getBar();
多个 await 命令后面的异步操作会按顺序完成。如果不存在继发关系,最好让它们同时触发。上面的代码只有 getFoo 完成,才会去执行 getBar,这样会比较耗时。如果这两个是独立的异步操作,完全可以让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;