Promise简介
Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。
约定
不同于“老式”的传入回调,在使用 Promise 时,会有以下约定:
- 在本轮事件循环运行完成之前,回调函数是不会被调用的。
- 即使异步操作已经完成(成功或失败),在这之后通过 then()添加的回调函数也会被调用。
- 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序进行执行。
Promise 很棒的一点就是链式调用(chaining)。
Promise原理分析
Promise内部有三个状态:pending,fullfilled和rejected。
pending是对象创建后的初始状态,当对象fulfill(成功)时变为fulfilled,当对象reject(失败)时变为rejected。且只能从pengding变为fulfilled或rejected ,而不能逆向或从fulfilled变为rejected 、从rejected变为fulfilled。如图所示:
Promise实例方法介绍
Promise对象拥有两个实例方法then()和catch()。
then()方法
成功和失败的回调函数我们是通过then()添加,在promise状态改变时分别调用。promise构造函数中通常都是异步的,所以then方法往往都先于resolve和reject方法执行。所以promise内部需要有一个存储fulfill时调用函数的数组和一个存储reject时调用函数的数组。
then方法可以接收两个参数,且通常都是函数(非函数时如何处理下一篇文章中会详细介绍)。
第一个参数会添加到fulfill时调用的数组中,第二个参数添加到reject时调用的数组中。
当promise状态fulfill时,会把resolve(value)中的value值传给调用的函数中。
var p = new Promise(function(resolve, reject){
resolve(5)
}).then(function(value){
console.log(value) //5
})
同理,当promise状态reject时,会把reject(reason)中的reason值传给调用的函数。
var p1 = new Promise(function(resolve, reject){
reject(new Error('错误'))
}).then(function(value){
console.log(value)
}, function(reason){
console.log(reason) //Error: 错误(…)
})
then方法会返回一个新的promise,下面的例子中p == p1将返回false,说明p1是一个全新的对象。
var p = new Promise(function(resolve, reject){
resolve(5)
})
var p1 = p.then(function(value){
console.log(value)
})
p == p1 // false
这也是为什么then是可以链式调用的,它是在新的对象上添加成功或失败的回调,这与jQuery中的链式调用不同。
then() 函数会返回一个和原来不同的新的 Promise:
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);
promise2 不仅表示 doSomething() 函数的完成,也代表了你传入的 successCallback 或者 failureCallback 的完成,这两个函数也可以返回一个 Promise 对象,从而形成另一个异步操作,这样的话,在 promise2 上新增的回调函数会排在这个 Promise 对象的后面。
基本上,每一个 Promise 都代表了链中另一个异步过程的完成。
我们可以把回调绑定到返回的 Promise 上,形成一个 Promise 链:
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
catch()方法
有可能会在一个回调失败之后继续使用链式操作,即,使用一个 catch,这对于在链式操作中抛出一个失败之后,再次进行新的操作会很有用。
Promise时序
为了避免意外,即使是一个已经变成 resolve 状态的 Promise,传递给 then() 的函数也总是会被异步调用:
Promise.resolve().then(() => console.log(2));
console.log(1);
//输出:
//1
//2
传递到 then() 中的函数被置入到一个微任务队列中,而不是立即执行,这意味着它是在 JavaScript 事件队列的所有运行时结束了,且事件队列被清空之后,才开始执行:
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1);
//输出:
//1
//2
//3
//4
那为什么会输出1 2 3 4 5呢?这就涉及到promise,then,以及setTimeout的执行顺序问题。
promise,then,setTimeout的执行顺序
1. setTimeout何时执行?
我们知道,JavaScript是基于事件驱动单线程执行的,所有任务都需要排队,也就是说前一个任务结束,才会去执行下一个任务。而像settimeout、ajax等异步操作的回调,会进入”任务队列“中,而且只有主线程中没有执行任何同步代码的前提下,才会执行异步回调。
2. promise何时执行?
Promise新建后立即执行,也就是说,Promise构造函数里的代码是同步执行的。
3. then何时执行?
then方法指向的回调将在当前脚本所有同步任务执行完后执行。
4. 为什么then比setTimeout执行的要早呢?
-
1) setTimeout的0是否真的为0?
其实,setTimeout有个最小执行时间(minimum delay of 4ms ),并不是0s执行的。
注:HTML5中已经将最小执行时间统一为4ms。
-
2) macrotask 与 microtask
Macrotasks和Microtasks 都属于异步任务中的一种,常用api分类: macrotasks: setTimeout,
setInterval, setImmediate, I/O, UI rendering microtasks:
process.nextTick, Promise, MutationObserver一个事件循环中只有一个macrotask任务,可以有一个或多个microtask任务。
再举一个例子:
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2)
for (var i = 0; i < 10000; i++) {
if(i === 10) {console.log(10)}
i == 9999 && resolve();
}
console.log(3)
}).then(function() {
console.log(4)
})
console.log(5);
//结果是:2 10 3 5 4 1
- 首先,setTimeout 被推进到 macrotask 队列(将在下一个macrotask中执行)中。
- 接着, 会先执行macrotask 中的第一个任务(整个 script中的同步代码 ),再加上promise 构造函数也是同步的(promise.then回调被推进到 microtask 队列中),所以会先打印出2 10 3,然后继续执行末尾的,打印出5
- 此时,已经执行完了第一个macrotask , 所以接下来会顺序执行所有的 microtask, 也就是 promise.then 的回调函数,从而打印出4。
- 此时,microtask 队列中的任务已经执行完毕,所以执行剩下的 macrotask 队列中的任务,也就是 setTimeout,所以打印出 1。