ES6中 Promise出现的目的是解决Node.js异步编程中回调地狱的问题。Promise只是异步编程语法上的改进,并没有提供新的功能。可以让我们将异步API的执行和处理相分离。
Promise本身是一个构造函数,如果需要用Promise,首先需要new一个Promise的实例对象。
Promise构造函数接收一个匿名函数作为参数,这个匿名函数中又有两个参数,分别是resolve,和reject。
Promise想我们把原本的异步API代码放在这个匿名函数当中。比如说里面有一个定时器,定时器执行完之后会有 一个结果出来。Promise不想我们在定时器的回调函数内部处理结果,而是把结果拿到回调函数外面去处理。
resolve参数实际上是一个函数,当异步API有返回结果的时候,我们可以调用resolve这个函数,并且把异步API的结果通过参数的形式传递出去。
reject参数实际上也是一个函数,当异步API执行失败了,我们就可以调用reject这个函数把失败结果传递到Promise外面去。
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
resolve({name: '张三'})
}else {
reject('失败了')
}
}, 2000);
});
promise.then(result => console.log(result)) // {name: '张三'})
.catch(error => console.log(error)); // 失败了)
Promise实际上就是把原本的异步API外面去包裹一层,然后当原本异步API有返回结果的时候,它为我们提供两个方法,成功就调用resolve把结果传递到Promise整体的外面去,失败就调用reject把结果传递到外面去。
我们可以通过创建的Promise的实例对象拿到。
promise里面有一个then方法,这个方法接收一个函数作为参数。我们之前在调用resolve方法的时候实际上就是在调用这个函数。这个函数也称为then方法的回调函数。
promise里面还有一个catch方法,这个方法也接收一个函数作为参数。我们之前在调用reject方法的时候实际上就是在调用这个函数。这个函数也称为catch方法的回调函数。
promise允许我们链式编程,所以在 .then 方法之后可以继续 .catch 调用方法
比如先在有一个需求:读取1.txt文件后再读取2.txt文件,最后读取3.txt文件(不能够是同时读取1、2、3文件)
我们要将原本读取文件的异步API分别放到一个Promise里面,然后把这个promise用函数封装起来。当我们要读取文件的时候就调用这个函数就可以,不调用函数就不会读取文件。
const fs = require('fs');
function p1 () {
return new Promise ((resolve, reject) => {
fs.readFile('./1.txt', 'utf8', (err, result) => {
resolve(result)
})
});
}
function p2 () {
return new Promise ((resolve, reject) => {
fs.readFile('./2.txt', 'utf8', (err, result) => {
resolve(result)
})
});
}
function p3 () {
return new Promise ((resolve, reject) => {
fs.readFile('./3.txt', 'utf8', (err, result) => {
resolve(result)
})
});
}
p1().then((r1)=> {
console.log(r1);
return p2();
})
.then((r2)=> {
console.log(r2);
return p3();
})
.then((r3) => {
console.log(r3)
})
在当前的then里面return一个promise对象,那么下一个then拿到的结果就是上一个then里面return的promise对象结果。
( 也就是原本链式操作一直返回的都是当前的promise,而我们在then里面return另一个promise,链式的下一个方法就基于return的那个promise来 )
Promise的缺点就是不能取消。
axios可以取消是因为它有cancelToken机制
Promise语法还是比较繁琐的,我们需要在Promise里面写异步API,然后手动调用方法将结果传递出去,接着还要链式使用 .then 来处理结果。这样代码看起来就比较臃肿。
我们需要更好的方式,既可以解决回调地狱又可以使代码看着清晰明了。
→ 引申出ES7中的异步函数语法 →
实际上这个异步函数就是基于Promise对象进行一层封装,它将一些我们看起来比较臃肿的代码封装起来,然后开放一些关键字供我们使用。
异步函数是异步编程语法的终极解决方案,它可以让我们将异步代码写成同步的形式,让代码不再有回调函数嵌套,使代码变得清晰明了。
所谓的异步函数就是在普通函数前面加上async关键字。
异步函数默认的返回值是promise对象,不是undefined
//语法:
const fn = async () => {};
//或者
async function fn () {}
/*例子------*/
async function fn () {
throw '发生了一些错误';
return 123;
}
// console.log(fn ())
fn ().then(function (data) {
console.log(data);
}).catch(function (err){
console.log(err);
})
async关键字里面还有另外一个神秘人物 它就是 await关键字
await关键字使用:我们继续上面依次读取三个文件的例子
我们先将之前的创建Promise对象改写成使用async关键字的写法(这里暂时用一个字符串代替原来的读取文件),然后再写多一个async异步函数来调用上面这三个读取文件的函数。
async function p1 () {
return 'p1';
}
async function p2 () {
return 'p2';
}
async function p3 () {
return 'p3';
}
async function run () {
// p1();p2();p3(); 这种写法就是同时调用这三个函数,所以我们要的不是这种方式,需要在他们的前面都加上await关键字
// p1()返回的是promise对象,正常来说我们是需要调用then来处理结果,也就是 p1().then()
//但是在异步函数内部可以不这样做,我们可以在promise对象前面加上await关键字,await关键字就会暂停当前函数的执行
// await p1() 也就是说当前这个p1没有返回结果,是不会向下执行的;还有一个特性就是我们可以通过返回值的形式拿到p1执行的结果
// await p2()
// await p3()
let r1 = await p1()
let r2 = await p2()
let r3 = await p3()
console.log(r1)
console.log(r2)
console.log(r3)
}
run();
接下来,我们将原来的返回字符串改写成真正的读取文件。但是,我们会发现 fs.readFile()方法是在回调函数里面获取返回结果,它不返回一个promise对象,那么我们就不能够在它前面加await关键字。另外它都不是直接返回结果,用async关键字也是得不到结果啊。也就是异步函数没法用了?还是要原来的new一个Promise那样??
于是Node.js为我们提供了一个方法,这个方法可以帮助我们对现有的异步API进行包装,让异步API返回一个promise对象,以支持异步函数语法。Node.js为我们提供的这个方法就叫做 promisify,它存放在 until 模块当中,所以我们使用promisify方法之前需要引入 until 模块
promisify方法
我们将需要改造的异步API作为参数给promisify方法进行处理,注意是传递异步API,不是调用异步API,所以作为promisify的参数时不要加小括号
promisify方法的返回值是一个改造好的新方法。
const fs = require('fs');
// 改造现有异步函数api 让其返回promise对象 从而支持异步函数语法
const promisify = require('util').promisify;
// 调用promisify方法改造现有异步API 让其返回promise对象
const readFile = promisify(fs.readFile);
async function run () {
let r1 = await readFile('./1.txt', 'utf8')
let r2 = await readFile('./2.txt', 'utf8')
let r3 = await readFile('./3.txt', 'utf8')
console.log(r1)
console.log(r2)
console.log(r3)
}
run();
在node.js中,异步API的错误信息都是通过回调函数获取的,支持Promise对象的异步API发生错误可以通过catch方法捕获。
异步函数执行如果发生错误要如何捕获错误呢?
try catch 可以捕获异步函数以及其他同步代码在执行过程中发生的错误,但是不能其他类型的API发生的错误。
/* 这是在一个服务器端的app.js文件中截取出来的片段 */
app.get("/", async (req, res, next) => {
try {
await User.find({name: '张三'})
}catch(ex) {
next(ex);
}
});