回调函数可以说是Javascript
中所有异步编程方式的根基,但是直接使用传统回调方式去完成复杂的异步流程,就无法避免大量的回调函数嵌套产生的回调地狱。为了避免回调地狱问题,CommonJS
社区提供了Promise
的规范,目的是为异步编程提供一种更合理、更强大的统一解决方案。后来在ES2015
中被标准化成为语言规范。
Promise
实际上就是一个对象,去表示一个异步任务最终结束后它究竟是成功还是失败。就是内部对外界作出了一个承诺,一开始这个承诺是一个待定的状态Pending
。最终有可能成功Fulfilled
,也有可能失败Rejected
。 不管成功或者失败都有相对应的反应,在承诺状态最终明确了过后都会有相对应的任务会被自动执行。而且这种承诺会有一个很明显的特点一旦明确了结果过后,就不可能再发生改变并且相应的状态会执行相应的回调。比如成功执行 onFulfilled
回调,失败执行onRejected
回调。
基本用法:
在代码层面Promise
它实际上就是Es2015
所提供的一个全局类型,我们可以使用它来构造一个Promise
实例,也就是创建一个新的承诺。这个类型的构造函数需要接受一个函数作为参数,这个函数就可以理解为一个兑现承诺的逻辑。这个函数会在构造Promise
的过程中被同步执行,在这个函数内部它可以接收到两个参数分别是:resolve
和 reject
二者都是一个函数。Resolve
函数的作用就是将这个Promise
对象的状态修改为成功 Fulfilled
,一般我们将异步任务的操作结果会通过这个 Resolve
参数传递出去。Reject
函数的作用就是将这个Promise
的状态修改为失败 Rejected
,这个失败的参数一般传递的是一个错误对象 new Error()
用来表示这个承诺它为什么失败,也就是一个理由。Promise
实例被创建成功以后,我们就可以使用这个实例的then
方法分别去指定onFullfilled
和onRejected
回调函数。then
方法传入的第一个参数就是onFulfilled
回调函数,可以在这个函数中定义一些成功后要执行的操作。第二参数传递的是onRejected
回调函数,在这个函数内部定义一些失败过后要处理的任务。
const promise = new Promise(function(resolve, reject){
if (Math.random(1, 10) > 5) {
resolve(100)
} else {
reject(new Error('promise rejected'))
}
})
promise.then(
(value) => {console.log(value)}, // 100
(error) => {console.log(error)}, // promise rejected
)
使用案例:
使用Promise
封装一个ajax
,首先定义一个叫Ajax
的函数这个函数有一个url
参数,用来接受外界需要请求的地址,然后在这个函数当中直接对外返回一个promise
对象实例,就相当于对外作出一个承诺,在这个promise
对象的执行逻辑当中使用XMLHttpRequest
对象去发送一个Ajax
请求。然后要设置一下 xhr
的请求方式,请求的地址为参数当中的url
。往下设置一下响应类型为json
,这个方式是html5
当中引入的一个新特性。这样的话我们就可以在请求完成过后直接拿到一个json
对象而不是字符串,接下来去注册一下xhr
的onload
事件。它同时也是html5
的新特性这个事件是请求完成过后,也就是我们传统的readyState
等于4的状态。在请求完成这个事件当中我们应该先去判断一下请求的状态是不是200
,如果是的话就意味着请求已经成功。那么我们应该去调用Resolve
去表示我们这个Promise
已经成功,并且将请求回来的数据传给Resolve
。反之如果请求失败的话,我们就调用Reject
函数去表示Promise
失败,这里我们就应该传入一个错误信息对象,这个错误信息就是当前的状态文本。完成以后去调用一下xhr
的send
方法开始执行这个异步请求,这样的话这个promise
版本的Ajax
函数就封装好了。
function ajax (url) {
return new Promise(function(resovle, reject) {
const xhr = new XMLHttpRequest()
xhr.open('POST', url)
xhr.requestType = 'json'
xhr.onLoad = function () {
if (this.status === 200) {
resovle(this.response)
} else {
reject(new Error(this.statusText))
}
}
})
}
ajax('/api/users.json')
.then(
res => {},
error => {}
)
常见误区:
通过前面的尝试我们发现从表象上来看Promise
的本质也就是使用回调函数方式去定义异步任务结束过后所需要执行的任务,只不过这里的回调函数是通过then
方法传递的而且Promise
将我们的回调分成成功onFulfilled
和失败onRejected
两种,既有回调函数也就会出现回调地狱的问题。这就产生我们使用Promise
的第一个误区,对此应该使用Promise
对象then
方法做链式调用尽量来保持异步调用的扁平化。
链式调用:
Promise
一个很大的优势就是链式调用,这样就能最大程度的避免回调地狱。**Promise**
的**then**
方法就是为**Promise**
对象去添加状态明确后的回调函数,它的第一个参数是 onFulfilled
回调、第二回调是 onRejected
回调。其中失败过后的回调是可以省略的,then
方法最大的特点就是它的内部也会返回一个Promise
对象。按照以往我们对链式调用的认知这里返回的promise
应该就是当前的这个Promise
对象,实际上并不是同一个对象。所以说这里的链式调用它并不是以往我们常见的那种在方法内部去返回this
的方式去实现的链式调用,这里的then
方法它返回的是一个全新的 Promise
对象,目的就是去实现一个promise
的链条也就是承诺结束过后再去返回一个新的承诺。那每一个承诺都可以负责一个异步任务,它们相互之间并没有什么影响,这就意味着如果我们不断的链式调用then
方法。这里每一个then
方法它实际上都是在为上一个then
方法返回的Promise
对象,去添加状态明确后的回调。这些promise
会依次执行,这里添加的这些回调函数自然也就是从前到后依次执行。而且我们也可以在then
的回调当中手动返回一个Promise
对象,它的执行结果会在下一个then
方法中被调用,这样我们就可以避免不必要的回调嵌套。而且以此类推,如果有多个连续的任务就可以使用这种链式调用的方式去避免回调的嵌套。从而尽量保证代码的扁平化,如果说我们的回调当中返回的不是一个Promise
而是一个普通的值,这个值就会作为当前这个then
方法返回的Promise
中的值,在下一个then
方法中接收的回调参数它实际上拿到的就是这样一个值,如果没有返回任何一个值它默认返回的就是一个 undefined
。
总结:1、Promise
对象的then
方法他会返回一个新的Promise
对象,所以说我们就可以使用链式调用的方式去添加then
方法;2、后面的then
方法它实际上就是在为上一个then
方法当中返回的Promise
去注册对应的回调;3、前面then
方法回调函数当中的返回值会作为后面then
方法回调的参数;4、如果说我们在回调当中返回的是一个Promise
对象的话那后面then
方法当中的回调实际上就会等待这个Promise
结束,也就是说后面的then
方法实际上就是相当于为我们所返回的这个Promise
对象去注册了对应的回调。
异常处理:
正如前面所说Promise
的结果一旦失败,它就会调用我们在then
方法当中去传入的onRejected
回调函数。在promise
执行的过程当中,出现了异常或者手动抛出了一个异常那onRejected
回调也会被执行。onRejected
函数实际上就是为promise
当中的异常去做一些处理,在Promise
失败了或者出现了异常时都会被执行。其实关于onRejected
回调的注册还有一个更常见的用法,就是使用Promise
实例的catch
方法去注册onRejected
回调。catch
方法其实就是then
方法的别名,因为调用它实际上就相当于调用了then
方法,然后第一个参数去传递了一个undefined
。相对来说catch
方法去指定失败回调要更为常见,应为这种方式会更适合于链式调用。从表象上看我们使用catch
方法注册失败方法,跟直接在then
方法的第二个参数去注册效果是一样的,他们都能捕获到Promise
它在执行过程中的异常。但是仔细去对比这两种方式其实他们有很大的差异,因为每个then
方法返回的都是新的Promise
对象,这也就是说我们在后面通过链式调用的方式调用的这个catch
他实际上是在给前面then
方法返回的promise
对象去指定失败回调,并不是直接去给第一个promise
对象所指定的。因为这是同一个Promise
链条,前面Promise
上的异常会一直被往后传递,所以在这里能够捕获到第一个Promise
当中的异常,而通过then
方法的第二个参数去指定的失败回调函数只是给第一个Promise
对象指定的,也就是说它只能捕获到这个Promise
对象的异常。具体在表象上的差异就是,如果我们在then
方法当中返回了第二个Promise
而且这个promise
执行过程当中出现了异常,那我们使用then
的第二个参数去注册的失败回调它是捕获不到第二个promise
的异常的。因为它只是给第一个promise
对象注册的失败回调,对于链式调用的情况下最好使用第二种方式去分开指定成功回调和失败回调。因为Promise
链条上任何一个异常都会被向后传递,直至被捕获。那也就是说这种方式更像是给整个Promise
链条注册的失败回调,所以说它相对来讲要更通用一些。除此之外我们还可以在全局对象上去注册一个onhandledrejection
事件,去处理那些我们代码当中没有被手动捕获的Promise
异常。在浏览器环境下可以注册在window
对象上,node
环境中要定义在process
对象上。
window.addEventListener('onhandledrejection', event => {
const {
reason, // Promise 失败原因
promise // 出现异常的Promise对象
} = event
event.preventDefault()
}, false)
process.addEventListener('onhandledRejection', (reason, promise) => {
console.log(reason, promise)
})
静态方法:
在Promise
类型当中还有两个静态方法,也经常会用到。首先是 Promise.resolve()
这个方法的作用就是快速的把一个值转换为一个返回成功结果的Promise
对象, Promise.resolve('foo')
会返回一个状态为fulfilled
的Promise
对象。foo
就是这个promise
对象的返回值也就是说我们可以在它的onFulfilled
的回调函数当中拿到的参数就是 foo
。这种方式完全等价于我们通过 new Promise(resolve => resolve('foo'))
的这种方式,如果这个方法接受到的是另一个Promise
对象,这个Promise
对象会被原样返回。
const promise = ajax('/api/user.json')
const promise2 = Promise.resolve(promise)
console,log(promise === promise2) // true
如果我们传入的是一个对象并且这个对象也和Promise
一样有一个then
方法,同时在这个then
方法中可以接收onFulfilled
和onRejected
两个回调函数,这样一个对象也可以作为一个Promise
对象被执行。因为这个对象也实现了thenable
的接口,换句话说它是一个可以被then
的对象。支持这种对象的原因是,在原生Promise
对象还没有普及之间很多时候都是使用第三方的库去实现的promise
。
Promise.resolve({
then(onFulfilled, onRejected) {
onFulfilled('foo')
}
}).then(value => {
console.log(value) // foo
})
除了Promise.resolve()
方法还有一个与之对应的Promisee.reject()
方法。它的作用就是快速创建一个是失败结果的Promise
。
Promise.reject(new Error('rejected'))
.catch(error => console.log(error)) // rejected
并行执行:
前面介绍的操作都是通过Promise
串联执行多个异步任务,也就是一个任务结束过后再去开启下一个任务。相比于传统回调的方式Promise
它提供了更扁平的异步编程体验,如果同时并行执行多个异步任务。Promise
也可以提供更为完善的体验,例如在页面中需要请求多个接口的情况。如果说这些接口相互间没有什么依赖,最好的选择就是同时去请求他们避免一个一个依次去请求会消耗更多的时间。这种并行请求其实很容易实现,我们只需要单独去调用这里的ajax
函数就可以了。难的是如何判断所有的请求都结束了,那样一个时机。传统我们的做法是定义一个计数器然后每结束一次请求,我们让这个计数器去累加一次。直到这个计数器的数量等于任务的数量,就表示所有的任务结束了。这种方法会非常的麻烦,而且还需要考虑出现异常的情况。在这样一种情况下使用Promise
类型的all
方法就会简单的多,因为这个方法可以将多个Promise
合并为一个Promise
统一去管理。Promise.all([....])
接收一个数组,数组中的每一项都是一个Promise
对象。我们可以把这些promise
都看作一个一个的异步任务,这个方法会返回全新的Promise
对象,当内部所有的Promise
都完成过后我们所返回的这个全新的Promise
才会完成。此时,这个Promise
对象它拿到的结果就是一个数组,在这个数组里面包含着每一个异步任务执行过后的结果。在这个任务的执行当中只有所有的任务都成功结束了,这里的新的Promise
才会成功结束。如果说其中有任何一个任务失败了这个Promise
就会以失败结束,这是一个很好的同步执行多个异步Promise
任务。
// 组合使用串行和并行
ajax('/api/urls.json')
.then(value => {
const urls = Object.values(value)
const tasks = urls.map(url => ajax(url))
return Promise.all(tasks)
}).then(values => {
console.log(values)
})
除了提供Promise.all()
方法以外,还提供了一个Promise.race()
方法。这个方法它同样可以把多个Promise
对象组合为一个全新的Promise
对象,但是与Promise.all()
方法不同。Promise.all()
它是等待所有的任务结束过后才会结束,而Promise.race()
它是跟着所有任务当中第一个完成的任务一起结束,也就是说只要有任何一个任务完成了,那这个所返回的新的Promise
对象也就会完成。
const request = ajax('/api/users.json')
const timeOut = setTimeout(() => throw(new Error('Time Out')), 500)
Promise.race([
request,
timeOut
]).then(
value => console.log(value)
).catch (
error => console.log(error)
)
执行时序:
最后来深入了解一下关于Promise
执行时序的问题,也就是执行的顺序。正如开始所见到的,即便说Promise
内部没有传递异步任务,它的回调函数仍然会进入到回调队列当中去排队。也就是说我么必须要等待当前所有的同步代码执行完成以后,才会去执行Promise
当中的回调。如果继续在后面使用链式调用的方式去传递多个回调,这里每一个回调也应该是依次执行。但是如果Promise
前面有个setTimeout
的话会先执行Promise
,以为setTimeout
属于宏任务、Promise
属于微任务。在回调队列当中的任务一般被称为宏任务,宏任务执行执行过程中可以额外加上一些额外需求。这时候对于这些临时额外的需求,可以选择作为一个新的宏任务重新进入到回调队列当中去排队。也可以作为当前任务的微任务,就是可以直接在当前这个任务结束过后就立即去执行,而不是到整个队伍的末尾在重新排队。这就是宏任务和微任务之间的差异,Promise
的回调就是作为微任务执行的,所以说他会在本轮调用结束的末尾去自动执行。微任务的概念实际上是在后来才被引入到JS
当中的,它的目的就是为了提高JS
应用的响应能力。目前接触到的大部分异步调用的API
都会作为宏任务进入到回调队列,而Promise
对象和MutationObserver
与node当中的 process.nextTick
会作为微任务,直接在本轮调用的末尾就执行。
console.log('global start')
setTimeout(() => {
console.log('Time out')
}, 0)
Promise.resolve(10)
.then(value => {
console.log(value)
return ++value
})
.then(value => {
console.log(value)
return ++value
})
console.log('global end')
// global start
// global end
// 10
// 11
// Time out
核心逻辑剖析:
Promise
就是一个类,在实例化的时候传递一个函数作为执行器,并且会立即执行。这个执行器有两个参数:resolve
与 reject
,这两个参数实际上是一个函数。它们的目的就是为了更改Promise
的状态。在Promise
中有三种状态:等待pending
、成功fulfilled
、失败rejected
,一旦状态确定就不可更改。pending --> fulfilled 、 pending --> rejected 、fulfilled -/-> rejected
。resolve
和reject
函数是用来更改状态的,resolve: fulfilled、reject:rejected
。resolve
接受一个成功的返回值,reject
接受一个失败的原因。
const PENDING = 'pending' // 等待
const FULFILLED = 'fulfilled' // 成功
const REJECTED = 'rejected' // 失败
class Promise {
constructor (executor) {
executor(this.resolve, this.reject) // 立即执行执行器
}
status = PENDING // Promise状态
resolve = () => { // 将Promise状态改为成功
if (this.status !== PENDING) return // 当非PENDING状态时返回
this.status = FULFILLED
}
reject = () => { // 将Promise状态改为失败
if (this.status !== PENDING) return // 当非PENDING状态时返回
this.status = REJECTED
}
}
then
方法做的事情就是判断状态,如果状态成功则调用成功回调函数;反之,调用失败回调函数。then
方法是被定义在原型对象上的。then
的成功回调函数 onFulfilled(value) value
表示成功之后的值、onRejected(reason) reason
是失败的原因。
class Promise {
....
value = undefined // 成功之后的值
reason = undefined // 失败之后的原因
resolve = value => {
...
this.value = value // 保存成功之后的值
}
rejected = reason => {
...
this.reason = reason // 保存失败之后的值
}
then (onFulfilled, onRejected) {
/** 判断Promise状态 */
if (this.status === FULFILLED) { // 成功状态
onFulfilled(this.value)
} else if (this.status === REJECTED) { // 失败状态
onRejected(this.reason)
}
}
}
Promise
内部处理异步函数的流程是:在then
函数被调用执行时,判断Promise
的状态,当状态还是pending
时就把成功回调函数和失败回调函数缓存起来。并在resolve
和reject
函数体内部判断是否有成功回调函数或者失败回调函数的缓存,有则执行。
class Promise {
...
fulfilledCallback = undefined // 实例属性成功回调
rejectedCallback = undefined // 实例属性失败回调
resolve = value => {
....
this.fulfilledCallback
&& this.fulfilledCallback(value) // 判断回调是否存在,存在就调用
}
reject = reason => {
...
this.fulfilledCallback // 判断回调是否存在,存在就调用
&& this.rejectedCallback(reason)
}
then (onFulfilled, onRejected) {
if (this.status === FULFILLED) {
...
} else if (this.status === REJECTED) {
...
} else { // 异步延迟执行下的等待状态
this.fulfilledCallback = onFulfilled
this.rejectedCallback = onRejected
}
}
}
在同一个Promise
对象下的then
方法是可以多次被调用的,这就需要将异步情况下成功回调函数或者失败回调函数缓存在数组中,并在resolve
或reject
函数体中按序弹出 while(this.successCallback.length) this.successCallback.shift()(this.value)
class Promise {
...
fulfilledCallbacks = [] // 实例属性成功回调
rejectedCallbacks = [] // 实例属性失败回调
resolve = value => {
....
while(this.fulfilledCallbacks.length) // 判断回调是否存在,存在就调用
this.fulfilledCallbacks.shift()(value)
}
reject = reason => {
...
while(this.fulfilledCallbacks.length) // 判断回调是否存在,存在就调用
this.rejectedCallbacks.shift()(reason)
}
then (onFulfilled, onRejected) {
if (this.status === FULFILLED) {
...
} else if (this.status === REJECTED) {
...
} else { // 异步延迟执行下的等待状态
this.fulfilledCallbacks.push(onFulfilled)
this.rejectedCallbacks.push(onRejected)
}
}
}
then
方法是可以被链式调用的,并且后面的then
方法的回调函数拿到的值是这个then
方法回调函数的返回值。要实现then
方法的链式调用就是在then
方法中return
一个新的Promise
实例,并将then
的执行体传入新的Promise
中作为执行器。这样就可以通过将本次成功回调函数的执行返回值传给新执行器的 resolve
来实现将上一个then
回调函数的返回值传给下一个then
的目的。
class Promise {
...
then (onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
const result = onFulfilled(this.value)
resolve(result)
} else if (this.status === REJECTED) {
...
} else {
...
}
})
}
}
如果在上一个then
的回调中又返回了一个Promise
对象,我们在处理then
函数时要对返回值进行类型的判断。
class Promise {
...
then (onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
const result = onFulfilled(this.value)
resolvePromise(result, resolve, reject)
} else if (this.status === REJECTED) {
...
} else {
...
}
})
}
}
function resolvePromise (result, resolve, reject) {
if (result instanceof Promise) {
result.then(resolve, rejiect)
} else {
resolve(result)
}
}
如果在then的回调中又返回了自身的这个Promise的实例就会发生循环调用的错误,为了避免这中错误,要在then的链式调用中进行识别判断该promise对象是否是自返回。
class Promise {
...
then (...) {
const promise = new Promise((...) => {
if (this.status === FULFILLED) {
setTimeout(() => {
....
resolve(promise, result, resolve, reject)
}, 0)
} else if (this.status === REJECTED) {
...
} else {
...
}
})
return promise
}
}
function resolvePromise (promise, result, resolve, reject) {
if (promise === result)
return reject(
new TypeError('Chaining cycle detected for promise #<Promise>')
)
...
}
错误捕获要考虑两个情况,第一个是在执行体内部发生错误,当执行器当中的代码在执行的过程中。发生错误的时候,就让Promise
的状态变成失败状态。也就是在then
方法的第二个参数要捕获到这个错误,可以在构造函数中执行器的外部添加 try catch
;
class Promise {
constructor (executor) {
try {
executor(this.resolve, this.reject)
} catch (error) {
this.reject(error)
}
}
...
}
第二个是在then
方法的回调函数内部发生错误,这个错误要在下一个then
方法执行时捕获到。这个捕获的实现是在then
方法体中successCallback
或failCallback
调用的外部添加try catch
这里也要考虑异步的情况。
class Promise {
resovle = value => {
...
this.onFulfilledCallback.shift()()
}
reject = reason => {
...
this.onRejectedCallback.shift()()
}
then (onFulfilled, onRejected) {
const promise = new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(_ => {
try {
const result = onFulfilled(this.value)
resolvePromise(promise, result, resovle, reject)
} catch (error) {
reject(error)
}
}, 0)
} else if (this.status ==== REJECTED) {
setTimeout(_ => {
try {
const result = onRejected(this.reason)
resolvePromise(promise, result, resovle, reject)
} catch (error) {
reject(error)
}
}, 0)
} else {
this.fulfilledCallbacks.push(_ => {
setTimeout(_ => {
try {
const result = onFulfilled(this.value)
resolvePromise(promise, result, resovle, reject)
} catch (error) {
reject(error)
}
}, 0)
}) this.rejectedCallbacks.push(_ => {
setTimeout(_ => {
try {
const result = onRejected(this.reason)
resolvePromise(promise, result, resovle, reject)
} catch (error) {
reject(error)
}
}, 0)
}) }
})
return promise
}
}
...
将then
方法的参数变成可选参数:successCallback ? successCallback : value => value failCallback ? failCallback : reason => throw reason