1. Promise A+规范的基本概念
Promise是一套专门处理异步场景的规范,它能有效的避免回调地狱的产生,使异步代码更加清晰、简洁、统一
这套规范最早诞生于前端社区,规范名称为Promise A+
该规范出现后,立即得到了很多开发者的响应
Promise A+ 规定:
-
所有的异步场景,都可以看作是一个异步任务,每个异步任务,在JS中应该表现为一个对象 ,该对象称之为Promise对象,也叫做任务对象
-
每个任务对象,都应该有两个阶段、三个状态
根据常理,它们之间存在以下逻辑:
- 任务总是从未决阶段变到已决阶段,无法逆行
- 任务总是从挂起状态变到完成或失败状态,无法逆行
- 时间不能倒流,历史不可改写,任务一旦完成或失败,状态就固定下来,永远无法改变
-
挂起->完成
,称之为resolve
;挂起->失败
称之为reject
。任务完成时,可能有一个相关数据;任务失败时,可能有一个失败原因。 -
可以针对任务进行后续处理,针对完成状态的后续处理称之为onFulfilled,针对失败的后续处理称之为onRejected
2. 创建Promise
js
// 创建一个任务对象,该任务立即进入 pending 状态
const pro = new Promise((resolve, reject) => {
// 任务的具体执行流程,该函数会立即被执行
// 调用 resolve(data),可将任务变为 fulfilled 状态, data 为需要传递的相关数据
// 调用 reject(reason),可将任务变为 rejected 状态,reason 为需要传递的失败原因
});
pro.then(
(data) => {
// onFulfilled 函数,当任务完成后,会自动运行该函数,data为任务完成的相关数据
},
(reason) => {
// onRejected 函数,当任务失败后,会自动运行该函数,reason为任务失败的相关原因
}
);
3. 针对Promise进行后续处理
下面任务的最终状态是什么,相关数据或失败原因是什么,最终输出是什么?
js
const pro1 = new Promise((resolve, reject) => {
console.log('开始任务');
resolve(1);
reject(2);
resolve(3);
console.log('结束任务');
})
console.log(pro1);
const pro2 = new Promise((resolve, reject) => {
console.log('开始任务');
resolve(1);
resolve(2);
console.log('结束任务');
})
console.log(pro2);
- 这里有一点需要注意,promise的状态一旦确定下来之后,是不会发生变化的,但后续的代码还会继续执行。
4.Promise链式调用
-
then方法必须会返回一个新的promise, 可以理解为 后续处理也是一个任务
-
新任务的状态取决于后续处理:
- 若没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的状态。
- 若有后续处理但未执行,新任务挂起。
- 若后续处理执行了,则根据后续处理的情况确定新任务的状态。
- 后续处理执行无错,新任务的状态未完成,数据为后续处理的返回值。
- 后续处理执行有错,新任务的状态为失败,数据为异常数据。
- 后续执行后返回时一个任务对象,新任务的状态和数据与该任务对象一致。
为了更直观的练习链式调用,来看几个题目:
js
// 下面代码的输出结果是什么
const pro1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const pro2 = pro1.then((data) => {
console.log(data);
return data + 1;
});
const pro3 = pro2.then((data) => {
console.log(data);
});
console.log(pro1, pro2, pro3);
setTimeout(() => {
console.log(pro1, pro2, pro3);
}, 2000);
- 结果是先输出三个padding状态的promise, 然后再打印 1 2 undefined,为啥?
- 首先来看第一个起始任务pro1, 等待1秒之后成功,pro1处于padding状态,接着产生了第二个任务pro2,pro2是pro1的后续处理,所以毫无疑问也是padding状态,里面的任务先不会执行,同样的pro3也是padding挂起状态。
- 接着继续执行pro1里面的代码,1秒钟到了后,pro1变成了fulfilled 数据是1;继续执行pro2里面的代码,打印了pro1传递过来的1,pro2变成了fulfilled data+1 返回一个2;pro3打印pro2传递过来的2,pro3状态变成fufilled, 没有返回结果,所以打印undefined;
再来一道,稍微改一下,把pro2的then改成catch:
js
const pro1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const pro2 = pro1.catch((data) => {
console.log(data);
return data + 1;
});
const pro3 = pro2.then((data) => {
console.log(data);
});
console.log(pro1, pro2, pro3);
setTimeout(() => {
console.log(pro1, pro2, pro3);
}, 2000);
- 结果是先输出三个padding状态的promise, 然后再打印 1 1 undefined
- 起始任务pro1, 等待1秒之后成功,pro1处于padding状态,打印fulfilled 1;接着产生了第二个任务pro2,但是这里pro2并没有对pro1的成功做处理,而是用catch, pro2的跟pro1一样处于padding状态,打印fulfilled 1;pro3针对pro2的成功做了处理,输出完成的数据1,pro3的运行状态取决于pro2的运行结果,运行过程没有报错,padding状态,并没有返回结果,打印fulfilled undefined。
- 最后打印 3个padding 1 fufilled 1 fufilled 1 fufilled undefined
再来改一下,给pro2抛一个错
js
const pro1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const pro2 = pro1.then((data) => {
throw 3;
return data + 1;
});
const pro3 = pro2.then((data) => {
console.log(data);
});
console.log(pro1, pro2, pro3);
setTimeout(() => {
console.log(pro1, pro2, pro3);
}, 2000);
- 默认pro1、pro2、pro3 都是pendding状态,pro1打印fufilled 1;运行pro2的过程中报了一个错,错误对象是3,pro2处于 rejected状态,下面的return不会运行,因为前面报错了;pro3 没有针对pro2的错误进行处理,pro3的状态跟pro2完全一致,rejected 3.
- 最终输出 3个padding, fufilled 1 reject 3 reject 3
继续下一道题目:
js
const pro = new Promise((resolve, reject) => {
resolve(1)
}).then((res) => {
console.log(res);
return 2;
}).catch((err) => {
return 3;
}).then((res) => {
console.log(res);
})
cnosole.log(pro)
- 你可能会以为是 1 3 undefined,为啥不是呢?
- 首先一步一步来分析,第一个promise处于padding resolve一个1,在第二个promise打印,输出 fulfilled 1; 第三个promise只处理了失败的结果,并且return 3,问题就在这里,你可能以为会把这个3给到第四个 promise,结果确实由于第三个promise没有对第二个promise成功的状态进行处理,所以延续了第二个promise的fulfilled 2的结果;第4个promise的结果取决于上一个promise,第三个promise成功了,并且延续的第二个的promise,所以在第四个promise打印的时候还是2,并不是3,最后第4个promise没有结果可返回,打印了一个undefined。
再稍微改造一下:
js
const pro = new Promise((resolve, reject) => {
resolve()
}).then((res) => {
console.log(res.toString());
return 2;
}).catch((err) => {
return 3;
}).then((res) => {
console.log(res);
})
console.log(pro)
- 第一个promise 这里简称pro1, pro1 resolve一个空,所以是fuilled undefined;pro2 将pro1延续的undefined进行toString(), 所以肯定报错,pro2变成了rejected状态;因为pro2报错,pro3的状态取决于pro2的处理,pro3捕获到了异常,并成功return了一个3,运行状态为fulfilled 返回一个3;pro4运行过程中没有错误,运行状态为 fufilled 打印pro3延续的结果3,pro4没有返回值,最后打印undefined
继续下一道:
js
new Promise((resolve, reject) => {
resolve(1)
})
.then((res) => {
console.log(res);
return new Error('2');
})
.catch((err) => {
throw err;
return 3;
})
.then((res) => {
console.log(res);
});
- 首先第一个pro1,resolve了一个1,fulfilled 1;pro2打印1,抛出异常,fulfilled error('2'); pro3捕获到了异常,同时抛出异常,后面的return 3 不会执行,fulfilled error('2');pro4 打印pro3的错误,fulfilled undefined;
js
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject();
}, 1000);
});
const promise2 = promise1.catch(() => {
return 2;
});
console.log('promise1', promise1);
console.log('promise2', promise2);
setTimeout(() => {
console.log('promise1', promise1);
console.log('promise2', promise2);
}, 2000);
- 一开始打印的两个promise 都处于padding挂起状态, 1秒后 promise1被拒绝了,rejected undefined;promise2对promise1的错误进行处理,并返回了一个2,promise2成功运行 fulfilled 2;
换一个类型的题目:
js
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log(2);
resolve();
console.log(3);
});
}).then(() => {
console.log(4);
})
console.log(5);
- 运行第一个promise,输出1,setTimeout没有传第二个参数,你可能会以为会立即执行,但是因为setTimeout属于宏任务,就算没有传递第二个参数,被分配到宏任务队列也是处于等待执行状态,因为resolve在setTimeout里面,整个promise没有改变状态,依旧还是padding状态;
- 第二个promise的then需要等待成功之后才会执行,但是因为上一个promise未成功,第二个promise不会进入微任务队列,所以不会执行;
- 接着打印全局作用域的5,全局作用域的任务执行完了,就会把宏任务队列的setTimeout进行执行,打印2,返回第一个promise的resolve状态,第二个promise进入微任务队列,第一个promise继续打印3,第一个promise执行完了,轮到第二个promise执行,打印4;最终结果1 5 2 3 4
js
setTimeout(() => {
console.log(1);
});
const promise = new Promise((resolve, reject) => {
console.log(2);
resolve();
}).then(() => {
console.log(3);
})
console.log(4);
- 首先登场的是一个setTimeout,加入到宏队列;其次是一个promise,输出一个2,resolve将状态改为fulfilled;
- .then开启了第二个promise,进入微任务队列,这个时候不会立即执行微任务队列,因为全局作用域还有代码没执行,全局作用域打印一个4
- 接着执行微任务队列,打印3,最后执行宏任务,打印1
来个简单一点的:
js
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject();
}, 1000);
});
const promise2 = promise1.catch(() => {
return 2;
});
console.log('promise1', promise1);
console.log('promise2', promise2);
setTimeout(() => {
console.log('promise1', promise1);
console.log('promise2', promise2);
}, 2000);
- 有了前面的积累,充满的一肯定一眼就能看出输出啥了,没错,首先是两个padding,2s后打印undefined 2
5. async await 消除回调
- async 关键字用于修饰函数,一般出现在函数前面,一定返回一个promise;
- await 关键字表示等待某个Promise完成,它必须用于async函数中;
js
async function m(){
console.log(0);
const n = await 1;
console.log(n);
}
m();
console.log(2);
- 为啥是 0 2 1,脑瓜子是不是嗡嗡的,听我细细道来,别被语法糖迷惑住了,async 后面的一定返回一个promise,m函数里面首先打印一个0;
- 而
const n = await 1;console.log(n)
相当于Promise.resolve(1).then((e) => { console.log(n); })
, 给n赋值和输出n这段逻辑本质上是在微队列,打印完0之后不会卡住等待await执行,而是结束掉m函数,执行全局作用域的代码,打印2;最后执行微队列的代码,打印1,有意思吧;
再来一道:
js
async function m() {
console.log(0);
const n = await 1;
console.log(n);
}
(async () => {
await m();
console.log(2);
})();
console.log(3);
- 看完不自信了吧,为啥?看了这么多题,还是会错?别急,听我一步一步分析
- 首先看括号里面的立即执行函数,立即执行函数里面await调用m()函数,打印0,后面的await进入微队列等待,那么立即执行函数里面的await返回的promise也是处于padding状态,下面的代码无法继续执行;
- 回到全局作用域,打印3,执行栈清空,开始执行微队列,打印1,m()函数执行完,可以执行立即执行函数了,打印2;最后打印 0 3 1 2
继续下一道:
js
async function m1() {
return 1;
}
async function m2() {
const n = await m1();
console.log(n);
return 2;
}
async function m3() {
const n = m2();
console.log(n);
return 3;
}
m3().then((n) => {
console.log(n);
});
m3();
console.log(4);
- 嘿,朋友,还好吗?别自闭了,这个确实有点绕,也不知道谁脑洞这么大出的这题,如果你有此困惑,说明还没彻底掌握,坚持住,听我逐一分析。
- 首先直接看m3.then(), m3返回一个promise, m3里面调用m2,m2里面调用m1,m1里面 return 了一个1,状态为fulfilled 1;
- m2的
await m1()
相当于await 1
,这个时候打印n,后续代码进入微队列,状态为padding。 - m3
const n = m2()
注意这里没有使用 await,打印n,返回3,状态为fulfilled 3; - m3执行完了,调用 .then 函数,.then里面的n是m3 返回的 3,所以打印3;
- 接着下面又调用了一次m3,继续m3调用m2,m2调用m1,m1返回1,状态为fufilled 1;m2 的await 进入微队列,后续代码进入微队列,状态为padding;m3没有等待m2,直接完成,返回3;
- 最后输出全局作用域的4,微队列的一个个拿出来,先输出 1,接着是3 1,最后打印 4 1 3 1
累了没?那来一道看起来比较简单的开火车题目:
js
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log);
- 你个老六,说好的比较简单,怎么火车上面还有雷,2和3去哪了,被炸了吗?
- 别慌,事出反常必有妖,且听我慢慢分析,事情还得从
.then(2)
开始,注意,这里不是一个函数,而是一个2,这不合常理,你只要记住一点,以后看到then里面传的不是函数,直接忽略这段代码,为什么?因为promise的then发现你传递的不是一个promise,也就是没有注册回调函数,那它的结果和状态和上一个promise是一致的,相当于.then(null)
; - 继续看
.then(Promise.resolve(3))
,.then里面传递的是一个promise对象,依旧不是函数,所以是无效的传递,也相当于.then(null)
; - 最后一个不用解释了,
.then(console.log)
的console.log
是一个函数,打印undefined。 - 奇怪的知识又增加了吧,记得关注我,我会提供更多查漏补缺的知识,来刷新你的认识。
来一个比较经典的问题,你可能会比较熟悉:
js
var a;
var b = new Promise((resolve, reject) => {
console.log('promise1');
setTimeout(() => {
resolve();
}, 1000);
})
.then(() => {
console.log('promise2');
})
.then(() => {
console.log('promise3');
})
.then(() => {
console.log('promise4');
});
a = new Promise(async (resolve, reject) => {
console.log(a);
await b;
console.log(a);
console.log('after1');
await a;
resolve(true);
console.log('after2');
});
console.log('end');
- 这个题有点复杂,但是别乱了阵脚,还是逐一分析。
- 首先定义一个a,值为undefined,然后定义一个b,你可能会以为直接是一个promise,但在js里面变量的声明和赋值是有先后顺序的,出现b这种,得先把promise算出来,才能赋值给b,在没算出来之前,都是undefined。
- 来看b的表达式,开了一个火车,从第一个promise开始,resolve放在一个定时器里面,也就是1s之后才能确定后面promise的状态,这里有个小知识,不管promise的火车有多长,.then接的有多少个,返回的都是最后一个 .then ,所以b是一个 padding状态的promise,打印 'promise1'。
- 继续看下面代码,创建了一个新的promise赋值给a,new Promise 表达式里面首先打印a,因为a还没有成功赋值,所以打印上面的undefined;
- 接着看
await b
等待上面的代码,因此后面await b
后面的代码不会执行,进入微队列,因此整个赋值给b的promise执行解释,状态为padding,赋值给a; - 执行打印全局作用域的 end ,等待1s后,定时器结束,赋值给b的promis状态为fufilled,执行后面的 .then 函数,开火车,依次打印 'promise2' 'promise3' 'promise4';
- b的promise赋值完成之后,微任务队列中
await b
后面的代码可以执行了,接着输出一个 'after1',又继续打印a,a这里前面执行过,为padding状态的promise; - 接着看
await a
, 自己等待padding状态的自己之后再执行下面的代码?这事肯定没尽头,所以后面的代码不会再执行了。
继续下一道,我怕再玩下去,你要忍不住到要打我了:
js
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('script end');
- 这是一道promise async 混合的题目,但是不要慌,其实都一样。
- 首先打印一个 'script start', 再看宏任务,有一个setTimeout,等待0秒,排队去,再看微任务,async1()被调用,打印 'async1 start',然后await 异步调用async2(),打印async2,返回一个promise,后续代码进入微队列;
- 继续回到外层的 new Promise,打印 'promise1',resolve完成,后面的.then进入微队列;
- 继续回到全局作用域的代码,打印 'script end',主线程执行完了,开始从微任务队列找代码执行,微任务首先打印async1里面等待的 'async1 end', async1这个promise状态完成,fufilled,然后再输出 new Promise 中挂起的 'promise2',微队列清空;
- 再看宏队列,打印 'setTimeout',结束。
最后一道,绝对颠覆你对finally的认知:
js
Promise.resolve('1')
.then(res => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2')
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res)
})
打印结果:
- 你可能会疑惑,为什么在执行第一个promise的时候,打印完1,没有继续执行finally,而是转而执行第二个promise的finally?
- 这个问题涉及到 JavaScript 中 Promise 的执行机制,特别是微任务(microtask)的调度顺序。让我为你详细解答,为什么在执行第一个 Promise 的
.then
并打印'1'
后,没有立即执行它的.finally
,而是转而执行第二个 Promise 的.finally
。
为了方便理解,先看看代码:
javascript
Promise.resolve('1')
.then(res => {
console.log(res) // 打印 '1'
})
.finally(() => {
console.log('finally') // 打印 'finally'
})
Promise.resolve('2')
.finally(() => {
console.log('finally2') // 打印 'finally2'
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res) // 打印 'finally2后面的then函数 2'
})
运行结果是:
csharp
1
finally2
finally
finally2后面的then函数 2
你好奇的是:为什么在打印 '1'
后,没有紧接着打印 'finally'
,而是先打印了 'finally2'
?
这与 JavaScript 的 微任务调度机制 有关。让我们一步步拆解:
1. Promise 的链式调用与微任务队列
- 在 JavaScript 中,Promise 的
.then
、.catch
和.finally
都会返回一个新的 Promise 对象。 - 当一个 Promise 完成(resolved 或 rejected)时,它注册的回调(如
.then
或.finally
中的函数)会被加入 微任务队列。 - 微任务队列是先进先出(FIFO)的,任务会按照加入队列的顺序依次执行。
2. 同步代码的执行
当代码运行时,同步部分会立即执行:
- 第一个 Promise :
Promise.resolve('1')
创建一个立即 resolved 的 Promise,值为'1'
。它的.then
回调(console.log(res)
)被加入微任务队列。 - 第二个 Promise :
Promise.resolve('2')
也创建一个立即 resolved 的 Promise,值为'2'
。它的.finally
回调(console.log('finally2')
)也被加入微任务队列。
同步代码执行完后,微任务队列的初始状态是:
- 队列:[
第一个 Promise 的 .then
,第二个 Promise 的 .finally
]
3. 微任务的逐步执行
JavaScript 的事件循环会在同步代码执行完后,清空微任务队列。让我们看看具体过程:
-
执行第一个任务:
- 从队列中取出
第一个 Promise 的 .then
,执行console.log('1')
,打印'1'
。 .then
执行后,返回一个新的 Promise(默认 resolved),并将.finally(() => { console.log('finally') })
加入微任务队列。- 此时队列变为:[
第二个 Promise 的 .finally
,第一个 Promise 的 .finally
]
- 从队列中取出
-
执行第二个任务:
- 队列中下一个任务是
第二个 Promise 的 .finally
,执行console.log('finally2')
,打印'finally2'
。 .finally
执行后,返回一个新的 Promise(值为原始的'2'
),并将.then(res => { console.log('finally2后面的then函数', res) })
加入微任务队列。- 队列变为:[
第一个 Promise 的 .finally
,第二个 Promise 的 .then
]
- 队列中下一个任务是
-
执行第三个任务:
- 执行
第一个 Promise 的 .finally
,打印'finally'
。 - 队列变为:[
第二个 Promise 的 .then
]
- 执行
-
执行第四个任务:
- 执行
第二个 Promise 的 .then
,打印'finally2后面的then函数 2'
。 - 队列清空,执行结束。
- 执行
4. 关键点解答
为什么在打印 '1'
后没有立即执行 'finally'
?原因在于:
- 当第一个 Promise 的
.then
执行时,它的.finally
回调并不是立即执行,而是被加入微任务队列的末尾。 - 此时,第二个 Promise 的
.finally
已经在队列中(因为它在同步代码执行时就加入了),而且排在第一个 Promise 的 .finally
前面。 - 微任务队列严格遵循先进先出的原则,因此在执行完
第一个 Promise 的 .then
后,会先执行队列中已有的第二个 Promise 的 .finally
(打印'finally2'
),然后才轮到第一个 Promise 的 .finally
(打印'finally'
)。
为了更直观地展示,以下是队列的演变过程:
-
初始状态(同步代码后):
- 队列:[
第一个 Promise 的 .then
,第二个 Promise 的 .finally
]
- 队列:[
-
执行 .then 后 (打印
'1'
):- 队列:[
第二个 Promise 的 .finally
,第一个 Promise 的 .finally
]
- 队列:[
-
执行第二个 .finally 后 (打印
'finally2'
):- 队列:[
第一个 Promise 的 .finally
,第二个 Promise 的 .then
]
- 队列:[
-
执行第一个 .finally 后 (打印
'finally'
):- 队列:[
第二个 Promise 的 .then
]
- 队列:[
-
执行第二个 .then 后 (打印
'finally2后面的then函数 2'
):- 队列:空
在执行第一个 Promise 的 .then
并打印 '1'
后,它的 .finally
没有立即执行,是因为 .finally
的回调是在 .then
执行后才加入微任务队列的。而第二个 Promise 的 .finally
在同步代码阶段就已加入队列,排在前面。因此,微任务调度机制决定先执行 finally2
,然后才执行第一个 Promise 的 finally
。这种顺序是由微任务队列的 FIFO 特性决定的,确保了异步任务的执行有条不紊。
总结
- 优先级:同步代码 > 微任务 > 宏任务;微任务队列必须清空后才会处理下一个宏任务。
- 状态不可逆:一旦从 Pending 变为其他状态,不能再改变;
- js代码永远不会卡住,后面有代码就会继续执行后面的;
- 任何一个
.then()
抛出错误,会直接跳到最近的.catch()
; - then里面如果传的不是函数,直接忽略这段代码,因为promise的then发现你传递的不是一个promise,也就是没有注册回调函数,那它的结果和状态和上一个promise是一致的,相当于
.then(null)
; .finally()
方法的回调函数不接受任何的参数,不管Promise对象最后的状态如何都会执行;- 微任务队列是先进先出(FIFO)的,任务会按照加入队列的顺序依次执行;
- .then、.catch 和 .finally 会返回一个新的 Promise 对象,事件循环会在同步代码执行完后,清空微任务队列。