目录
(2)、Promise 函数接受的参数——resolve 函数和 reject 函数
(3)、Promise.prototype.finally()
(3)、Promise 与 Generator 函数结合使用
(1)、Generator.prototype.next()
(2)、Generator.prototype.throw()
(3)、Generator.prototype.return()
3、async 函数与 Promise 对象、Generator 函数的比较
前言
async/await 和 Generator 都是 Promise 的语法糖,越来越甜了。
一、Promise 对象
1、Promise 概述
Promise 是异步编程的一种解决方案。
Promise 任务是微任务(事件任务相关请戳这里)。
Promise 本身是同步的,他的 then() 方法和 catch() 方法是异步的。
Promise 对象的状态:
- Promise 异步操作有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。
- Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型)。
const p1 = new Promise(function(resolve,reject){
resolve('success1');
resolve('success2');
});
const p2 = new Promise(function(resolve,reject){
resolve('success3');
reject('reject');
});
p1.then(function(value){
console.log(value); // success1
});
p2.then(function(value){
console.log(value); // success3
});
Promise 对象的优点:
- 有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
- Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise 对象的缺点:
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署Promise更好的选择。
2、Promise 构造函数
(1)、创建一个 Promise 实例
Promise 是一个构造函数,用来生成 Promise 实例。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 操作成功 */){
resolve(value);
} else {
reject(error);
}
});
(2)、Promise 函数接受的参数——resolve 函数和 reject 函数
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject 。
resolve 和 reject 是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve函数:
- 用来将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
- 可以接受一个参数:(可选的)一个值或另一个Promise对象。
reject函数:
- 用来将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
- 可以接受一个参数:Error对象的实例,表示抛出的错误。
①、resolve 函数接收一个值,比如:接收一个字符串。
const p = new Promise(function (resolve, reject) {
resolve(1);
});
p.then(result => console.log(2));
// 2
上述代码中,可以给resolve函数预设一个参数值,也可以在then() 方法中指定一个参数值,后者会覆盖前者。
②、resolve 函数接收另一个 Promise 对象作为参数。
// 案例一
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => resolve("successful"), 3000)
});
const p2 = new Promise(function (resolve, reject) {
resolve(p1);
})
p2.then(result => console.log(result));
// successful
// 案例二
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
resolve(p1);
})
p2.then(result => console.log(result), error => console.log(error))
// Error: fail
上面代码中,p1和p2都是 Promise 的实例,但是p2的resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。
③、调用 resolve 或 reject 并不会终结 Promise 的参数函数的执行。
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
上面代码中,调用 resolve(1) 以后,后面的 console.log(2) 还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
一般来说,调用 resolve 或 reject 以后,Promise 的使命就完成了,后继操作应该放到 then() 方法里面,而不应该直接写在 resolve 或 reject 的后面。所以,最好在它们前面加上 return 语句,这样就不会有意外:
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
(3)、Promise 函数的执行
Promise 新建后立即执行,其回调函数会异步执行,无法中途取消。
new Promise((resolve, reject) => {
console.log(1);
resolve(2);
console.log(3);
}).then((res) => console.log(4));
console.log(5);
// 1
// 3
// 5
// 4(2 被 4 覆盖了)
上面代码中,因为 Promise 创建后立即执行,所以首先输出的是1。因为 Promise 的回调函数会异步执行,将在当前脚本所有同步任务执行完才会执行,并且,由于调用 resolve 或 reject 并不会终结,所以接着输出 3。然后是 5 ,至此,所有同步脚本执行完毕。最后再去执行异步脚本,输出 4,没有输出 2 是因为传入的 4 将 2 覆盖了。
(4)、promise 函数的链式调用
如果依次读取两个以上的文件,就会出现多重嵌套,不便于实现与管理代码,代码的可读性差。因为多个异步操作形成了强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为"回调函数地狱"(callback hell)。
Promise 对象就是为了解决这个问题而提出的。允许将回调函数的嵌套,改成链式调用。采用 Promise,连续读取多个文件。
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
上面代码中,我使用了fs-readfile-promise模块,它的作用就是返回一个 Promise 版本的readFile函数。Promise 提供then方法加载回调函数,catch方法捕捉执行过程中抛出的错误。
3、Promise 构造函数的实例方法
- Promise.prototype.then():当 promise 执行后返回 fullfilled 状态时执行该函数,该函数里接收并执行一个 resolve 函数。用于执行 “成功” 后的处理。
- Promise.prototype.catch():当 promise 执行后返回 rejected 状态时执行该函数,该函数里接收并执行一个 reject 函数。用于执行 “出错” 后的处理。
- Promise.prototype.finally():无论 promise 执行的结果是成功还是失败,都会执行该函数里的回调函数。
(1)、Promise.prototype.then()
then() 方法接收两个函数作为参数,第一个参数是 Promise 执行成功时的回调;第二个参数是(可选的) Promise 执行失败时的回调,用来抛出错误。两个函数只会有一个被调用。then() 方法总是返回一个新的 Promise 实例。
promise.then(function(result) {
console.log(result);
}, function(error) {
console.log(error);
});
// 用箭头函数简写为
promise.then(result => console.log(result), error => console.log(error));
上述代码中,一个 promise 对象调用 then() 方法,如果该 promise 对象的状态变为resolved,就调用第一个回调函数,如果该 promise 对象的状态变为rejected,就调用第二个回调函数。最终返回一个新的 Promise 实例。
由于 then() 方法总是返回一个新的 Promise 实例,所以,then() 方法可以采用链式写法,用来指定一组按照次序调用的回调函数。
const p = new Promise(function(resolve, reject){
resolve(1);
})
.then(value => {
console.log(value); // 第一个then // 1
return value * 2;
})
.then(value => {
console.log(value); // 第二个then // 2
})
.then(value => {
console.log(value); // 第三个then // undefined
// 嵌套 Promise
return Promise.resolve('resolve');
})
.then(value => {
console.log(value); // 第四个then // resolve
throw new Error('reject');
})
.then(value => {
console.log( value);
}, err => {
console.log(err); // 最后一个then //Error: reject
});
由上面的代码可知,then 方法会返回一个 resolved 或 rejected 状态的 Promise 对象用于链式调用,且 Promise 对象的值就是这个返回值。
使用 then() 方法的注意事项:
- 简便的 Promise 链式编程最好保持扁平化,尽量不要嵌套 Promise,以提高代码的可读性。
- 要总是返回或终止 Promise 链。大多数浏览器中不能终止的 Promise 链里的 reject,建议后面都跟上 .catch(error => console.log(error))。
(2)、Promise.prototype.catch()
catch() 方法是then(null, rejection)或then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
该方法可以接受一个参数:一个 Error 对象的实例。
该方法总是返回一个新的 Promise 实例。
// 写法一
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
}).then(val => console.log('resolved:', val))
.catch(error => console.log(error));
// Error: test
// 写法二
const promise = new Promise(function(resolve, reject) {
return reject(new Error('test'));
}).then(val => console.log('resolved:', val))
.catch(error => console.log(error));
// Error: test
如果 Promise 状态已经变成resolved,再抛出错误是无效的。
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止,而且只会捕获最先发生的那个错误。
const p = new Promise((resolve, reject)=>{
resolve();
});
p.then(value=>{
console.log("hello");
throw new Error("222");
}).then((newValue) => {
console.log(newValue);
throw new Error("333");
}).catch(error=>console.log(error));
// hello
// Error: 222
一般来说,不要在then()方法里面定义 reject 状态的回调函数(即then的第二个参数),总是建议 Promise 对象后面要跟catch()方法,这样可以处理 Promise 内部发生的错误。
// 不推荐
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// 推荐
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
catch() 方法返回的还是一个 Promise 对象,因此也可以链式调用。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
(3)、Promise.prototype.finally()
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
finally 本质上是 then 方法的特例:
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 })
);
};
finally 方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是 rejected。这表明,finally 方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
4、Promise 构造函数自身的方法
- Promise.any():有一个成功就返回成功。有一个 Promise 变为 fulfilled 状态,就表示成功。
- Promise.all():有一个失败就返回失败。有一个 Promise 变为 rejected 状态,就表示失败。
- Promise.race():第一个结果就是最终结果。返回第一个执行的 Promise 的最终状态。适用于“请求超时处理”。
- Promise.allSettled():总是返回成功的。所有的 promise 都执行结束后,总是返回 fulfilled 状态。适用于“不关心异步操作的结果,只关心这些操作有没有结束”。
- Promise.resolve():该方法会将普通对象转状态为 fulfilled 的 Promise 对象。
- Promise.reject():该方法会将普通对象转状态为 rejected 的 Promise 对象。
- Promise.try():统一管理 promise 里的同步的或异步的函数,同步的函数执行后自动返回同步的结果,异步的函数执行后自动返回异步,不用分别管理。
(1)、Promise.all()
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,p的状态由p1、p2、p3决定,分成两种情况:
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
举个栗子:
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
上面代码中,promises是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。
如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]
上面代码中,p1会resolved,p2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。
如果p2没有自己的catch方法,就会调用Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错了
(2)、Promise.race()
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(console.log)
.catch(console.error);
上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。
(3)、Promise.allSettled()
Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator();
该方法返回的新的 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 }
// ]
上面代码中,Promise.allSettled()的返回值allSettledPromise,状态只可能变成fulfilled。它的监听函数接收到的参数是数组results。该数组的每个成员都是一个对象,对应传入Promise.allSettled()的两个 Promise 实例。每个对象都有status属性,该属性的值只可能是字符串fulfilled或字符串rejected。fulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值。
下面是返回值用法的例子。
const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
// 过滤出成功的请求
const successfulPromises = results.filter(p => p.status === 'fulfilled');
// 过滤出失败的请求,并输出原因
const errors = results
.filter(p => p.status === 'rejected')
.map(p => p.reason);
有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()方法无法做到这一点。
(4)、Promise.any()
Promise.any()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。该方法目前是一个第三阶段的提案 。
Promise.any()跟Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束。
const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c'),
];
try {
const first = await Promise.any(promises);
console.log(first);
} catch (error) {
console.log(error);
}
上面代码中,Promise.any()方法的参数数组包含三个 Promise 操作。其中只要有一个变成fulfilled,Promise.any()返回的 Promise 对象就变成fulfilled。如果所有三个操作都变成rejected,那么await命令就会抛出错误。
Promise.any()抛出的错误,不是一个一般的错误,而是一个 AggregateError 实例。它相当于一个数组,每个成员对应一个被rejected的操作所抛出的错误。下面是 AggregateError 的实现示例。
new AggregateError() extends Array -> AggregateError
const err = new AggregateError();
err.push(new Error("first error"));
err.push(new Error("second error"));
throw err;
捕捉错误时,如果不用try...catch结构和 await 命令,可以像下面这样写。
Promise.any(promises).then(
(first) => {
// Any of the promises was fulfilled.
},
(error) => {
// All of the promises were rejected.
}
);
下面是一个例子。
var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);
var alsoRejected = Promise.reject(Infinity);
Promise.any([resolved, rejected, alsoRejected]).then(function (result) {
console.log(result); // 42
});
Promise.any([rejected, alsoRejected]).catch(function (results) {
console.log(results); // [-1, Infinity]
});
(5)、Promise.resolve()
该方法用来将现有对象转为 Promise 对象。
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
Promise.resolve方法的参数分成四种情况:
①、参数是一个 Promise 实例
如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
②、参数是一个thenable对象
thenable对象指的是具有then方法的对象,比如下面这个对象。Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。注意与 Promise.reject() 方法区分。
let thenable1 = {
then(resolve, reject) {
resolve(42);
}
};
Promise.resolve(thenable1)
.then(value => {
console.log(value); // 42
console.log(value === thenable1); // false
});
上面代码中,thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出 42。
③、参数不是具有then方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。
const p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
上面代码生成一个新的 Promise 对象的实例p。由于字符串Hello不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。Promise.resolve方法的参数,会同时传给回调函数。
④、不带有任何参数
Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。
所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。
const p = Promise.resolve();
p.then(function () {
// ...
});
需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是立即执行,因此最先输出。
(6)、Promise.reject()
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
上面代码生成一个 Promise 对象的实例p,状态为rejected,回调函数会立即执行。
Promise.reject() 方法不会像 Promise.resolve() 方法那样将接受的一个参数自动转为 Promise 对象后执行thenable对象的then方法,而是会将这个reject的参数原封不动地作为后续方法的参数。
const thenable2 = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable2)
.catch(e => {
console.log(e);// {then: ƒ}
console.log(e === thenable2);// true
})
(7)、Promise.try()
实际开发中,经常遇到一种情况:不知道或者不想区分,函数 f() 是同步函数还是异步操作,但是想用 Promise 来处理它。因为这样就可以不管 f() 是否包含异步操作,都用 then 方法指定下一步流程,用 catch 方法处理 f() 抛出的错误。一般就会采用下面的写法。
Promise.resolve().then(f)
上面的写法有一个缺点,就是如果 f() 是同步函数,那么它会在本轮事件循环的末尾执行。也就是说,用 Promise 包装了以后,就变成异步执行了。
那么有没有一种方法,让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 呢?回答是可以的,并且还有两种写法。
第一种写法是用async函数来写。
const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next
上面代码中,第二行是一个立即执行的匿名函数,会立即执行里面的async函数,因此如果f是同步的,就会得到同步的结果;如果f是异步的,就可以用then指定下一步,就像下面的写法。
(async () => f())()
.then(...)
.catch(...)
第二种写法是使用new Promise()。
const f = () => console.log('now');
(
() => new Promise(
resolve => resolve(f())
)
)();
console.log('next');
// now
// next
上面代码也是使用立即执行的匿名函数,执行new Promise()。这种情况下,同步函数也是同步执行的。
鉴于这是一个很常见的需求,所以 ES6 提供了 Promise.try() 方法替代上面的写法。
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
由于Promise.try为所有操作提供了统一的处理机制,所以如果想用then方法管理流程,最好都用Promise.try包装一下。这样可以更好地管理异常。
举个栗子:
function getUsername(userId) {
return database.users.get({id: userId})
.then(function(user) {
return user.name;
});
}
上面代码中,database.users.get()返回一个 Promise 对象,如果抛出异步错误,可以用catch方法捕获,就像下面这样写。
database.users.get({id: userId})
.then(...)
.catch(...)
但是database.users.get()可能还会抛出同步错误(比如数据库连接错误,具体要看实现方法),这时你就不得不用try...catch去捕获。
try {
database.users.get({id: userId})
.then(...)
.catch(...)
} catch (e) {
// ...
}
上面这样的写法就很笨拙了,这时就可以统一用promise.catch()捕获所有同步和异步的错误。
Promise.try(() => database.users.get({id: userId}))
.then(...)
.catch(...)
事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。
5、Promise 构造函数的应用
(1)、Promise + fetch 获取数据
export const getApiListByType = (projectName) => {
return new Promise((resolve, reject) => {
fetch( url,
{
method: 'GET',
mode: 'cors',
credentials: 'include'
})
.then(res => resolve(res.json()))
.catch(err => {
reject(err)
})
})
}
上面代码,fetch 请求成功时,在 then() 方法中调用 resolve() 方法将成功的结果抛出。执行失败时,在 catch() 方法中调用 reject() 方法将失败的结果抛出。
在外面使用此方法获取数据时:
this.getApiListByType(demoName)
.then(res=>{
console.log('--->res', res)
})
.catch(e=>{
console.log(e)
)
上面代码,获取数据成功时,执行 then() 方法,接收成功的结果。获取数据失败时,执行 catch() 方法,接收失败的结果。
(2)、异步加载图片
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;
});
}
可以简写为:
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用resolve方法,否则就调用reject方法。
(3)、Promise 与 Generator 函数结合使用
使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。
function getFoo () {
return new Promise(function (resolve, reject){
resolve('foo');
});
}
const g = function* () {
try {
const foo = yield getFoo();
console.log(foo);
} catch (e) {
console.log(e);
}
};
function run (generator) {
const it = generator();
function go(result) {
if (result.done) return result.value;
return result.value.then(function (value) {
return go(it.next(value));
}, function (error) {
return go(it.throw(error));
});
}
go(it.next());
}
run(g);
上面代码的 Generator 函数g之中,有一个异步操作getFoo,它返回的就是一个Promise对象。函数run用来处理这个Promise对象,并调用下一个next方法。
6、Promise 构造函数的问题
Promise 的最大问题是:代码冗余。原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。那么,有没有更好的写法呢?请看 ES6 对“协程”的不完全实现的结果—— Generator 函数。
7、手写 Promise 构造函数
请戳这里:https://github.com/leocoder351/my-promise
二、Generator 函数
Generator 函数是 ES6 提供的一种异步编程解决方案。
至此,JavaScript 异步编程的实现有以下 5 种方案:
- 回调函数
- 事件监听
- 发布/订阅
- Promise 对象
- Generator 函数
Generator 函数的最大特点是:可以暂停函数执行,返回任意表达式的值。
1、Generator 函数是 ES6 对协程的实现
协程是一种程序运行的方式:多个线程互相协作,完成异步任务。
协程有点像函数,又有点像线程。它的运行流程大致如下:
- 第一步,协程A开始执行。
- 第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
- 第三步,(一段时间后)协程B交还执行权。
- 第四步,协程A恢复执行。
上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。
Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield表达式交换控制权。
function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
上面代码中,调用 Generator 函数,会返回一个内部指针(即遍历器)g。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针g的next方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的yield语句,上例是执行到x + 2为止。
2、Generator 函数的语法
(1)、Generator 函数组成
Generator 有两个区分于普通函数的部分:
- 一是,在 function 后面,函数名之前有个 * ;
- 二是,在函数体内部使用 yield 表达式。
其中 * 用来表示函数为 Generator 函数,yield 用来定义函数内部的状态。
function* func(){
console.log("one");
yield '1';
console.log("two");
yield '2';
console.log("three");
return '3';
}
(2)、Generator 函数的执行机制
调用 Generator 函数和调用普通函数一样,在函数名后面加上 () 即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,需要调用遍历器对象Iterator 的 next 方法,指针就会从函数头部或者上一次停下来的地方开始执行。
Generator 函数是分段执行的,yield 表达式是暂停执行的标记,而 next 方法可以恢复执行。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next();
// {value: "hello", done: false}
hw.next();
// {value: "world", done: false}
hw.next();
// {value: "ending", done: true}
hw.next()
// { value: undefined, done: true }
上述代码中,定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
上述代码中,一共调用了四次 next 方法:
- 第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。
- 第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。
- 第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。
- 第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。
【总结一下】Generator 函数的执行顺序
调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
(3)、作为对象属性的 Generator 函数
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
// 等同于
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
(4)、Generator 函数的作用域
Generator 函数执行产生的上下文环境,一旦遇到yield命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。
function* gen() {
yield 1;
return 2;
}
let g = gen();
console.log(
g.next().value,
g.next().value,
);
Generator 函数的实例(一个遍历器对象)能够继承 Generator 函数的 prototype 对象上的一切属性和方法。
function* g() {}
g.prototype.a= function () {
return 11;
};
let obj = g();
obj instanceof g;// true
obj.a();// 11
上面代码表明,Generator 函数g返回的遍历器obj,是g的实例,而且继承了g.prototype。
(5)、Generator 函数的注意事项
①、Generator 函数返回的总是遍历器对象,而不是this对象。
如果把 Generator 函数当作普通的构造函数,并不会生效,因为 Generator 函数返回的总是遍历器对象,而不是this对象。
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a;// undefined
上面代码中,obj对象是一个遍历器对象。Generator 函数g在this对象上面添加了一个属性a,但是obj对象拿不到这个属性。
那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this?
下面是一个变通方法。首先,生成一个空对象,使用call方法绑定 Generator 函数内部的this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
上面代码中,首先是 F 内部的this对象绑定 obj 对象,然后调用它,返回一个 Iterator 对象。这个对象执行三次 next 方法(因为 F 内部有两个 yield 表达式),完成 F 内部所有代码的运行。这时,所有内部属性都绑定在 obj 对象上了,因此 obj 对象也就成了 F 的实例。
上面代码中,执行的是遍历器对象 f,但是生成的对象实例是 obj,有没有办法将这两个对象统一呢?
一个办法就是将 obj 换成 F.prototype。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
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 函数不能跟new命令一起用,会报错。
function* F() {
yield this.x = 2;
yield this.y = 3;
}
new F()
// TypeError: F is not a constructor
下面是一个变通方法。
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
3、yield* 表达式
(1)、yield* 表达式与 yield 表达式
yield:返回一个遍历器对象。
yield* 表达式:在返回一个遍历器对象后,立即调用该遍历器对象。也就是说,在 Generator 函数内部调用另一个 Generator 函数。
function* inner() {
yield 'hello!';
}
//不使用 yield* 表达式
function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
var gen = outer1()
gen.next().value;// "open"
gen.next().value;// 返回一个遍历器对象:inner {<suspended>}
gen.next().value;// "close"
// 使用 yield* 表达式
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
上面例子中,outer2使用了yield*,outer1没使用。结果就是,outer1返回一个遍历器对象,outer2返回该遍历器对象的内部值。
(2)、yield*表达式的特点
- yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。
- 任何数据结构只要有 Iterator 接口,就可以被yield*遍历。
- 如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。
①、yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
var myBar = bar();
console.log(myBar.next());// {value: "x", done: false}
console.log(myBar.next());// {value: "a", done: false}
console.log(myBar.next());// {value: "b", done: false}
console.log(myBar.next());// {value: "y", done: false}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
var myBar = bar();
console.log(myBar.next());// {value: "x", done: false}
console.log(myBar.next());// {value: "a", done: false}
console.log(myBar.next());// {value: "b", done: false}
console.log(myBar.next());// {value: "y", done: false}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// x
// a
// b
// y
②、任何数据结构只要有 Iterator 接口,就可以被 yield* 表达式遍历。
比如:数组原生和字符串原生都有 Iterator 接口,所以都可以被 yield* 表达式遍历。
// 数组
function* gen(){
yield ["a", "b", "c"];
yield* ["a", "b", "c"];
}
let f = gen();
f.next() // {value: Array(3), done: false}
f.next() // { value:"a", done:false }
// 字符串
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"
上面代码中,对于数组,yield命令后面如果不加星号,返回的是整个数组,yield*表达式返回的是数组的第一个遍历器对象。对于字符串,yield表达式返回整个字符串,yield*表达式返回单个字符。因为字符串具有 Iterator 接口,所以被yield*遍历。
③、如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
上面代码结合扩展运算符,可以简写为:
function* foo() {
yield 2;
yield 3;
return "hello";
}
function* bar(fn) {
yield 1;
var v = yield* fn;
console.log("v: " + v);
yield 4;
}
console.log([...bar(foo())]);
// v: hello
// [1, 2, 3, 4]
上面代码中,存在两次遍历。第一次是扩展运算符遍历函数bar返回的遍历器对象,第二次是yield*语句遍历函数foo返回的遍历器对象。这两次遍历的效果是叠加的,最终表现为扩展运算符遍历函数foo返回的遍历器对象。所以,最后的数据表达式得到的值等于[1, 2, 3, 4]。但是,函数foo的return语句的返回值hello,会返回给函数bar内部的v变量,因此会有终端输出。
4、Generator 函数返回的遍历器对象的方法
Generator 函数总是返回一个遍历器,该实例继承了 Generator 函数的prototype对象上的方法。
Generator 函数返回的遍历器对象的方法有三个:
- next():将 yield 表达式替换成一个值。
- throw():将 yield 表达式替换成一个 throw 语句。
- return():将 yield 表达式替换成一个 return 语句。
这三个方法的作用:本质上都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式。
(1)、Generator.prototype.next()
next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息(value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
next() 方法可以接收一个参数:(可选的)一个值。
当 next() 方法传入参数时,该方法用来将 yield 表达式替换成一个值。
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;
next() 方法类似于分解的 for... of 语句,用来逐步遍历 Generator 函数生产的 Iterator 对象。
一般情况下,next 方法不传入参数的时候,yield 表达式的返回值是 undefined。当 next 传入参数的时候,该参数会作为上一步yield的返回值。
function* sendParameter(){
console.log("strat");
var x = yield '2';
console.log("one:" + x);
var y = yield '3';
console.log("two:" + y);
console.log("total:" + (x + y));
}
// next 不传参
var sendp1 = sendParameter();
console.log(sendp1.next());
// strat
// {value: "2", done: false}
console.log(sendp1.next());
// one:undefined
// {value: "3", done: false}
console.log(sendp1.next());
// two:undefined
// total:NaN
// {value: undefined, done: true}
// next 传参
var sendp2 = sendParameter();
console.log(sendp2.next(10));
// strat
// {value: "2", done: false}
console.log(sendp2.next(20));
// one:20
// {value: "3", done: false}
console.log(sendp2.next(30));
// two:30
// total:50
// {value: undefined, done: true}
(2)、Generator.prototype.throw()
throw()是将yield表达式替换成一个throw语句。
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
throw 方法可以在 Generator 函数体外面抛出异常,在函数体内部捕获。
var g = function* () {
try {
yield;
} catch (e) {
console.log('catch inner', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('catch outside', e);
}
// catch inner a
// catch outside b
(3)、Generator.prototype.return()
return()是将yield表达式替换成一个return语句。
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
return 方法返回给定值,并结束遍历 Generator 函数。
return 方法提供参数时,返回该参数;不提供参数时,返回 undefined 。
function* foo(){
yield 1;
yield 2;
yield 3;
}
var f = foo();
f.next();
// {value: 1, done: false}
f.return("foo");
// {value: "foo", done: true}
f.next();
// {value: undefined, done: true}
5、Generator 函数的应用
(1)、异步操作的同步化表达
用 Generator 函数处理异步操作,改写回调函数。具体实现就是:把异步操作写在yield表达式里面,等到调用next方法时再往后执行。
①、通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
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();
上面代码的main函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式,本身是没有值的,总是等于undefined。
②、通过 Generator 函数逐行读取文本文件。
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
(2)、控制流管理
如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
采用 Promise 改写上面的代码。
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done();
Generator 函数可以进一步改善代码运行流程。
function* longRunningTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
var value5 = yield step4(value4);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
(3)、部署 Iterator 接口
利用 Generator 函数,可以在任意对象上部署 Iterator 接口。
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7
上述代码中,myObj是一个普通对象,通过iterEntries函数,就有了 Iterator 接口。也就是说,可以在任意对象上部署next方法。
6、Generator 函数的自动执行
自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。
能够做到自动执行的机制有:回调函数、Promise 对象、Thunk 函数、co 模块等。
(1)、Thunk 函数
Thunk 函数是自动执行 Generator 函数的一种方法。
如果将参数放到一个临时函数之中,再将这个临时函数传入函数体。那么,这个临时函数就叫做 Thunk 函数。
在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// 正常版本的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);
上面代码中,fs模块的readFile方法是一个多参数函数,两个参数分别为文件名和回调函数。经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数。这个单参数版本,就叫做 Thunk 函数。
任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。
// 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);
}
};
};
使用上面的转换器的例子:
function f(a, cb) {
cb(a);
}
const ft = Thunk(f);
ft(1)(console.log) // 1
生产环境下,建议使用 Thunkify 模块。
Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 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);
上面代码的run函数,就是一个 Generator 函数的自动执行器。内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数的下一步(gen.next方法),然后判断 Generator 函数是否结束(result.done属性),如果没结束,就将next函数再传入 Thunk 函数(result.value属性),否则就直接退出。
有了这个执行器,执行 Generator 函数方便多了。不管内部有多少个异步操作,直接把 Generator 函数传入run函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在yield命令后面的必须是 Thunk 函数。
var g = function* (){
var f1 = yield readFileThunk('fileA');
var f2 = yield readFileThunk('fileB');
// ...
var fn = yield readFileThunk('fileN');
};
run(g);
上面代码中,函数g封装了n个异步的读取文件操作,只要执行run函数,这些操作就会自动完成。这样一来,异步操作不仅可以写得像同步操作,而且一行代码就可以执行。
Thunk 函数并不是 Generator 函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。
(2)、co 模块
co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。
co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co 模块。
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);
上面代码中,Generator 函数只要传入co函数,就会自动执行。
co函数返回一个Promise对象,因此可以用then方法添加回调函数。
co(gen).then(function (){
console.log('Generator 函数执行完成');
});
上面代码中,等到 Generator 函数执行结束,就会输出一行提示。
进一步学习 co 模块,请戳此链接:https://es6.ruanyifeng.com/#docs/generator-async#co-%E6%A8%A1%E5%9D%97
三、async 函数
深入浅出 async 和 await:javascript - 理解 JavaScript 的 async/await_个人文章 - SegmentFault 思否
1、async 函数的语法
(1)、创建一个 async 函数
创建一个 async 函数的方式有以下 5 种方法:
- 函数声明
- 函数表达式
- 对象的方法
- Class 的方法
- 箭头函数
// 函数声明
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 () => {};
(2)、async 函数的调用
async 函数自带执行器。也就是说,async 函数可以像普通函数那样直接通过小括号调用。
async function helloAsync(){
return "hello";
}
console.log(helloAsync());
// Promise {<resolved>: "hello"}
(3)、async 函数的 await 命令
async 函数的 await 命令后面,可以是 Promise 对象,也可以是原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
- 如果await命令后面是一个 Promise 对象,就返回该对象的结果。
function testAwait (x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function helloAsync() {
return await testAwait ("hello world");
}
helloAsync().then(v => console.log(v));
// hello world
- 如果await命令后面不是 Promise 对象,就直接返回对应的值。
async function f() {
// 等同于
// return 123;
return await 123;
}
f().then(v => console.log(v))
// 123
- 如果 await 命令后面是一个 thenable 对象(即定义了then方法的对象),那么 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命令后面是一个Sleep对象的实例。这个实例不是 Promise 对象,但是因为定义了then方法,await会将其视为Promise处理。
(4)、async 函数的执行
当async函数执行的时候,一旦遇到await命令就会先返回该命令对应的结果,等到所有异步操作完成,最后会返回一个 Promise 对象,之后,可以用then方法指定下一步的操作。
有三种情况,会让async函数执行结束,返回 Promise 对象:
- 遇到 return 语句;
- 抛出错误;
- 其内部所有 await 命令后面的 Promise 对象执行完。
①、遇到return语句
async function f() {
await console.log(123);;
return "你好!";
await console.log('666');
}
f().then(v=>console.log("v:", v));
// 123
// v: 你好!
②、抛出错误
async function f() {
throw await new Error("出错了");
await Promise.resolve('hello world'); // 不会执行
}
f().then(v=>console.log("v:", v));
// Uncaught (in promise) Error: 出错了
③、内部所有 await 命令后面的 Promise 对象执行完
function testAwait (x) {
return new Promise(resolve => {
setTimeout(() => {
console.log(x);
resolve(x);
}, 2000);
});
}
async function f() {
await console.log(123);;
await testAwait ("hello world");
return "你好!";
}
f().then(v=>console.log("v:", v))
// 123
// "hello world"
// v: 你好!
(5)、async 函数的错误处理
await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到。同时,整个 async 函数都会中断执行。
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。两种优化方法:
- 第一种是,可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。
- 第二种是,await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。
①、将 await 放在 try...catch 结构里面(推荐)
async function f() {
try {
throw await new Error("出错了");
} catch(e) {
}
return await Promise.resolve('hello world');
}
f().then(v => console.log(v))
// hello world
这样不管这个异步操作是否成功,第二个await都会执行 。
②、await 后面的 Promise 对象再跟一个 catch 方法
async function f() {
await Promise.reject('出错了').catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f().then(v => console.log(v))
// 出错了
// hello world
(6)、注意事项
- await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。
- 多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
- await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
- async 函数可以保留运行堆栈。
①、await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。
async function f() {
try {
throw await new Error("出错了");
} catch(e) {
}
return await Promise.resolve('hello world');
}
f().then(v => console.log(v))
// hello world
②、 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
上面代码中,getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
上面两种写法,getFoo和getBar都是同时触发,这样就会缩短程序的执行时间。
③、await命令只能用在async函数之中,如果用在普通函数,就会报错。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
}
上面代码会报错,因为await用在普通函数之中了。但是,如果将forEach方法的参数改成async函数,也有问题。
function dbFuc(db) { //这里不需要 async
let docs = [{}, {}, {}];
// 可能得到错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
上面代码可能不会正常工作,原因是这时三个db.post操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
也可以使用数组的reduce方法。
async function dbFuc(db) {
let docs = [{}, {}, {}];
await docs.reduce(async (_, doc) => {
await _;
await db.post(doc);
}, undefined);
}
上面例子中,reduce方法的第一个参数是async函数,导致该函数的第一个参数是前一步操作返回的 Promise 对象,所以必须使用await等待它操作结束。另外,reduce方法返回的是docs数组最后一个成员的async函数的执行结果,也是一个 Promise 对象,导致在它前面也必须加上await。
如果确实希望多个请求并发执行,可以使用Promise.all方法。
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
④、async 函数可以保留运行堆栈。
const a = () => {
b().then(() => c());
};
上面代码中,函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()或c()报错,错误堆栈将不包括a()。
现在将这个例子改成async函数。
const a = async () => {
await b();
c();
};
上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c()报错,错误堆栈将包括a()。
2、async 函数的实现原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。async 函数是 Generator 函数的语法糖。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。
下面给出spawn函数的实现,基本就是前文自动执行器的翻版。
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); });
});
}
3、async 函数与 Promise 对象、Generator 函数的比较
async 函数是 Generator 函数的语法糖:async函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。
async 函数对 Generator 函数的改进,体现在以下四点:
- 内置执行器:Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数可以像普通函数那样直接简单的使用。
- 更好的语义:async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
- 更广的适用性:co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
- 返回值是 Promise:async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。可以用then方法指定下一步的操作。进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
举个栗子:假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。
公共代码部分:
function f1(elem){
let nowTarget, speed = 5, timer;
clearInterval(timer);
timer = setInterval(()=>{
nowTarget = parseInt(getComputedStyle(elem)["left"]);// 初始值
if(nowTarget >= 300){
clearInterval(timer);
return false;
}else{
console.log("f1");
elem.style.left = nowTarget + speed + "px";
}
}, 50);
}
function f2(elem){
let nowTarget, speed = 5, timer;
clearInterval(timer);
timer = setInterval(()=>{
nowTarget = parseInt(getComputedStyle(elem)["top"]);// 初始值
if(nowTarget >= 200){
clearInterval(timer);
return false;
}else{
console.log("f2");
elem.style.top = nowTarget + speed + "px";
}
}, 50);
}
function f3(elem){
throw new Error("这是一个错误");
}
①、Promise 的写法
var arr = [f1, f2, f3];
var div = document.getElementById("myDiv");
chainAnimationsPromise(div, arr);
function chainAnimationsPromise(elem, animations) {
// 变量ret用来保存上一个动画的返回值
let ret = null;
// 新建一个空的Promise
let p = Promise.resolve();
// 使用then方法,添加所有动画
for(let anim of animations) {
p = p.then(function(val) {
console.log(val);
ret = val;
return anim(elem);
});
}
// 返回一个部署了错误捕捉机制的Promise
return p.catch(function(e) {
console.log(e);
/* 忽略错误,继续执行 */
}).then(function() {
return ret;
});
}
虽然 Promise 的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是 Promise 的 API(then、catch等等),操作本身的语义反而不容易看出来。
②、Generator 函数的写法
var arr = [f1, f2, f3];
var div = document.getElementById("myDiv");
chainAnimationsGenerator(div, arr);
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
let ret = null;
try {
for(let anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
console.log(e);
}
return ret;
});
}
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); });
});
}
上面代码使用 Generator 函数遍历了每个动画,语义比 Promise 写法更清晰,用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于,必须有一个任务运行器,自动执行 Generator 函数,上面代码的spawn函数就是自动执行器,它返回一个 Promise 对象,而且必须保证yield语句后面的表达式,必须返回一个 Promise。
③、async 函数的写法
var arr = [f1, f2, f3];
var div = document.getElementById("myDiv");
chainAnimationsAsync(div, arr);
async function chainAnimationsAsync(elem, animations) {
let ret = null;
try {
for(let anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
console.log(e);
}
return ret;
}
可以看到 Async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将 Generator 写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。如果使用 Generator 写法,自动执行器需要用户自己提供。
4、顶层 await
顶层的await命令有点像,交出代码的执行权给其他的模块加载,等异步操作完成后,再拿回执行权,继续向下执行。
四、使用实记
1、fetch 与 Promise 的使用案例
post:
fetch(`${BASE_URL}/all/api/one`, {
method: 'POST',
mode: 'cors',
credentials: 'include',
body: JSON.stringify(value)
})
.then(res => res.json())
.then(res => {
console.log(res.data);
})
.catch((e)=>{
message.error(e)
})
fetch:
const getMyList = () => {
const url = "/my/api/list";
fetch(url, {
method: "GET",
mode: "cors",
credentials: "include",
})
.then(res => res.json())
.then(res => {
console.log(res.data);
})
.catch((e)=>{
message.error(e)
})
};
简单的 fetch,本身就很简洁,没必要改成 async-await,否则反而显得不那么优雅了。
2、async-await 与 Promise 的使用案例
在react-redux中,我们想在处理异步接口数据时返回一个Promise对象,怎么办呢?
export const _dispatch = (dispatch, action) => {
const { payload } = action;
if (typeof payload !== "object") {
console.log('_dispatch的参数类型错误:参数必须是一个对象');
}
return new Promise((resolve, reject) => {
dispatch({
type: action.type,
payload: {
...action.payload,
},
callback: {
resolve,
reject,
},
});
});
};
使用:
const Test = () => {
const {
getOneData,
getTwoData,
pageModel,
} = props;
const { oneList, twoList } = pageModel;
useEffect(() => {
getOneList();
getTwoData({key: "two"});
}, [])
const getOneList = () => {
getOneData({key: "one_my"});
getOneData({key: "one_all"});
}
return (
<div>
{oneList.length ? (
<>
{oneList.map((item, idx) => {
<div key={idx}>
{item}
</div>
})}
</>
) : ("")}
{twoList.length ? (
<>
{twoList.map((item, idx) => {
<div key={idx}>
{item}
</div>
})}
</>
) : ("")}
</div>
)
const mapStateToProps = (state) => ({
pageModel: state.pageModel.toJS(),
});
const mapDispatchToProps = (dispatch) => {
return {
getOneData (payload) => {
return _dispatch (dispatch, {
type: "ONE_LIST",
payload,
})
},
getTwoData (payload) => {
dispatch({
type: "TWO_LIST",
payload,
})
}
}
}
以上述代码为例,说明一下:在 mapDispatchToProps 中使用 _dispatch 和 dispatch 的区别:
- _dispatch:在需要返回一个 callback 时使用,传入的参数必须是一个对象,返回的是一个 Promise 对象。
- dispatch:在不需要返回一个 callback 时使用,传入任意类型参数,返回任意类型参数。
为上述案例Test添加loading状态:
const Test = () => {
const {
getOneData,
getTwoData,
pageModel,
} = props;
const { oneList, twoList } = pageModel;
const [loading, setLoading] = useState(false);
useEffect(() => {
getOneList();
getTwoData({key: "two"});
}, [])
const getOneList = () => {
setLoading(true);
getOneData({key: "one_my"})
.then(() => getOneData({key: "one_all"})
.then(() => {
setLoading(false);
}));
}
return (
<div>
{loading ? (
<>
{oneList.length ? (
<>
{oneList.map((item, idx) => {
<div key={idx}>
{item}
</div>
})}
</>
) : ("")}
{twoList.length ? (
<>
{twoList.map((item, idx) => {
<div key={idx}>
{item}
</div>
})}
</>
) : ("")}
</>
) : (
没有数据
)
</div>
)
const mapStateToProps = (state) => ({
pageModel: state.pageModel.toJS(),
});
const mapDispatchToProps = (dispatch) => {
return {
getOneData (payload) => {
return _dispatch (dispatch, {
type: "ONE_LIST",
payload,
})
},
getTwoData (payload) => {
dispatch({
type: "TWO_LIST",
payload,
})
}
}
}
上述代码中的getOneList方法可以进一步优化:
const getOneList = async() => {
await setLoading(true);
await getOneData({key: "one_my"});
await getOneData({key: "one_all"});
setLoading(false);
}
什么时候可以使用 async-await ?当知道返回值是 Promise 对象的时候。
为什么不使用 then,而使用 async-await ?两个原因:
一是:then 的结构是嵌套的,没有 async-await 简洁。
二是:上述代码中,setLoading 也是异步的,使用 then 时,若要将 setLoading 也同步化执行,需要为其创建Promise对象(案例中没有实现这一步),比较麻烦;而使用 async-await,只需在其前面加上 await 关键字,就可以实现其同步化执行。
五、踩坑记
Promise、Generator 和 async 踩坑记:Promise、Generator 和 async 踩坑记_weixin79893765432...的博客-CSDN博客
推荐链接:
异步Promise及Async/Await可能最完整入门攻略
本文参考文档: