本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
本文会随时有更新,更新会在标题上体现日期,方便查看。收藏专栏或文章不怕迷路噢~
ECMA-262 规范 镇楼 🙏🏻 ,祝看到此文的小伙伴快乐暴富~
学习资料
📌 Promise/A+原文
📌 Promise MDN原文
重中之重的A+前言 💍
一个 promise 代表一个异步操作的最终结果。与 promise 交互最主要的方式就是通过它的 then 方法, then 方法通过注册回调函数来获取另一个 promise 的最终结果或未完成的原因。
该规范详细说明了 then 方法的行为,提供了一个可交互的基础,所有符合 promise 实现的 Promise/A+ 标准都可以依赖该基础来提供。
废话不多说,直接上手写Promise实现 👊🏻
有一定基础的小伙伴可以直接看代码,不想赘述太多文字,实现细节上直接用注释说明了。有问题欢迎留言~
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
const isFunction = (fn) => typeof fn === 'function'
class MPromise {
// 一个promise实例可以被多次调用promise.then(),promise.then()...
// 需要一个list存放当前promise实例注册的回调函数们,promise状态发生变化时,所有回调顺序执行
fulfilledCallbacks = []
rejectedCallbacks = []
// promise有三个状态 初始状态pending 我们使用getter,setter的特性实现了status trigger触发器的效果,
// 为避免set和get循环调用 所以需要借一个中转值存储status
_status = PENDING
// 构造函数调用方式 new Promise(function(resolve, reject) {...})
constructor(executor) {
// 术语之一 表示promise的执行结果
this.value = undefined
// 术语之一 表示promise的拒绝原因
this.reason = undefined
try {
// new Promise(function(resolve, reject) {...})
// executor是外部传入的方法,resolve和rejected作为普通函数简单调用
// this会指向全局对象,故手动绑定this
executor(this.resolve.bind(this), this.reject.bind(this))
} catch (error) {
this.reject(error)
}
}
// status setter: 充当状态trigger,执行绑定的回调函数们
set status(status) {
this._status = status
switch (this.status) {
case FULFILLED: {
this.fulfilledCallbacks.forEach((callback) => {
callback(this.value)
})
break
}
case REJECTED: {
this.rejectedCallbacks.forEach((callback) => {
callback(this.reason)
})
break
}
default:
break
}
}
// 状态 getter
get status() {
return this._status
}
// Promise的行为之一 改变状态为已完成
// resolve(value) => PENDING -> FULFILLED
resolve(value) {
// 只有 PENDING 状态可以修改状态
if (this.status === PENDING) {
// 因为status setter中有用到value,所以先设置value在设置status
this.value = value
this.status = FULFILLED
}
}
// Promise的行为之一 改变状态为已拒绝
// reject(reason) => PENDING -> REJECTED
reject(reason) {
// 只有 PENDING 状态可以修改状态
if (this.status === PENDING) {
// 因为status setter中有用到reason,所以先设置reason在设置status
this.reason = reason
this.status = REJECTED
}
}
// 解析promise,改变promise的状态触发下一个callback
// 每一轮then方法返回的promise都要经过resolvePromise的解析改变状态,才能触发下一个callback
resolvePromise(newPromise, callbackResult, resolve, reject) {
// case1: newPromise和callbackResult不能是同一个对象
// 程序设计严谨性 防止循环引用
if (newPromise === callbackResult) {
return reject(
new TypeError('The promise and the return value are the same')
)
}
if (callbackResult instanceof MPromise) {
// case2: callbackResult是一个promise
// 回调函数返回的是一个promise,先将promise执行(即调用.then执行),如果是fulfilled,
// 拿到它的value继续再和newPromise做解析,如果是rejected则调用reject
// todo: 是否要加一个状态判断
callbackResult.then((value) => {
// 继续调用resolvePromise做解析
this.resolvePromise(newPromise, value, resolve, reject)
}, reject)
} else if (
typeof callbackResult === 'object' ||
isFunction(callbackResult)
) {
// case3: callbackResult是对象或者函数
// 因为then方法应该返回一个promise,根据规范把拥有then方法的对象或者函数称之为promise
// 那么callbackResult应该要有一个then方法,如果没有则reject newPromise
// 边界情况 null也是object
if (callbackResult === null) {
return resolve(callbackResult)
}
try {
let then = callbackResult.then
// 如果then是一个可执行的东西
if (isFunction(then)) {
let called = false
// 调用then方法传递callback执行结果,继续和newPromise做解析
then.call(
callbackResult,
(value) => {
// 继续调用resolvePromise做解析
// 按照规范 onFulfilled 和 onRejected 都只能调用一次,
// 回调函数的执行会改变当前promise的状态以触发下一个then方法的回调函数,所以不能重复调用。
// 需要手动设置一个标记判断是否被调用过,非初次调用的话直接忽略(value也不会被传递)。
if (called) return
called = true
this.resolvePromise(newPromise, value, resolve, reject)
},
(reason) => {
if (called) return
called = true
reject(reason)
}
)
} else {
// 非可执行对象 直接传下去
return resolve(callbackResult)
}
} catch (error) {
return reject(error)
}
} else {
// case4: 以上三种都不是
// callbackResult直接传递给下一个then
return resolve(callbackResult)
}
}
// 立即注册,延迟回调
then(onFulfilled, onRejected) {
const parent = this
// 参数过滤
const onFulfilledFn = isFunction(onFulfilled) ?
onFulfilled :
(value) => {
// 如果onFulfilled不是一个函数,那么手动塞一个函数(原因:将上一个promise的value传递下去)
return value
}
const onRejectedFn = isFunction(onRejected) ?
onRejected :
(reason) => {
// 如果onRejected不是一个函数,那么手动塞一个函数(原因:将上一个promise的reason throw出去)
// throw的原因时因为当前onRejected不是个函数,直接用抛错处理
throw reason
}
// then应该是一个promise(promise链继续thenable)
// 为什么要手动返回一个新的promise呢?是因为这样无论then方法注册的回调函数执行结果是什么,
// 都可以保证返回的是一个promise,并且是新的promise,
// 根据回调函数执行结果对newPromise做对应的resolve/reject,触发下一个.then对应的callback(如果有下一个的话)
// 既然我们手动返回了一个newPromise,那么这个newPromise改变状态的逻辑
// 和then方法上callbacks的执行结果之间就存在一个关系,我们定义一个用于解析两者关系的函数,
// 通过该函数定义newPromise的状态根据callback的执行结果是怎样流转的,从而进入下一个.then。
const newPromise = new MPromise((resolve, reject) => {
const self = this
// 这里使用箭头函数,就不用改this指向了
// 重新包一个fulfilled函数,函数体内执行成功回调函数,再调用resolvePromise解析newPromise
const _fulfilledFn = () => {
queueMicrotask(() => {
/* 微任务中将运行的代码 */
try {
// FULFILLED状态执行onFulfilled
const returnValue = onFulfilledFn(parent.value)
// 解析newPromise和onFulfilledFn执行结果
this.resolvePromise(self, returnValue, resolve, reject)
} catch (error) {
reject(error)
}
})
}
// rejected函数和fulfilled函数做同样处理
const _rejectedFn = () => {
queueMicrotask(() => {
/* 微任务中将运行的代码 */
try {
// REJECTED状态执行onRejected
const returnValue = onRejectedFn(parent.reason)
// 解析newPromise和onRejectedFn执行结果
this.resolvePromise(self, returnValue, resolve, reject)
} catch (error) {
reject(error)
}
})
}
// 根据上一个promise状态,决定newPromise执行什么回调
switch (parent.status) {
case FULFILLED: {
// 同步 resolve
_fulfilledFn()
break
}
case REJECTED: {
// 同步 reject
_rejectedFn()
break
}
case PENDING: {
// 异步任务 先收集注册的回调 监听状态变化批量顺序执行回调
// 这样在当前promise状态变化时(trigger)才能一次性执行所有绑定的回调函数
parent.fulfilledCallbacks.push(_fulfilledFn)
parent.rejectedCallbacks.push(_rejectedFn)
break
}
default:
break
}
})
return newPromise
}
catch (onRejected) {
return this.then(null, onRejected)
} finally(onFinally) {
return Promise.resolve(onFinally)
}
// 静态方法 创建一个新的resolved的promise
static resolve(value) {
if (value instanceof MPromise) {
return value
}
return new MPromise(function(resolve, reject) {
resolve(value)
})
}
// 静态方法 创建一个新的rejected的promise
static reject(reason) {
return new MPromise(function(resolve, reject) {
reject(reason)
})
}
// [race用法参考MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/race)
// race的使用场景:同时做了两个操作想知道哪个操作先做完
static race(promiseList) {
return new MPromise((resolve, reject) => {
if (!promiseList) {
return reject(new TypeError('undefined is not iterable'))
}
if (promiseList.length === 0) {
return resolve()
}
// promiseList中任何一个promise is settled(处理过了的),则resolve/reject当前promise
promiseList.forEach((promise) => {
// Promise.resolve将非promise对象包装成promise返回,promise对象直接返回
MPromise.resolve(promise).then(
(value) => {
resolve(value)
},
(reason) => {
reject(reason)
}
)
})
})
}
// [all用法参考MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)
// all的使用场景:并发请求
static all(promiseList) {
return new MPromise((resolve, reject) => {
if (!promiseList) {
return reject(new TypeError('undefined is not iterable'))
}
const taskLen = promiseList.length
const resolvedValueList = []
if (taskLen === 0) {
return resolve([])
}
// 当settled promise个数和promiseList个数相等,证明全部执行完成
for (let i = 0; i < promiseList.length; i++) {
MPromise.resolve(promiseList[i])
.then((value) => {
// push into resolvedValueList
resolvedValueList[i] = value
})
.catch((e) => {
// catch 第一个error message or rejected promise reason
// 如果有遇到onRejected或者异常的promise,立即reject(每个promise的reject只会被执行一次,即使重复执行只有第一次生效)
reject(e)
})
}
if (resolvedValueList.length === taskLen) {
return resolve(resolvedValueList)
}
})
}
// [allSettled用法参考MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled)
static allSettled(promiseList) {
return new MPromise((resolve, reject) => {
// 存储返回值List
const returnValueList = []
for (let i = 0; i < promiseList.length; i++) {
MPromise.resolve(promiseList[i])
.then(
(value) => {
returnValueList[i] = {
status: FULFILLED,
value
}
},
(reason) => {
returnValueList[i] = {
status: REJECTED,
reason
}
}
)
.finally(() => {
// 在promise结束时,都会被执行的回调
if (returnValueList.length === promiseList.length) {
resolve(returnValueList)
}
})
}
})
}
}
export default MPromise
✍🏻:完整代码已上传仓库 需要可查阅 >
经历手写实现之后get的一些新知识 🤙🏻 @0722
-
Promise链的每一层(then、catch)都会返回一个新生成的promise对象。实话说我原来一直以为是一个promise一直向后传递... YY能力🤯
-
每个注册在
then方法上的回调函数是否执行只和它上一个promise状态相关。 -
catch是通过调用then实现的。 -
then方法如果设置了第二个参数 - 已拒绝状态的回调,那么当前then方法return的promise状态会变成fulfilled状态 - 即已完成,catch方法的回调不会被执行。 -
promise的状态只会被修改一次,一旦状态变成最终态,无论再调用resolve或reject都不会改变promise状态。 -
当「前一个回调函数返回
Promise对象状态」为 (@0722更新)pending: 之后的
then方法没有被执行如果前一个
Promise对象状态是pending,那么下一个then方法不会被执行,直到上一个Promise对象不是pending状态为止。✅fulfilled:之后的
then方法执行onFulfilled逻辑如果前一个
Promise对象状态时fulfilled,那么下一个then方法也会执行onFulfilled回调,并传递上一个promise的值(value)。✅rejected:之后的
then方法执行onRejected逻辑如果前一个
Promise对象状态时rejected,那么下一个then方法也会执行onRejected回调,并传递上一个promise的拒绝原因(reason)。✅这便是
Promise Resolution Procedure规范的其中一种场景,这样的规范使得无论是fulfilled还是rejected(非pending状态),Promise链可以一直传递下去不会被终断。📌 上述结论对应
Promise/A+规范原文 2.3.2部分 >
做题检测 🏅
1. 用promise实现链式执行(上一个settled下一个才能开始)
const p1 = new Promise((resolve, reject) => {
console.log(111)
resolve(111)
})
const p2 = new Promise((resolve, reject) => {
console.log(222)
resolve(222)
})
const p3 = new Promise((resolve, reject) => {
console.log(333)
resolve(333)
})
const promiseList = [p1, p2, p3]
/**
* reduce 的用法
* [].reduce(callbackfn, initialValue)
*
* callbackfn 例子
* function handler(total, currentItem) {
* return total + currentItem
* }
*/
const runAsChain = (promiseList) => {
console.log('runAsChain running...')
// 借用reduce将promise数组连起来\
return Array.from(promiseList).reduce((previousPromise, currentPromise) => {
return previousPromise.then(() => {
return currentPromise
})
}, Promise.resolve())
}
runAsChain(promiseList)
我第一次是这么写的,发现在我打印 runAsChain running... 之前我 promise 中的 log 已经被打印了。观察了一下我的实现,注意到我在定义 promiseList 的时候,我是直接用 new Promise() 赋给了一个变量,根据之前手写 promise 的实践, Promise 构造函数接收的参数( executor 函数),是在实例化的时就会被调用的。也就是只要我使用了 new Promise() ,那传入的函数一定立即就会被执行。
改写一下实现,首先将赋值改成为一个函数返回,避免手动写好几个 promise ,我们直接包一个生成 promise 的函数。
const promiseCreator = (n) => {
return () =>
new Promise((resolve, reject) => {
// 为了体现时间差,设置了一个定时器,与上一个回调间隔n秒
setTimeout(() => {
console.log(n, new Date().toTimeString())
resolve(n)
}, n * 1000)
})
}
const promiseList = [promiseCreator(1), promiseCreator(2), promiseCreator(3)]
/**
* reduce 的用法
* [].reduce(callbackfn, initialValue)
*
* callbackfn 例子
* function handler(total, currentItem) {
* return total + currentItem
* }
*/
const runAsChain = (promiseList) => {
console.log('runAsChain running...', new Date().toTimeString())
// 借用reduce将promise数组连起来
return Array.from(promiseList).reduce((previousPromise, currentPromise) => {
return previousPromise.then(() => currentPromise())
}, Promise.resolve())
}
// 调用
runAsChain(promiseList)
打印如下:
runAsChain running... 20:52:26 GMT+0800 (China Standard Time)
1 20:52:27 GMT+0800 (China Standard Time)
2 20:52:29 GMT+0800 (China Standard Time) // 与上一个任务间隔2秒
3 20:52:32 GMT+0800 (China Standard Time) // 与上一个任务间隔3秒
[Done] exited with code=0 in 6.105 seconds
2.promise的cancel
// 待补充
3. 封装一个带有超时处理的fetch(可借用promise做状态管理)
function fetchWizTimeout(...fetchApiProps, time) {
return new Promise((resolve, reject) => {
// 执行fetch逻辑,请求成功则resolve当前promise,失败则reject promise
fetch(fetchApiProps).then(resolve).catch(reject)
// 因为new Promise里面代码是同步的,fetch请求会被立即发出
// 该题的实现关键是promise的状态只会被修改一次
// 如果在定时器响应之前请求已完成,则定时器设定的reject也不会生效
setTimeout(reject, time)
})
}
划重点:该题的实现关键是promise的状态只会被修改一次 !
4. 以下代码输出promise对象的状态是什么
const test = new Promise((resolve, reject) => {
setTimeout(() => {
reject(111)
}, 100)
}).catch((reason) => {
console.log('error:', reason)
console.log(test) // ?:此时test状态是什么
})
setTimeout(() => {
console.log(test) // ?:此时test状态是什么
}, 300)
输出结果:
error: 111
Promise { <pending> } // 100 setTimeout 的输出 pending
Promise { undefined } // 300 setTimeout 的输出 fulfilled
ps:第三行 Promise { undefined } 就是指 fulfilled 状态且 value = undefined
该题重点是要清楚 promise链 上的每一层都会生成一个新的 promise ,而题目中打印的 test 都是指 promise().then() 返回的 promise 状态。
在 then 方法中 100 setTimeout 打印的时候,当前 promise 还没执行完毕(并且没有调用任何 resolve/reject ),所以是 pending 状态。
在 300 setTimeout 打印的时候, test 执行完毕,并且虽然 promise链 中有 reject ,但是有设置 catch , catch 相当于将异常拦截=>恢复正常=>可以继续链式下去。 catch 实现也是调用 then 方法,会返回一个新生成的 promise 对象,并且我们在 catch 方法中没有设置返回值,根据规范相当于 resolve(null) ,所以就是 fulfilled 状态。
5. 写一个 promise 重试函数,可以设置时间间隔和次数 function foo(fn, interval, times) {} @0722
手写题的精髓就在“按题干拆解步骤,按步骤确定方案”,这样你会发现题目可能没有描述的那么复杂,也不容易自乱阵脚。按照这个方法论我们来做下这道题:
➡️ catch error 可以自动 retry
➡️ catch error x秒后自动 retry
➡️ 自动 retry n次后不再 retry
function foo(fn, internal, times) {}
6. 封装一个通用的异步函数超时逻辑(超过times则reject)
// todo
Promise.all 如何保证一定会走then方法(@0805)
通常我们会有掉n个请求之后,执行xxx的业务场景,此时我们就可以使用Promise.all函数,Promise.all接收一组Promise,批量执行后,依旧返回一个promise,可以支持继续链式调用,并传递一组promise的执行结果,前提是这一组promise都是fullfilled状态才会返回。
那么这个问题就是在问如何保证error时不打断Promise链。按照A+规范及上述手写实现,我们可以很快理解,因为promise的内部原理就是没有个then方法都要返回一个promise,包括catch方法内部也是调用then实现继续可链的特性(具体可以查看上述手写实现中then方法和resolvePromise这两块)。无特别情况(手动设置返回Promise状态、返回特殊对象object or function),只要我们写了失败回调或是catch拦截的处理,Promise内部就会帮我们返回一个resolve的promise,也就可以继续走下一个then方法的成功回调。
那么只要我们Promise.all([promise1.catch(...), promise2.catch(...)]).then(...) 类似这样每一个promise都有失败回调或catch拦截处理,我们的异常就不会影响Promise.all的向后传递。
这是最基本的处理方法,但是我们会发现一旦批量执行的promise多了,这每一个promise都要加一个catch会相当的费力。于是Promise又提供了一个静态方法allSettled,这个方法和Promise.all用途差不多,但链式状态不会受被执行的promise影响,它只要所有promise状态发生改变之后,就会触发allSettled返回的promise状态,也就是下一个then方法的执行。allSettled会将这组promise执行结果以{status: xx, value: xx}这样的对象返回给下一个回调。这就很符合需要批量执行promise,且就算遇到失败也不影响执行回调里的逻辑的场景。
什么是发布订阅 @0720
// todo
async/await 原理(generator 协程)@0721
// todo
// 更多题目待补充...
THE END