回顾
白话文,轻松理解Promise原理,并实现(一)
前文中已经将promise一步一步实现,当前这个版本虽然不符合A+规范,但是更易于我们理解原理。把上一版本的代码贴出来回顾以下。
class Promise {
// 回调收集
callbacks = []
// 返回值保存
value = null
// 错误原因
reason = null
// 状态管理
state = 'pending'
constructor (fn) {
// 最初实例化,传递resolve、和reject方法,异常处理
try {
fn(this._resolve.bind(this), this._reject.bind(this))
} catch (error) {
this._reject(error)
}
}
then (onFulfilled = null, onRejected = null) {
const me = this
return new Promise((resolve, reject) => {
// 将收集的回调封装成对象
me._handle({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
_handle (cbObj) {
// 首先判断状态
if (this.state === 'pending') {
this.callbacks.push(cbObj)
return
}
// 依据状态获取用户回调
const cb = this.state === 'fulfilled' ? cbObj.onFulfilled : cbObj.onRejected
const stateF = this.state === 'fulfilled' ? cbObj.resolve : cbObj.reject
// 如果没有传递回调函数
if (!cb) {
stateF(this.state === 'fulfilled' ? this.value : this.reason)
return
}
// 异常处理
try {
const val = cb(this.state === 'fulfilled' ? this.value : this.reason)
// 注意,这里我们先用内置函数的resolve执行
cbObj.resolve(val)
} catch (error) {
cbObj.reject(error)
}
}
_resolve (val) {
const me = this
// 限制状态转化
if (me.state !== 'pending') return
// 判断结果值是否是当前promise
if (me === val) {
return me._reject(new TypeError("cannot return the same promise object from onfulfilled or on rejected callback."))
}
// 判断val是否是promise对象,有没有then方法可以挂载
if (val && (typeof val === 'object' || typeof val === 'function')) {
const valThen = val.then
if (valThen && typeof valThen === 'function') {
// 保持this指向正确,如果不绑定,此时valThen中的this会指向内置promise,而不是用户自己的promise
// resolve和reject也需要绑定this
valThen.call(val, me._resolve.bind(me), me._reject.bind(me))
return
}
}
setTimeout(()=>{
me.value = val
me.state = 'fulfilled'
me._execute()
})
}
_reject (reason) {
const me = this
// 限制状态转化
if (me.state !== 'pending') return
setTimeout(()=>{
me.reason = reason
me.state = 'rejected'
me._execute()
})
}
_execute () {
const me = this
me.callbacks.forEach(cbObj => {
me._handle(cbObj)
})
}
}
可以看出,对于使用者而言,我们的promise对象只提供了一个then方法去注册成功回调和失败回调。但往往还不足以应付平日开发的习惯。
这篇文章便会补充完整其他的promise原型方法和静态方法,最后再补充一些对promise封装的实用函数。
👌开始
catch
日常使用请求方法获取后端数据时,往往会这么写
// 业务场景
function getData () {
return new Promise((resolve) => {
http.$get(url, (result) => {
resolve(result)
})
})
}
getData().then((data) => {
// 成功获取数据,进行业务处理
console.log(data)
}).catch((error)=>{
// 请求异常,进行提示或处理
console.log(error)
})
其实我们只需要将catch里传入的回调函数收集起来就可以。它相当于我们在then中注册失败回调。
catch (onRejected) {
return this.then(null,onRejected)
}
这里我们有几点需要非常留意的⚠️⚠️
Promise.prototype.catch() 阮一峰对catch的分析,我们需要仔细阅读。
这里我提一些很明显的。先看代码案例:
const promise = new Promise(function (resolve, reject) {
reject('err')
}).then((res) => {
console.log('成功回调', res)
}, (err) => {
console.log('失败回调', err)
}).catch((err) => {
console.log('最后结果', err)
})
// 输出
// 失败回调 err
// undefined
const promise = new Promise(function (resolve, reject) {
reject('err')
}).then((res) => {
console.log('成功回调', res)
}, (err) => {
console.log('失败回调', err)
}).then((res) => {
console.log('最后结果——成功', res)
})
// 输出
// 失败回调 err
// 最后结果——成功 undifined
由此我们可以总结几点注意事项
- promise链式报错错误后,只要经过错误处理。promise链的状态会变成fulfilled。
- **注册错误回调时尽力在catch中注册,**这种写法可以捕获前面
then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch()方法,而不使用then()方法的第二个参数。
为什么推荐用catch?没明白看👇👇
const promise = new Promise(function (resolve, reject) {
reject('err')
}).then((res) => {
console.log('成功回调', res)
}).catch((err)=>{
console.log('捕获主promise错误', err)
})
const promise = new Promise(function (resolve, reject) {
resolve('success')
}).then((res) => {
console.log('成功回调', res)
reject('err')
}).catch((err)=>{
console.log('捕获then中错误', err)
})
为什么一旦错误收集后我们的promise链状态就会变成fulfilled呢?之前在第一篇时,我没写上。因为这边是属于规范的一部分,完整代码在github中。这里我贴图表示:
接着我们只需要关注Promise._pResolve(cbObj,'err',cbObj.p)中是怎么处理。
从这种情况中,我们可以发现,如果我们处理了错误。它会将内置promise的状态改变为fulfilled。
finally
回想一个业务场景,此时我们需要请求获取一个列表数据。在数据返回前,我们需要显示loading图,数据返回成功或者失败时,隐藏loading图。
let loading = true
// 通过finally,我们可以这么写
getData().then((data) => {
// 成功获取数据,进行业务处理
console.log(data)
}).catch((error)=>{
// 请求异常,进行提示或处理
console.log(error)
}).finally(()=>{
loading = false
})
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
第一感觉那我们只要在继续通过then注册就好了。
finally (done) {
return this.then(done, done)
}
这样的确是能实现我们需要的效果。但是根据规范,这样的写法,还是过于简单,不够满足。
- finally中的回调函数不需要接收参数,也不需要考虑promise的状态是否成功。
- finally最终还需要延续链式调用,以及状态,执行回调后,继续返回promise成功的值或失败的原因。
因此根据规范,看了阮一峰的promise对象分析,代码修改如下。
finally (done) {
// 此时的this指向当前promise实例,但是由于后续的resolve属于Promise静态方法
// 所以需要获取构造函数,当然如果不存在别的promise标准,直接Promise也可以
const p = this.constructor
return this.then(
val => p.resolve(done()).then(()=>val),
reason => P.resolve(callback()).then(() => { throw reason })
)
}
静态方法Promise.resolve
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上
static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
有时,我们需要直接将现有对象转化成promise对象时,就可以用到这个方法。
return new Promise((resolve,reject)=>{
resolve(obj)
})
Promise.resolve(obj)
// 效果是一样的,都返回一个promise对象
注意的是对于resolve,我们需要处理4种情况。
- resolve参数是一个promise实例
这种情况下,我们直接返回参数(promise实例)即可,不做修改。
- 参数是一个thenable对象
即该参数是个具有then方法的对象,我们需要把这个对象转化成promise对象,并立即执行它的then方法(promise的状态可能不一定是fulfilled,得根据参数then的执行改变)。
let thenable = { then: function (resolve, reject) { resolve(1) }} // thenable对象
- 参数不是具有then方法的对象,或者不是对象,如字符串,数字等等。
则我们需要返回一个fulfilled状态的promise,并将参数设置为返回值。
- 没有传递参数时,我们直接返回一个fulfilled状态的promise。
👌,理解了需求我们直接上代码。
static resolve(val){
if (val && val instanceof Promise) {
// 如果参数是promise对象,我们直接返回
return val
} else if (val && (typeof val === 'object' || typeof val === 'function')) {
// 如果参数是对象,则判断有没有then方法,并新建个promise对象包裹
try {
const valThen = val.then
if (valThen && typeof valThen === 'function') {
return new Promise((resolve, reject) => {
// 由参数then的直接决定promise状态
valThen.call(val, resolve, reject)
})
} else return new Promise(resolve => resolve(val))
} catch (error) {
// 异常处理
return new Promise((resolve, reject) => reject(error))
}
} else if (val !== void 0) {
// 参数是不是具有then方法的数据,普通参数
return new Promise(resolve => resolve(val))
// 接着就是没有传递参数的情况
} else return new Promise(resolve => resolve())
}
静态方法Promise.reject
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。 Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。
我们返回的promise只可能是rejected状态,无论参数是promise对象还是thenable,我们也只处理reject。其他情况参数将作为reject的理由。
static reject(val){
// 是否是promise对象或者包含then方法的对象
if (val && (typeof val === 'object' || typeof val === 'function')) {
try {
const valThen = val.then
if (valThen && typeof valThen === 'function') {
return new Promise((resolve, reject) => {
// 只处理reject
valThen.call(val, null, reject)
})
} else return new Promise((resolve, reject) => reject(val))
} catch (error) {
return new Promise((resolve, reject) => reject(error))
}
// 其他情况直接将val作为错误原因
} else return new Promise((resolve, reject) => reject(val))
}
静态方法Promise.all
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。 Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
const p = Promise.all([p1, p2, p3])
p也会是一个promise实例,它的状态更具两种情形决定:
-
p1, p2, p3的最终状态全部变成fulfilled,p的状态才会变成fulfilled。此时返回值是由p1, p2, p3的返回值组成的数组,传递给p实例的resolve。
-
p1, p2, p3中只要有一个最终状态变为rejected,p的状态就会变成rejected。此时的返回值就是第一个reject的promise的原因,并传递给p实例的reject。
白话文:全部成功就resolve,返回值变成promise数组,顺序和参数顺序一致。有一个失败就reject。
static all(iterator){
let count = 0
// 将iterator转化成数组
const arr = Array.from(iterator)
const len = arr.length
// 结果
const result = []
return new Promise((resolve, reject) => {
for (let i = 0; i < len; i++) {
const itemP = arr[i]
Promise.resolve(itemP).then((res) => {
count++
result[i] = res
// 所有promise都返回成功状态,并将返回值保存。最后执行resolve
if (count === len) {
resolve(result)
}
}).catch(err => {
// 错误处理
reject(err)
break
})
}
})
}
注意⚠️⚠️
如果我们传入的数组promise中有catch处理。而且这个promise,报错了。但是由于被catch处理,它最终的状态还是成功。promise.all也会正确处理整个数组。原理同catch中介绍的一样。
静态方法Promise.race
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
同样参数必须是有Iterator 接口。其中谁先改变状态,就返回谁,就像赛跑一样。很容易理解。
static race(iterator){
// 将iterator转化成数组
const arr = Array.from(iterator)
return new Promise((resolve, reject) => {
for (let i = 0; i < arr.length; i++) {
const itemP = arr[i]
Promise.resolve(itemP).then((result) => {
resolve(result)
break
}).catch(err => {
// 错误处理
reject(err)
break
})
}
})
}
promise原理篇完结
至此promise原理的介绍以及代码实现我们都一步步完成。关键的知识点
- 观察者模式收集回调
- 链式调用(then方法)
- promise.resolve、 catch的理解
这些概念和运用,多看几次,便能加深印象。同样我自己也是会反复阅读,查阅资料。才学会总结,为了让记忆保持,自己也会反复阅读写的文章。
promise扩展篇(面试时常遇到的手写题)
在面试的过程中,不光是会被问到promise原理,有可能会让你手写promise,当然如果你认真阅读了之前的内容,现在手写promise也是没问题的。还有一些情况,让你依据业务封装promise。接下来我便介绍几个场景,附上代码和讲解。
情形一:promise.retry 错误重复请求
promise.retry 的作用是执行一个函数,如果不成功最多可以尝试 times 次。传参需要三个变量,所要执行的函数,尝试的次数以及延迟的时间。
利用闭包保持重试次数
function retry (fn, times, delay) {
let time = times
// 返回一个promise,关键在于reject的执行
return new Promise((resolve, reject) => {
function next () {
time -= 1
console.log('执行',time)
// 对fn注册成功回调,一旦成功直接resolve,
// 如果失败则需要判断失败次数,并加上延迟执行下一次请求
Promise.resolve(fn()).then((res) => resolve(res)).catch((err) => {
console.log('拒绝')
if (time > 0) {
setTimeout(() => {
next()
}, delay)
} else {
reject(err)
}
})
}
next()
})
}
// 测试代码
const ajaxF = function (time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(1)
}, 1000)
})
}
retry(ajaxF, 3, 1000).then(() => { console.log('成功') }, (err) => { console.log(err) })
情形二:控制promise最大并发数
这个情形,我忘记从哪篇文章看到的,描述如下。
微信小程序最一开始对并发数限制为5个,后来升级到10个,如果超过10个会被舍弃。后来微信小程序升级为不限制并发请求,但超过10个会排队机制。也就是当同时调用的请求超过 10 个时,小程序会先发起 10 个并发请求,超过 10 个的部分按调用顺序进行排队,当前一个请求完成时,再发送队列中的下一个请求。
梳理完需求,我们可以明确三点。
- 我们需要个任务队列,收集请求。
- 我们需要在执行请求前,确认当前有多少任务在执行,是否超出最大限制。如果超出限制,则不执行。
- 在每一个任务执行结束,不管成功失败。我们再判断上一点,然后再依次获取队列任务执行。
👌上代码
class Queue {
constructor(max) {
// 最大并发
this.max = max
// 任务队列
this.taskList = []
// 当前正在异步请求的数目
this.asyncNum = 0
}
add (task) {
// 添加任务
this.taskList.push(task)
this.run()
}
run () {
const me = this
// 判断当前队列长度和最大并发数,取最小值
let len = Math.min(me.max, me.taskList.length)
// 再结合当前正在请求的数量,获取剩余坑位
len = len > (me.max - me.asyncNum) ? (me.max - me.asyncNum) : len
for (let i = 0; i < len; i++) {
// 任务出队列
let task = me.taskList.shift()
// 数量增加
me.asyncNum++
// 执行任务获取promise对象并设置finall
task().finally(() => {
me.asyncNum--
me.run()
})
}
}
}
const ajaxF = function (time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1)
}, 1000)
}).then((res) => {
console.log(res)
})
}
const q = new Queue(2)
for (let i = 0; i < 10; i++) {
q.add(ajaxF)
}
代码和测试例子如上,可以先看理解,自己再执行,印象更深。