JS异步的发展历程是callback->promise/generator->async/await
1. Promise
1.1 带着问题看 Promise
- 了解 Promise 吗?
- Promise 解决的痛点是什么?
- Promise 解决的痛点还有其他方法可以解决吗?如果有,请列举。
- Promise 如何使用?
- Promise 常用的方法有哪些?它们的作用是什么?
- Promise 在事件循环中的执行过程是怎样的?
- Promise 的业界实现都有哪些?
- 能不能手写一个 Promise 的 polyfill。
1.2 Promise 出现的原因?
在 Promise 出现以前,我们处理一个异步网络请求,大概是这样:
// 请求 代表 一个异步网络调用。
// 请求结果 代表网络请求的响应。
请求1(function(请求结果1){
处理请求结果1
})
看起来还不错。
但是,需求变化了,我们需要根据第一个网络请求的结果,再去执行第二个网络请求,代码大概如下:
请求1(function(请求结果1){
请求2(function(请求结果2){
处理请求结果2
})
})
这样看起来就要复杂得多,于是当这种情况增加时,我们就会陷入回调地域。回调地域会让我们的代码非常难维护。
于是 Promise 规范诞生了,并且在业界有了很多实现来解决回调地狱的痛点。比如业界著名的 Q 和 bluebird,bluebird 甚至号称运行最快的类库。
看官们看到这里,对于上面的问题 2 和问题 7 ,心中是否有了答案呢。^_^
1.3 什么是 Promise
Promise 是异步编程的一种解决方案,比传统的异步解决方案【回调函数】和【事件】更合理、更强大。现已被 ES6 纳入进规范中。
(1) 代码书写比较
还是使用上面的网络请求例子,我们看下 Promise 的常规写法:
new Promise(请求1)
.then(请求2(请求结果1))
.then(请求3(请求结果2))
.then(请求4(请求结果3))
.then(请求5(请求结果4))
.catch(处理异常(异常信息))
比较一下这种写法和上面的回调式的写法。我们不难发现,Promise 的写法更为直观,并且能够在外层捕获异步函数的异常信息。
(2) API
Promise 的常用 API 如下:
- Promise.resolve(value)
类方法,不需要return,该方法返回一个以 value 值解析后的 Promise 对象
(1) 如果这个值是个 thenable(即带有 then 方法),返回的 Promise 对象会“跟随”这个 thenable 的对象,采用它的最终状态(指 resolved/rejected/pending/settled)。
(2) 传入Promise返回Promise本身。
(3) 其他情况以该值为成功状态返回一个 Promise 对象。
上面是 resolve 方法的解释,传入不同类型的 value 值,返回结果也有区别。这个 API 比较重要,建议大家通过练习一些小例子,并且配合上面的解释来熟悉它。如下几个小例子:
传入promise返回promise本身。
function fn(resolve){
setTimeout(function(){
resolve(123);
},3000);
}
let p0 = new Promise(fn);
let p1 = Promise.resolve(p0);
// 返回为true,返回的 Promise 即是 入参的 Promise 对象。
console.log(p0 === p1);
传入 thenable 对象,返回 Promise 对象跟随 thenable 对象的最终状态。
ES6 Promises 里提到了 Thenable 这个概念,简单来说它就是一个非常类似 Promise 的东西。最简单的例子就是 jQuery.ajax,它的返回值就是 thenable 对象。但是要谨记,并不是只要实现了 then 方法就一定能作为 Promise 对象来使用。
//如果传入的 value 本身就是 thenable 对象,返回的 promise 对象会跟随 thenable 对象的状态。
let promise = Promise.resolve($.ajax('/test/test.json'));// => promise对象
promise.then(function(value){
console.log(value);
});
其它情况,返回一个状态已变成 resolved 的 Promise 对象。
let p1 = Promise.resolve(123);
//打印p1 可以看到p1是一个状态置为resolved的Promise对象
console.log(p1)
//123
console.log(p1.then(res => console.log(res)));
Promise.reject
类方法,且与 resolve 唯一的不同是,返回的 promise 对象的状态为 rejected。
Promise.prototype.then
实例方法,可能需要return,可以设置两个参数,为 Promise 两种状态注册回调函数,函数形式:fn(vlaue){},value 是上一个任务的返回结果,then 中的函数一定要 return 一个结果或者一个新的 Promise 对象,才可以让之后的then 回调接收。
//可设置两个参数 p.then(onFulfilled[, onRejected]);
Promise.resolve("成功").then(res => {console.log(res)},error =>{console.log(error)})
//成功
- Promise.prototype.catch
该catch()方法处理Promise被拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected)相同。这意味着 onRejected即使您想回退到一个 undefined结果值,您也必须提供一个函数——例如obj.catch(() => {})。
注意因为抛出异常会返回rejected状态,所以catch也能捕获异常。可以搭配then完成链式操作
//(1)基本使用
Promise.reject("失败").catch(error =>{console.log(error)})//失败
//等同于
Promise.reject("失败").then(undefined,error =>{console.log(error)})
//(2)搭配then使用
var p1 = new Promise(function(resolve, reject) {
resolve('成功');
});
p1.then(function(value) {
console.log(value); //成功
throw '异常信息';
}).then(function(){
console.log('跳过'); // 没有resolved状态,跳过
}).catch(function(e) {
console.log(e); // 异常信息
});
Promise.prototype.finally
finally()返回一个方法Promise。在结束时,无论结果是否被履行或被拒绝,都会执行指定的功能完成。这为在Promise成功后都需要执行的代码提供了一种方式。这同样的语句需要在then()和catch()中各写一次的情况。
Promise.race
类方法,多个 Promise 任务同时执行,返回最先执行结束的 Promise 任务的结果,不管这个 Promise 结果是成功还是失败。
Promise.all
类方法,多个 Promise 任务同时执行。
如果全部成功执行,则以数组的方式返回所有 Promise 任务的执行结果。 如果有一个 Promise 任务 rejected,则只返回 rejected 任务的结果。
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
- …
以上便是常用的API了
(3) 链式操作
出门上班,想自己做饭但时间不够,那么把事情交给保姆:
- 你先去超市买菜。
- 用超市买回来的菜做饭。
- 将做好的饭菜送到我单位。
- 送到单位后打电话告诉我。
上面三步都是需要消耗时间的,我们可以理解为三个异步任务。利用 Promise 的写法来书写这个操作:
function 买菜(resolve,reject) {
setTimeout(function(){
resolve(['西红柿'、'鸡蛋'、'油菜']);
},3000)
}
function 做饭(resolve, reject){
setTimeout(function(){
//对做好的饭进行下一步处理。
resolve ({
主食: '米饭',
菜: ['西红柿炒鸡蛋'、'清炒油菜']
})
},3000)
}
function 送饭(resolve,reject){
//对送饭的结果进行下一步处理
resolve('送达');
}
function 电话通知我(){
//电话通知我后的下一步处理
给保姆加100块钱奖金;
}
那么在执行时,有以下链式操作
// 告诉保姆帮我做几件连贯的事情,先去超市买菜
new Promise(买菜)
//用买好的菜做饭
.then((买好的菜)=>{
return new Promise(做饭);
})
//把做好的饭送到老婆公司
.then((做好的饭)=>{
return new Promise(送饭);
})
//送完饭后打电话通知我
.then((送饭结果)=>{
电话通知我();
})
至此,我通知了保姆要做这些事情,然后我就可以放心去上班了。
上面举的例子,除了电话通知我是一个同步任务,其余的都是异步任务,异步任务 return 的是 promise对象。
(4) Promise的三个状态
- pending,异步任务正在进行。
- resolved (也可以叫fulfilled),异步任务执行成功。
- rejected,异步任务执行失败。
(5)使用总结
首先初始化一个 Promise 对象,可以通过两种方式创建,
这两种方式都会返回一个 Promise 对象。
new Promise(fn)
Promise.resolve(fn)
然后调用上一步返回的 promise 对象的 then 方法,注册回调函数。
then 中的回调函数可以有一个参数,也可以不带参数。如果 then 中的回调函数依赖上一步的返回结果,那么要带上参数。比如
new Promise(fn)
.then((res)=>{
//...
})
最后注册 catch 异常处理函数,处理前面回调中可能抛出的异常。
new Promise(fn)
.then((res)=>{
//...
})
.catch((err)=>{
//...
})
通常按照这三个步骤,你就能够应对绝大部分的异步处理场景。用熟之后,再去研究 Promise 各个函数更深层次的原理以及使用方式即可。
(6) 事件循环
then、 catch 、finally属于微任务
Promise在初始化时,传入的函数是同步执行的,然后注册 then 回调。注册完之后,继续往下执行同步代码,在这之前,then 中回调不会执行。同步代码块执行完毕后,才会在事件循环中检测是否有可用的 promise 回调,如果有,那么执行,如果没有,继续下一个事件循环。
(7) 实例
//实测jquery1.10中有then方法,百度搜索用的是这个版本,可在那控制台使用
function ajaxTest(resolve){
$.ajax({type:"post",url:'https://server.guet.link/app/user/login'}).then(res =>{
resolve(res)
})
}
new Promise(ajaxTest)
.then((res)=>{
// 注意
// axios不需要new Promise 返回
// jq ajax 有 thenable 也可以不用 这里是为了方便理解
// console.log("第1个then",res)
return new Promise(ajaxTest)
})
.then((res)=>{
// console.log("第2个then",res)
return new Promise(ajaxTest)
})
.catch((err)=>{
console.log(err)
})
.finally(()=>{
//isLoading = false
});
2. generatir以及async/await语法糖
ES6 出现了 generator 以及 async/await 语法糖,使异步处理更加接近同步代码写法,可读性更好,同时异常捕获和同步代码的书写趋于一致。
async/await是generator迭代函数的语法糖,关于generator请看另一篇
上面的列子可以写成这样:
function ajaxTest(resolve){
$.ajax({type:"post",url:'https://server.guet.link/app/user/login'}).then(res =>{
console.log("...网络访问完毕",res)
resolve(res)
})
}
(async ()=>{
let loginRes = await new Promise(ajaxTest);
let reloginRes = await new Promise(ajaxTest);
console.log("两次结果一览",loginRes,reloginRes);
})()