图片来源https://objcer.com/2017/05/07/About-Promise/
Promise是前端目前不论面试还是工作中都应该十分了解的知识,本文希望全面并且简单的能够把promise的概念、使用、原理讲清楚,所以分了为了三篇。
这第一篇使用基本就是一些我自己的概念理解,以及许多部分搬运阮一峰大大的文章,将一部分最核心的篇幅,重新学习,并且记录了一下。
一、什么是promise
文档解读:
1 | Promises/A+ 规范中表示为一个异步操作的最终结果,ECMAscript 规范定义为延时或异步计算最终结果的占位符。 |
白话解读:
- Promise是异步编程的一种解决方案
- Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果
- 从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
二、Promise使用
1. 基本用法
(1) 创造了一个Promise实例
1 | const promise = new Promise(function(resolve, reject) { |
(2) 指定回调函数
1 | promise.then(function(value) { |
then方法可以接受两个回调函数作为参数。
[1]第一个回调函数是Promise对象的状态变为resolved时调用,
[2]第二个回调函数是Promise对象的状态变为rejected时调用。
[3]其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。
[4]Promise 新建后就会立即执行。
1 | let promise = new Promise(function(resolve, reject) { |
[5]调用resolve或reject并不会终结 Promise 的参数函数的执行。
1 | new Promise((resolve, reject) => { |
上面代码中,调用resolve(1)以后,后面的console.log(2)还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
一般来说,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。
1 | new Promise((resolve, reject) => { |
(3) promise嵌套
果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。
1 | const p1 = new Promise(function (resolve, reject) { |
这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。
2. api讲解
(1)Promise.prototype.then()
它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
1 | getJSON("/posts.json").then(function(json) { |
(2)Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。
1 | getJSON('/posts.json').then(function(posts) { |
如果 Promise 状态已经变成resolved,再抛出错误是无效的。
1 | const promise = new Promise(function(resolve, reject) { |
(3)Promise.prototype.finally()
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
1 | promise |
上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。
1 | promise |
实现:
1 | Promise.prototype.finally = function (callback) { |
从上面的实现还可以看到,finally方法总是会返回原来的值。
1 | // resolve 的值是 undefined |
(4)Promise.all()
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
1 | const p = Promise.all([p1, p2, p3]); |
p1、p2、p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
p的状态由p1、p2、p3决定,分成两种情况。
[1]只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
[2]只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
(5)Promise.race()
Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
1 | const p = Promise.race([p1, p2, p3]); |
(6)Promise.resolve();
Promise.resolve方法的参数分成四种情况。
[1]参数是一个 Promise 实例
如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
[2]参数是一个thenable对象
thenable对象指的是具有then方法的对象,比如下面这个对象。
1 | let thenable = { |
Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。
1 | let thenable = { |
[3]参数不是具有then方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。
1 | const p = Promise.resolve('Hello'); |
上面代码生成一个新的 Promise 对象的实例p。由于字符串Hello不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。Promise.resolve方法的参数,会同时传给回调函数。
[4]不带有任何参数
1 | const p = Promise.resolve(); |
上面代码的变量p就是一个 Promise 对象。
需要注意的是,立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
1 | setTimeout(function () { |
上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log(‘one’)则是立即执行,因此最先输出。
(7)Promise.reject()
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
1 | const p = Promise.reject('出错了'); |
上面代码生成一个 Promise 对象的实例p,状态为rejected,回调函数会立即执行。
注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。
(8)Promise.try()
[1] 无法或不想区分,数f是同步函数还是异步操作
1 | Promise.resolve().then(f) |
[2] 让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API
(1) async函数来写
1 | const f = () => console.log('now'); |
上面代码中,第二行是一个立即执行的匿名函数,会立即执行里面的async函数,因此如果f是同步的,就会得到同步的结果;如果f是异步的,就可以用then指定下一步,就像下面的写法。
1 | (async () => f())() |
需要注意的是,async () => f()会吃掉f()抛出的错误。所以,如果想捕获错误,要使用promise.catch方法。
1 | (async () => f())() |
(2) 第二种写法是使用new Promise()
1 | const f = () => console.log('now'); |
上面代码也是使用立即执行的匿名函数,执行new Promise()。这种情况下,同步函数也是同步执行的。
(3) Promise.try
事实上,Promise.try存在已久,Promise 库Bluebird、Q和when,早就提供了这个方法。
由于Promise.try为所有操作提供了统一的处理机制,所以如果想用then方法管理流程,最好都用Promise.try包装一下。这样有许多好处,其中一点就是可以更好地管理异常。
1 | try { |
上面这样的写法就很笨拙了,这时就可以统一用promise.catch()捕获所有同步和异步的错误。
1 | Promise.try(database.users.get({id: userId})) |
事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。