异步模式对于单线程的 JavaScript 非常重要,同时也是 JavaScript 的核心特点。
而回调函数则是 JavaScript 中所有异步编程方式的根基 。
如果我们直接使用传统回调方式去处理复杂的异步逻辑,那么我们就一定避免不了大量的回调函数嵌套问题(回调地狱)。
1. Promise 是什么?都有哪几种状态?
commonJs 社区最早提出和实现,在ECMAScript 2015 被标准化,成为语言规范。并且原生提供了Promise
对象。
Promise 实际上就是一个对象,用于表示一个异步任务结束后究竟是成功还是失败。Promise 一共有三种状态:Pending(进行中)、Fulfilled(已成功)) 和 Rejected(已失败)。
当 Promise 状态转为 Fulfilled 时,会自动触发 onFulfilled 回调函数。
当 Promise 状态转为 Rejected 时,会自动触发 onRejected 回调函数。
一旦 Promise 的状态 转为 Fulfilled 或 Rejected,状态不会再发生转变。也就是说 Promise 的状态变化是不可逆的。
基本用法:
在代码层面,Promise是Es6提供的一个全局类。可以使用它来创建一个promise实例(创建一个新的承诺)。这个类的构造函数需要接收一个函数作为参数,这个函数可以理解为兑现承诺的逻辑,这个函数会在构造promise的过程中被同步执行,
它接收两个参数:resolve,reject,二者都是函数。
resolve函数的作用是将promise对象的状态修改为fulfilled(成功 )。一般将异步任务的操作结果通过resolve的参数传递出去。
resolve函数的作用是将promise对象的状态修改为rejected(失败)。这里失败的参数一般传递的是一个错误对象,用来表示承诺为什么失败。
const promise = new Promise(function(resolve, reject) {
// 在 Promise 构造过程中同步执行
resolve(100) // Promise 状态转变为 Fulfilled
reject(new Error('异常原因')) // Promise 状态转变为 Rejected
})
promise.then(function (value) {
// 成功的回调函数
console.log('resolved', value)
}, function (error) {
// 失败的回调函数
console.log('rejected', error)
})
注意:上面代码因为promise的状态一旦确定过后就不能在被修改,所以resolve和reject二者只能执行其一。
promise对象创建完成以后就可以去调用它的then方法分别去指定onfulfilled和onrejected回调函数。分别是 then 方法的第 1 位置和第 2 位置参数。
同时要注意的是:即使 Promise 里没有任何的同步操作逻辑需要执行,then 方法指定的回调函数都会放到消息队列中,直到当前调用栈清空时(同步代码执行完成后),再从消息队列里取出执行。
2.Promise使用案例---模拟ajax
function ajax(url){
return new Promise(function(resolve,reject){
const xhr = new XMLHttpRequest();
xhr.open("GET",url);
xhr.responseType = "json";
xhr.onload = function(res){
if(this.status == 200){
resolve(this.response)
}else{
reject( new Error(this.statusText) )
}
};
xhr.send();
})
};
const end = ajax("../api/urls.json");
end.then(function(res){
console.log(res)
},function(error){
console.log(error)
})
3.Promise的常见误区
从表象来看。promise的本质也是使用回调函数去定义异步任务执行结束后所需要执行的任务。只不过这里的回调函数是通过then方法传递进去的。而且 Promise 把回调函数分为了两种,分别是成功回调 onFulfilled 和失败回调 onRejected。
那既然还是回调函数,如果说我们还需要连续串联多个异步任务,那也就仍然会出现回调函数嵌套的问题。
也就是下面代码:
ajax('/api/url.json').then(function (url) {
ajax('/api/url.json').then(function (url) {
ajax('/api/url.json').then(function (url) {
ajax('/api/url.json').then(function (url) {
})
})
})
})
如果这样写,Promise 确实就没有存在的意义了,不仅没有解决问题,还增加了额外复杂度,还不如使用传统的回调函数方式。
嵌套使用的方式是使用promise最常见的误区,正常的做法是借助于promise的then方法链式调用的特点,尽量保证异步任务的扁平化。
4.Promise的链式调用
promise提供了更扁平的异步方式体验。
Promise 最大的优势就是可以链式调用。链式调用能够尽可能保证异步任务扁平化,这样就可以尽量的避免出现回调嵌套的问题。
正如前面所说,then方法的作用是为promise对象添加状态明确后的回调函数。 成功回调 onFulfilled 和失败回调 onRejected,其中失败回调一般可以省略的。
then方法最大的特点:是它内部也会返回一个promise对象
打印一下下代码的变量值,就会发现确实 promiseResult 确实也是一个 Promise 对象。
但需要明确的是,promiseObj 和 promiseResult 是完全不同的 Promise对象。then 方法返回的是一个全新的 Promise 对象,而不是像我们常见的 在方法内部返回一个this对象去实现链式调用。
const promiseObj = ajax('../api/urls.json')
const promiseResult = promiseObj.then(res => {
console.log('onFulfilled', res);
}).catch(error => {
console.log('onRejected', error);
})
console.log(promiseResult)
console.log(promiseObj === promiseResult)
这样做的目的是为了去实现一个 Promise 链条,也就是在承诺结束后返回一个新的承诺。这样每一个 Promise 对象(承诺)都可以负责一个异步任务,且相互之间不会有任何影响。
这里每一个 then 方法,实际上都是在为上一个 then 返回的 Promise 对象添加状态明确过后的回调,这些 Promise 会依次执行,因此 then 里的回调也是从前到后依次执行。
ajax('/api/url.json').then(res => {
console.log(1);
}).then(res => {
console.log(2);
}).then(res => {
console.log(3);
}).then(res => {
console.log(4);
})
// 1
// 2
// 3
// 4
而且我们也可以在then的回调当中手动返回一个promise对象,例如:
在第一个then方法的回调当中return出来的 ajax方法返回的promise对象。那么第二个then方法实际上就是在为它添加的状态明确过后的回调函数;
也就是说这里的ajax请求成功过后会自动执行第二个then方法。这样就能避免不必要的回调嵌套了。
而且以此类推,如果有多个连续的串行异步任务,就可以使用这种链式调用的方式去避免回调嵌套,从而保证代码的扁平化。
ajax('/api/url.json').then(res => { //第一个then方法
console.log(1);
return ajax("../api/urls.json"); //上文的ajax方法返回一个promise对象
}).then(res => { //第一个then方法
console.log(res);
})
那如果then的回调返回的不是一个promise,而是一个普通的值,那么这个值就会作为当前then方法返回的promise中的值,在下一个then方法的回调函数的参数被拿到。
如果当前then方法没有返回值,那么 默认返回的值就是undefined。
总结:
- Promise 对象的 then 方法会返回一个全新的 Promise 对象,于是我们就可以使用链式调用的方式避免回调函数嵌套。
- 后面的 then 方法就是在为上一个 then 返回的 Promise 注册回调。
- 前面 then 方法中回调函数的返回值会作为后面 then 方法回调的参数。
- 如果回调中返回的是 Promise, 那么下一个 then 方法的回调会等待它的结束。
5.Promise 异常的处理
promise的结果一旦失败,就会调用then方法传入的onRejected 回调函数。
例如 我们请求ajax时传入一个不存在的文件,那它就会执行onRejected回调函数;
除此之外,如果在执行promise的过程中出现了异常,或者手动抛出了一个异常,onRejected回调函数也会被执行。例如在promise执行时调用一个不存在的Foo方法 ,这个方法不存在 。这个异常行为会被onRejected回调捕捉到并返回报错信息
function ajax(url){
Foo(); //这个方法不存在 。这个异常行为会被onRejected回调捕捉到并返回报错信息
throw new Error() //手动抛出的异常 也会被onRejected回调捕捉到并返回异常信息
return new Promise(function(resolve,reject){
const xhr = new XMLHttpRequest();
xhr.open("GET",url);
xhr.responseType = "json";
xhr.onload = function(res){
if(this.status == 200){
resolve(this.response)
}else{
reject( new Error(this.statusText) )
}
};
xhr.send();
})
};
const end = ajax("../api/不存在.json");
end.then(function(res){
console.log(res)
},function(error){
console.log(error)
})
所以说 onRejected 回调实际上就是为promise当中的异常做处理。当promise失败 或 出现异常时 它都会被执行。
其实关于onRejected 回调的注册还有一个更常见的用法。就是使用promise的 catch 方法去注册onRejected 回调。测试一下发现结果可以正常执行。
ajax('/api/不存在.json')
.then(function onFulfilled (value) {
console.log('onFulfilled', value)
})
.catch(function onRejected (error) {
console.log('onRejected', error)
})
其实这里的catch方法 就是 then 方法的一个别名。比如下面代码:
这里的 catch(onRejected) 实际上就相当于 then(undefined, onRejected)
ajax('/api/不存在.json')
.then(function onFulfilled (value) {
console.log('onFulfilled', value)
})
.then(undefined, function onRejected (error) {
console.log('onRejected', error)
})
相对来说用catch来指定onRejected 回调更为常见一点,因为这种方式更适合于链式调用。具体原因看下面:
首先下面两个promise 它们的的catch 方法的onRejected回调 和 then方法的onRejected回调都可以捕获到第一个promise的执行中的异常。
但是仔细对比这两种方式,它们有很大的差异。前面说过:因为每个then方法返回的都是全新的promise对象。就是说在后面的catch 方法指定的回调 其实是给前面then方法返回的promise对象 注册的onRjected回调。并不是给第一个promise指定的回调。
只不过因为这是一个同promise链条,Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。前面promise上的异常会一直往后传递 ,所以在这里才能捕获到第一个promise的异常。
而通过then方法第二个参数去指定的onRejected 失败回调函数只是给第一个promise指定的,也就是说它只能捕获第一个promise 的异常。
如果我们在第一个then方法返回了第二个promise,而且这个promise执行中如果出现了异常,那第一个then方法第二个参数指定的失败回调就捕获不到第二个promise的异常。
ajax('/api/不存在.json')
.then(function onFulfilled (value) {
console.log('onFulfilled', value);
return ajax('不存在2.json');
},(error) {
console.log('onRejected', error)
});
ajax('/api/不存在.json')
.then(function onFulfilled (value) {
console.log('onFulfilled', value);
return ajax('不存在2.json');
})
.catch(function onRejected (error) {
console.log('onRejected', error)
})
所以说对于链式调用的情况下,建议使用第二种方式 分开去指定成功回调和失败回调。因为promise链条上的任何一个异常都会被向后传递 直至被捕获。
因此这种方式更像是给整个promise链条注册的 失败回调。所以说它相对而言更通用些。
unhandledrejection 全局捕获 Promise 异常
不推荐。我们应该在代码中明确的捕获每一个可能的异常,而不是丢给全局统一处理。
// Web
window.addEventListener('unhandledrejection', event => {
const { reason, promise } = event
// reason => Promise 失败原因,一般是一个错误对象
// promise => 出现异常的 Promise 对象
console.log(reason, promise);
event.preventDefault()
}, false)
// Node
process.on('unhandledRejection', (reason, promise) => {
console.log(reason, promise);
})
6.Promise 静态方法
promise.resolve():快速的把一个值转化为promise对象,例如:
通过Promise.resolve('foo') 传入一个 "foo" 的字符串,他就直接返回为一个状态为fulfilled (成功)的一个promise对象。"foo"字符串就会作为这个promise对象所返回的值,在它的onFulfilled回调函数拿到的参数就是"foo"字符串。
const promise = Promise.resolve('foo')
console.log(promise) //promise对象
promise.then(function (value) {
console.log(value)
})
它等价于:
new Promise(function (resolve, reject) {
resolve('foo')
}).then(value => {
console.log(value) // foo
})
另外,promise.resolve()方法如果接收到的是一个promise对象,那么它会原样返回这个promise对象。
同样的如下代码所示,我们通过 Promise.resolve 包装一个 Promise 对象,那么实质上两个结果是没什么不一样的。
const promise_1 = ajax('./1-ajax.json')
const promise_2 = Promise.resolve(promise_1)
console.log(promise_1 === promise_2); // true
值得一提的是,我们也可以给 Promise.resolve 传递一个包含 then 属性的对象,这个对象也有一个跟promise一样的then方法。 这样的写法实际上是利用了 thenable 的模式实现的,代码如下:
Promise.resolve({
then: function (onFulfilled, onRejected) {
onFulfilled('foo')
}
}).then(value => {
console.log(value);
})
Promise.rejected() 方法:作用是快速创建一个 一定是失败的对象,它的参数没有那么多情况了,因为无论传入什么,它的参数都会作为失败的结果
Promise.reject(new Error('rejected')).catch(error => {
console.log(error)
})
7.Promise 并行执行----Promise.all()
前面说的都是promise去串联多个异步任务,也就是一个任务结束后再去开启下一个任务。Promise 的使用方式正如前面案例那样进行链式调用,每一个 Promise 对象亦或说每一个 then 方法都是顺序依次执行的。
那么如果我们需要同时并行多个异步任务,promise也提供了更为完善的体验。
正如我们在项目中经常遇到的情况:我们需要同时请求多个接口来获取数据渲染页面,而且必须等待所以数据都准备完毕才能执行操作,那怎么样去判断所有请求都已经结束的那个时机呢。一般是定义一个计数器,每当一个接口成功返回执行计数器++,直到计数器的值和请求数量一致,就表示所有请求结束,这样会比较麻烦,而且还要考虑有请求异常的情况。
那么我们使用 Promise 的 all 方法就会简单许多。它会将多个promise 合并为一个promise 统一去管理。
Promise.all()方法 接收一个数组,数组的每个元素都是一个promise 对象。 这个方法返回一个全新的promise对象,当这个数组中的所有promise都完成之后。这个全新的promise才会完成。
此时这个新的promise对象拿到的结果也是一个数组,数组包含每一个异步任务执行的结果。
需要注意的是,只要管理的多个 Promise 中存在一个失败的结果,那么 promiseAll 同样也会是失败结果。
const promiseAll = Promise.all([ajax('./1-ajax.json'), ajax('./1-ajax.json')])
promiseAll.then(res => {
console.log(res)
})
这是一种同步执行多个promise的方式,并行请求相比顺序依次请求会消耗更少的时间
接下来再综合使用一下Promise的串联 和 并行的两种方式:
const promise = ajax("../api/urls.json");
promise.then(function(res){
console.log(res);
let urls = Object.values(res); //通过请求urls.json返回一个装有多个地址的对象并取出放到数组里
arr = urls.map( item=> ajax(item) ); //通过数组的map方法将字符串数组转换成一个包含有所有promise对象任务对象的数组
return Promise.all( [ ...arr ] );
}).then(function(res){
console.log(res)
})
8.Promise的执行时序
前面说过,即使promise 内部没有执行的异步任务,它的回调函数仍然会进入回调队列中去排队,等待所有同步代码执行完毕才会执行promise的回调(这句话并不是很严谨)
接下来看一个例子:
console.log('global start')
// setTimeout 的回调是 宏任务,进入回调队列排队
setTimeout(() => {
console.log('setTimeout')
}, 0)
// Promise 的回调是 微任务,本轮调用末尾直接执行
Promise.resolve()
.then(() => {
console.log('promise')
})
.then(() => {
console.log('promise 2')
})
.then(() => {
console.log('promise 3')
})
console.log('global end')
什么是宏任务?什么是微任务?
回调队列中的任务称之为【宏任务】。
宏任务执行过程中可以临时加上一些额外需求,这些额外的需求有两种处理方案:第一种是可以选择作为一个新的宏任务进到队列中排队,第二种就是可以作为当前任务的【微任务】。
微任务指的是在当前任务结束过后立即执行。
微任务是后来才加入到 js 中的,就是为了提高整体的响应能力。
哪些属于宏任务?哪些属于微任务?
Promise 的回调会作为微任务执行,所以会在本轮调用的末尾去自动执行。而 setTimeout 是以宏任务的形式进入到回调队列的末尾。
目前绝大多数异步调用都是作为宏任务执行。而 Promise、MutationObserver、process.nextTick 是会作为微任务在本轮任务末尾执行的。
博主自己要注意:宏任务,微任务 和它相关的执行栈 执行上下文 事件循环,事件队列中的运行规则还要抽时间 找更详细的相关资料去深入学习!