面试高频 Promise
Promise出现的背景
- 异步回调函数不连续,多个回调函数同时注册,执行时间却不够线性
const callback1 = function() {}
const callback2 = function() {}
const callback3 = function() {}
const res1 = ajax(callback1)
const res2 = ajax(callback2)
const res3 = ajax(callback3)
三个回调函数同时注册,但是他们的调用时机确实不固定的
- 回调地狱
function triggerCb(cb1) {
return function(cb2) {
cb1()
cb2()
}
}
回调地狱会导致以下几个问题:
1.容易造成代码的可读性极差,并且内部的操作会非常紊乱
2.每个回调函数都有成功和失败的状态,需要每个回调函数都维护一套错误机制
熟悉设计模式的小伙伴应该清楚职责链模式, Promise属于职责链中异步调用的典型案例
Promise知识点汇集
Promise的状态
Promise的状态分为: pending、fulfilled、rejected
A promise must be in one of three states: pending, fulfilled, or rejected.When pending, a promise:may transition to either the fulfilled or rejected state.When fulfilled, a promise:must not transition to any other state.must have a value, which must not change.When rejected, a promise:must not transition to any other state.must have a reason, which must not change. Here, “must not change” means immutable identity (i.e. ===), but does not imply deep immutability.
翻译出中文就是说当状态从pending变成fulfiiled或者rejected后就已经锁定,无法回溯以及变更
Promise的thenable
Promise.then的用法想必大家不陌生,但是具体它里面有哪些隐含点,今天可以梳理下
1.then的参数不一定是回调函数,并且透传值
then的两个参数本身定义是resolvedFn和rejectedFn,但是它是可以传非函数形式的数据
var p = Promise.resolve('martin')
p.then(1).then((res) => {})
p.then(new Error('error')).then((res) => {})
以上例子最终执行结果res是1,我们可以发现当then处理了非函数形式的数据时,会把原来正确的PromiseResult直接传递给了下一个then(包括rejectedFn也是如此)
2. then返回的是一个新的Promise then返回的变量其实已经和调用then方法的promise不是一个promise了
var p = Promise.reject('martin')
var p1 = p.then()
p1 !== p
p1.PromiseStatus === 'fulfilled'
p.PromiseStatus === 'rejected'
以上例子中, p1是p调用then方法之后返回的变量,执行p1 !== p,结果是true,证明了then返回的不是一个promise
then must return a promise;
以上翻译就是将Promise.then回调函数必须是一个promise
3. then可以被一个Promise多次调用
上面说到then返回的是一个新的Promise,我们编写代码的时候通常都是链式调用,继承上一个Promise生成一个新的Promise,但是我们也可以使用变量缓存Promise,然后在该变量上注册若干个then回调函数
var p = Promise.resolve('martin')
p.then(() => {})
p.then(() => {})
这两个then方法注册在p上,所以p的状态会同时作用于这两个then方法上
4. then返回的状态
我们知道then方法返回的是一个新的Promise之后,但是它的状态是否会发生变化呢? 绝大部份情况下then返回的Promise都是fulfilled状态,但是只要then回调函数里抛出错误就会将新的Promise状态置为rejected
var p = Promise.reject('martin')
var p1 = p.then(() => {}) // 正常情况下是fulfilled
var p2 = p.then(() => { throw new Error('martin') }) // 此时就会变成rejected
var p3 = p.then(() => { return new Error('martin') }) // 只有throw才会变成rejected,此时还是fulfilled
Promise.catch
1. Promise.catch会rejected状态下的Promise
上面我们说到过多个then可以作用于同个promise,那么catch同样也可以
var p = Promise.reject('martin')
p.then(() => {}, function reject(){})
p.catch(e => e)
p.catch(e => e)
上面then的第二个回调函数reject和下面的两个catch都会执行,不知道大家此处是否会有疑问,因为之前有个逻辑其实是说当reject函数注册的时候catch不会执行,其实说的不太准确,then返回的是一个新的Promise,当发生错误的时候,如果上游的Promise没有拦截掉那么就会由下游的Promise进行兜底
var p = Promise.reject('martin')
var p2 = p.then(() =>{}).catch(function error(){})
var p3 = p.then(() =>{}, ()=>{}).catch(function error(){})
上面的代码执行后可以发现p2的catch回调函数执行而p3却没有,这是因为p3在注册catch的时候会被之前then注册的reject函数拦截掉
2. Promise.catch会劫持then回调函数发生错误的情况下
此处就不说了,then发生的错误就会被后面的catch劫持掉
3. Promise.catch返回的是Promise状态 Promise.catch返回的Promise状态判断与then一样
Promise.finally
1. Promise.finally状态跟随上一个Promise
Promise.finally的状态会跟随上一个Promise
var p = Promise.reject('martin')
var p1 = p.finally(() => {})
p1的状态会跟随这p的状态,所以此时是rejected
手写Promise
- 我们首先定义一个Promise的一个类,它有着状态、值两个私有值
class AWeSomePromise {
constructor() {
this.PromiseState = 'pending'
this.PromiseResult = void 0
}
}
- Promise实例化时传入的是一个回调函数,回调函数的两个参数分别是resolve与reject函数,resolve和reject函数主要做的是将值赋值为传入的值并且更改状态
class AWeSomePromise {
constructor(callback) {
this.PromiseState = 'pending'
this.PromiseResult = void 0
if (typeof callback === 'function') {
callback(this.resolve.bind(this), this.reject.bind(this))
}
}
resolve(resvalue) {
this.excuteStatus(resvalue, 'fulfilled')
}
reject(resvalue) {
this.excuteStatus(resvalue, 'rejected')
}
excuteStatus(promiseResult, promiseState) {
this.PromiseResult = promiseResult // 赋值
this.PromiseState = promiseState // 变更状态
triggerFn() // 触发thenable,具体实现看下面
}
}
-
在原型上定义一个then方法,在这里需要结合上面Promise.then说到的几个特性
- thenable返回的是一个新的Promise
then(resolveFn, rejectFn) { this.newPromise = new AWeSomePromise() // then返回的是一个新的Promise return this.newPromise }
- 第二个及后面thenable会根据之前的状态直接调用resolveFn或者rejectFn
then() { if (status !== 'pending') { this.excuteThen() } } excuteThen() { setTimeout(() => { if (this.PromiseState === 'fulfilled') this.triggerFn('fulfilled') else if (this.PromiseState === 'rejected') this.triggerFn('rejected') }, 10) }
- thenable接收的参数可以是非函数的数据,并且若干个thenable可以同时作用于一个Promise实例上
get resolveFns() { return [] } // 保存resolveFn get rejectFns() { return [] } // 同理 then(resolveFn, rejectFn) { const isResolveFn = typeof resolveFn === 'function' // 判断reslveFn是否是函数 const isRejectFn = typeof rejectFn === 'function' function excuteFn(thenStatus) { return function(fn, newPromise) { const type = thenStatus === 'fulfilled' ? isResolveFunction : isRejectFunction try { newPromise.PromiseState = status = 'fulfilled' if (this.PromiseState === thenStatus && type) { if (fn) { // 判断传入的resolveFn是否是函数形式 value = fn(this.PromiseResult) // value是全局的值,等下会讲到 if (typeof value === 'undefined') value = this.PromiseResult // 如果thenable里没有返回值,则会对值进行透传 newPromise.PromiseResult = value // 更新下一个Promise的值(当Promise是异步的时候) } else { value = this.PromiseResult } } if (thenStatus === 'rejected' && !type) { const errorCallbacks = newPromise.errorCallbacks this.triggerErrorCallback(errorCallbacks) // 触发catch回调函数 } } catch(err) { status = thenStatus value = err newPromise.PromiseState = status = 'fulfilled' setTimeout(() => { this.triggerErrorCallback(newPromise.errorCallbacks) }, 0) } this.triggerFn(null, newPromise.finallyCallbacks) // 触发finally回调函数 } } this.resolveFns.push(excuteFn('fulfilled').bind(this, resolveFn, this.newPromise)) this.rejectFns.push(excuteFn('rejected').bind(this, rejectFn, this.newPromise)) } triggerFn(thenStatus, fn) { // 循环触发resolveFns和rejectFns if (!fn) { fn = thenStatus === 'fulfilled' ? this.resolveFns : this.rejectFns } let currentFn = null while(currentFn = fn.shift()) { // 这里需要从resolveFns中取出最前面进入的回调函数执行并且踢出队列 if (currentFn) { currentFn() } } } // 此时需要更改下上一步resolve、reject、excuteStatus的操作 resolve(resvalue) { this.excuteStatus(resvalue, 'fulfilled', this.triggerFn.bind(this, 'fulfilled')) } reject(rejectvalue) { this.excuteStatus(rejectvalue, 'rejected', () => { this.triggerFn.bind(this, 'rejected') }) } excuteStatus(promiseResult, promiseStatus, cb) { ... setTimeout(() => { // 因为Promise.thenable是微任务,所以这里用setTimeout来模拟 if (cb) { cb() } this.triggerFn(this.finallyCallbacks) // 触发finallyCallbacks }, 0) }
- thenable状态和值透传 我们知道第二次及后面注册的thenable的状态不是由resolve和reject函数来更改的,所以这里需要在全局环境下定义几个值用于缓存上一个的状态
let status = 'pending' let value = void 0 class AWeSomePromise { constructor(callback) { this.PromiseState = status // 这里直接先赋值上一次的状态 this.PromiseResult = value // 这里直接先变更上一次的值 if (typeof callback === 'function') { callback(this.resolve.bind(this), this.reject.bind(this)) } } then() { function excuteFn(thenStatus) { ... return function(fn, newPromise) { try { newPromise.PromiseState = status = 'fulfilled' if (this.PromiseState === thenStatus && type) { if (fn) { // 判断传入的resolveFn是否是函数形式 value = fn(this.PromiseResult) // value是全局的值, 这里对上一个thenable返回的值进行缓存 if (typeof value === 'undefined') value = this.PromiseResult // 如果thenable里没有返回值,则会对值进行透传 newPromise.PromiseResult = value // 更新下一个Promise的值(当Promise是异步的时候) } else { value = this.PromiseResult } } } catch(err) { status = thenStatus value = err } } ... } } }
这里可能有点绕,我们理清下newPromise和Promise的关系
var p = new AWeSomePromise() var p1 = p.then() p.newPromise === p1
关系如下: Promise.newPromise === nextPromise 这样子thenable的基本原理已经开发完成,接下来看下catch函数的问题
-
catch函数它与thenable开发流程很相似,我们也可以根据它的特性来开发, 若干个catch可以同时作用于一个Promise实例, catch函数劫持thenable的错误以及reject函数未注册时触发rejected状态的Promise
reject() { this.excuteStatus(rejectvalue, 'rejected', () => { this.triggerErrorCallback() // 这里需要触发已经注册在当前Promise下的所有catch回调函数 this.triggerFn.bind(this, 'rejected') }) } get errorCallbacks() { return [] } catch(callback) { this.errorCallbacks.push(callback) return this.newPromise || (this.newPromise = new AWeSomePromise()) // catch之前可能没有注册thenable,所以需要处理下this.newPromise } triggerErrorCallback(errorCallbacks = this.errorCallbacks) { let currentFn = null while(currentFn = errorCallbacks.shift()) { if (currentFn) { currentFn(this.PromiseResult) } } } then() { function excute() { return function(fn, newPromise) { const type = thenStatus === 'fulfilled' ? isResolveFunction : isRejectFunction try{ if (thenStatus === 'rejected' && !type) { // 这里是触发reject函数没有劫持的情况下会去调用catch函数 const errorCallbacks = newPromise.errorCallbacks this.triggerErrorCallback(errorCallbacks) } } catch(err) { newPromise.PromiseState = status = 'fulfilled' setTimeout(() => { this.triggerErrorCallback(newPromise.errorCallbacks) }, 0) } } } }
-
finally函数无论resolve函数触发还是reject函数触发最终都会执行
excuteStatus(cb) {
setTimeout(() => {
if (cb) {
cb()
}
this.triggerFn(this.finallyCallbacks) // 这里是为了触发当前Promise下finally回调函数 promise.finally()
}, 0)
}
then() {
function excuteFn() {
return function() {
...
this.triggerFn(null, newPromise.finallyCallbacks) // 这里触发的是thenable后链式注册的回调函数 then().finally
}
}
}
- 处理异步, 这也是Promise的核心案例, 我们可以先看下异步的例子:
var p = new Promise(resolve => {
setTimeout(() => { resolve('martin') }, 1000)
})
p.then(() => {}).then(() => {})...
以上p.thenable函数及后面所有链式调用的thenable函数会在1000ms延迟后才会执行,根据我们上面写的第二个及后面的thenable函数在注册调用时就会去触发excuteThen函数,延迟10ms后会去触发resolveFns,但是以上案例明显需要再次去延迟1000ms之后才能触发,所以这里虽然我们不知道resolve函数会在延迟几秒之后才会执行,但是它执行的时候就代表可以去执行后续的thenable的回调函数,所以我们只需将thenable返回的Promise实例缓存到数组里,在resolve函数执行完后去检测当前promise的实例在数组中的下标值,并将后续添加进来的promise逐个触发resolveFns即可
let promiseArray = []
class AWeSomePromise {
excuteStatus() {
this.reconnect()
}
then() {
promiseArray.push(newPromise)
}
reconnect() {
if (promiseArray.length) {
const index = promiseArray.indexOf(this)
if (~index) {
promiseArray.slice(index).forEach(context => {
if (context instanceof AWeSomePromise) {
context.excuteThen()
}
})
}
}
}
}
这样就可以初步的完成Promise的模拟
完整代码
let status = 'pending'
let value = void 0
let promiseArray = []
class MyPromise {
constructor(callback) {
this.PromiseState = status
this.PromiseResult = value
this.resolveFns = []
this.rejectFns = []
this.errorCallbacks = []
this.finallyCallbacks = []
this.done = false
if (callback) {
callback(this.resolve.bind(this), this.reject.bind(this))
}
}
resolve(resvalue) {
this.excuteStatus(resvalue, 'fulfilled', this.triggerFn.bind(this, 'fulfilled'))
}
reject(rejectvalue) {
this.excuteStatus(rejectvalue, 'rejected', () => {
this.triggerErrorCallback()
this.triggerFn.bind(this, 'rejected')
})
}
excuteStatus(promiseResult, promiseStatus, cb) {
this.PromiseResult = value = promiseResult
status = promiseStatus
this.PromiseState = status
if (this.newPromise) {
this.newPromise.PromiseResult = promiseResult
this.newPromise.PromiseState = promiseStatus
}
this.reconnect()
setTimeout(() => {
if (cb) {
cb()
}
this.triggerFn(this.finallyCallbacks)
}, 0)
}
then(resolveFn, rejectFn) {
this.newPromise = new MyPromise()
const isResolveFunction = typeof resolveFn === 'function'
const isRejectFunction = typeof rejectFn === 'function'
function excuteFn(thenStatus) {
return function(fn, newPromise) {
const type = thenStatus === 'fulfilled' ? isResolveFunction : isRejectFunction
try {
newPromise.PromiseState = status = 'fulfilled'
if (this.PromiseState === thenStatus && type) {
if (fn) {
value = fn(this.PromiseResult)
newPromise.PromiseResult = value
} else {
value = this.PromiseResult
}
}
if (thenStatus === 'rejected' && !type) {
const errorCallbacks = newPromise.errorCallbacks
this.triggerErrorCallback(errorCallbacks)
}
} catch(err) {
status = thenStatus
value = err
newPromise.PromiseState = status = 'fulfilled'
setTimeout(() => {
this.triggerErrorCallback(newPromise.errorCallbacks)
}, 0)
}
this.triggerFn(null, newPromise.finallyCallbacks)
}
}
this.resolveFns.push(excuteFn('fulfilled').bind(this, resolveFn, this.newPromise))
this.rejectFns.push(excuteFn('rejected').bind(this, rejectFn, this.newPromise))
if (status !== 'pending') {
this.excuteThen()
}
promiseArray.push(this)
return this.newPromise
}
catch(callback) {
this.errorCallbacks.push(callback)
return this.newPromise || (this.newPromise = new MyPromise())
}
finally(callback) {
this.finallyCallbacks.push(callback)
return this.newPromise || (this.newPromise = new MyPromise())
}
reconnect() {
if (promiseArray.length) {
const index = promiseArray.indexOf(this)
if (~index) {
promiseArray.slice(index).forEach(context => {
if (context instanceof MyPromise) {
context.excuteThen()
}
})
}
}
}
triggerErrorCallback(errorCallbacks = this.errorCallbacks) {
let currentFn = null
while(currentFn = errorCallbacks.shift()) {
if (currentFn) {
currentFn(this.PromiseResult)
}
}
}
triggerFn(thenStatus, fn) {
if (!fn) {
fn = thenStatus === 'fulfilled' ? this.resolveFns : this.rejectFns
}
let currentFn = null
while(currentFn = fn.shift()) {
if (currentFn) {
currentFn()
}
}
}
excuteThen() {
setTimeout(() => {
if (this.PromiseState === 'fulfilled') this.triggerFn('fulfilled')
else if (this.PromiseState === 'rejected') this.triggerFn('rejected')
}, 10)
}
}