手写 promise
Promises/A+
一个开放、健全且通用的 JavaScript Promise 标准。由开发者制定,供开发者参考
promise表示一个异步操作的最终结果,与之进行交互的方式主要是 then方法,该方法注册了两个回调函数,用于接收 promise 的终值或本 promise 不能完成的原因。
本规范详细列出了 then 方法的完成过程,所有遵循 Promises/A+ 规范实现的 promise 均可以本标准作为参照基础来实施 then 方法。因而本规范是十分稳定的。尽管 Promise/A+ 组织有时可能会修订本规范,但主要是为了处理一些特殊的边界情况,且这些改动都是微小且向下兼容的。如果我们要进行大规模不兼容的更新,我们一定会在事先进行谨慎地考虑、详尽的探讨和严格的测试。
从历史上说,本规范实际上是把之前 Promise/A 规范 中的建议明确成为了行为标准:我们一方面扩展了原有规范约定俗成的行为,一方面删减了原规范的一些特例情况和有问题的部分。
最后,核心的 Promises/A+ 规范不涉及如何创建、解决和拒绝 promise,而是专注于提供一个通用的 then 方法。上述对于 promise 的操作方法将来在其他规范中可能会提及。
1. 术语
- “promise” 是一个拥有
then方法的对象或函数,其行为符合本规范; - “thenable”是一个定义了
then方法的对象或函数,文中译作“拥有 then 方法”; - “value”指任何 JavaScript 的合法值(包括
undefined,thenable和promise); - “exception”是使用
throw语句抛出的一个值。 - “reason”表示一个
promise的拒绝原因。
2. 要求
2.1 promise 的状态
一个 promise 的当前状态必须为以下三种状态中的一种:等待态(Pending)、完成态(Fulfilled)和拒绝态(Rejected)。
2.1.1 处于等待态时,promise 需满足以下条件:
2.1.1.1 可以迁移至完成态或拒绝态
2.1.2 处于完成态时,promise 需满足以下条件:
2.1.2.1 不能迁移至其他任何状态
2.1.2.2 必须拥有一个不可变的终值
2.1.3 处于拒绝态时,promise 需满足以下条件:
2.1.3.1 不能迁移至其他任何状态
2.1.3.2 必须拥有一个不可变的拒绝原因
这里的不可变指的是恒等(即可用 ===判断相等),而不是意味着更深层次的不可变
2.2 Then 方法
一个 promise 必须提供一个 then 方法以访问其当前值、终值和拒绝原因。
promise 的 then方法接受两个参数:
promise.then(onFulfilled, onRejected)2.2.1 onFulfilled 和 onRejected 都是可选参数。
2.2.1.1 如果 onFulfilled 不是函数,其必须被忽略
2.2.1.2 如果 onRejected 不是函数,其必须被忽略
2.2.2 如果 onFulfilled 是函数:
2.2.2.1 当 promise 完成结束后其必须被调用,其第一个参数为 promise 的终值
2.2.2.2 在 promise 完成结束前其不可被调用
2.2.2.3 其调用次数不可超过一次
2.2.3 如果 onRejected 是函数:
2.2.3.1 当 promise 被拒绝完成后其必须被调用,其第一个参数为 promise 的拒绝原因
2.2.3.2 在 promise 被拒绝完成前其不可被调用
2.2.3.3 其调用次数不可超过一次
2.2.4 onFulfilled 和 onRejected 只有在完成环境堆栈仅包含平台代码时才可被调用
2.2.5 onFulfilled 和 onRejected 必须被作为函数调用(即没有 this 值)
2.2.6 then 方法可以被同一个 promise 调用多次
2.2.6.1 当 promise 成功完成时,所有 onFulfilled 需按照其注册顺序依次回调
2.2.6.2 当 promise 被拒绝完成时,所有的 onRejected 需按照其注册顺序依次回调
2.2.7 then 方法必须返回一个 promise 对
promise2 = promise1.then(onFulfilled, onRejected) 2.2.7.1 如果 onFulfilled 或者 onRejected 返回一个值 x ,则运行下面的 promise 解决过程:[[Resolve]](promise2, x)
2.2.7.2 如果 onFulfilled 或者 onRejected 抛出一个异常 ``e ,则 TypeError必须拒绝完成,并返回拒绝原因e
2.2.7.3 如果onFulfilled不是函数且promise1成功完成,promise2必须成功完成并返回相同的值
2.2.7.4 如果onRejected不是函数且promise1拒绝完成,promise2 必须拒绝完成并返回相同的拒绝原因
2.3 promise 解决过程
promise 解决过程是一个抽象的操作,其需输入一个 promise 和一个值,我们表示为 [[Resolve]](promise, x),如果 x 有 then 方法且看上去像一个 promise ,解决程序即尝试使 promise 接受 x 的状态;否则其用 x 的值来完成 promise 。
这种 thenable 的特性使得 promise 的实现更具有通用性:只要其暴露出一个遵循 Promise/A+ 协议的 then 方法即可;这同时也使遵循 Promise/A+ 规范的实现可以与那些不太规范但可用的实现能良好共存。
运行 [[Resolve]](promise, x) 需遵循以下步骤:
2.3.1 如果 x 为 promise ,则使 promise 接受 x 的状态
2.3.2 如果 x 为 promise,则使 promise 接受 x 的状态:
2.3.2.1 如果 x 处于等待态, promise 需保持为等待态直至 x 被完成或拒绝
2.3.2.2 如果 x 处于完成态,用相同的值完成 promise
2.3.2.3 如果 x 处于拒绝态,用相同的拒绝原因拒绝 promise
2.3.3 如果 x 为对象或者函数:
2.3.3.1 把 x.then 赋值给 then
2.3.3.2 如果取 x.then 的值时抛出错误 e ,则以 e 为拒绝原因拒绝 promise
2.3.3.3 如果 then 是函数,将 x 作为函数的作用域 this 调用之。传递两个回调函数作为参数,第一个参数叫做 resolvePromise,第二个参数叫做 rejectPromise:
2.3.3.3.1 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)
2.3.3.3.2 如果 rejectPromise 以拒绝原因 r 为参数被调用,则以拒绝原因 r 拒绝 promise
2.3.3.3.3 如果 resolvePromise 和 rejectPromise 均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
2.3.3.3.4 如果调用 then 方法抛出了异常 e:
2.3.3.3.4.1 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之
2.3.3.3.4.2 否则以 e 为拒绝原因拒绝 promise
2.3.3.4 如果 then 不是函数,以 x 为参数完成 promise
2.3.4 如果 x 不为对象或者函数,以 x 为参数完成 promise
如果一个 promise 被一个循环的 thenable 链中的对象解决,而 [[Resolve]](promise, thenable) 的递归性质又使得其被再次调用,根据上述的算法将会陷入无限递归之中。算法虽不强制要求,但也鼓励施者检测这样的递归是否存在,若检测到存在则以一个可识别的 TypeError 为拒绝原因来拒绝 promise
3. Notes
- 这里的平台代码指的是引擎、环境以及
promise的实施代码。实践中要确保onFulfilled和onFulfilled方法异步完成,且应该在then方法被调用的那一轮事件循环之后的新完成栈中完成。这个事件队列可以采用“宏任务(macro-task)”机制或者“微任务(micro-task)”机制来实现。由于promise的实施代码本身就是平台代码,故代码自身在处理在处理程序时可能已经包含一个任务调度队列。 - 也就是说在严格模式(strict)中,函数
this的值为undefined;在非严格模式中其为全局对象 - 代码实现在满足所有要求的情况下可以允许
promise2 === promise1。每个实现都要文档说明其是否允许以及在何种条件下允许promise2 === promise1 - 总体来说,如果
x符合当前实现,我们才认为它是真正的promise。这一规则允许那些特例实现接受符合已知要求的promise状态。 - 这步我们先是存储了一个指向
x.then的引用,然后测试并调用该引用,以避免多次访问x.then属性。这种预防措施确保了该属性的一致性,因为其值可能在检索调用时被改变。 - 实现不应该对
thenable链的深度设限,并假定超出本限制的递归就是无限循环。只有真正的循环递归才应能导致TypeError异常;如果一条无限长的链上thenable均不相同,那么递归下去永远是正确的行
代码实现
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
const runMicrotasks = (fn) => {
// queueMicrotask 将微任务加入队列以在控制返回浏览器的事件循环之前的安全时间执行。
if (typeof queueMicrotask === 'function') {
queueMicrotask(fn)
} else if (typeof process === 'object' && typeof process.nextTick === 'function') {
// node 环境
process.nextTick(fn)
} else if (typeof MutationObserver === 'function') {
const observer = new MutationObserver(fn)
const textNode = document.createTextNode('1')
observer.observe(textNode, { characterData: true }) // 监听文本节点
textNode.data = '2'
} else {
setTimeout(fn, 0)
}
}
const isPromiseLike = (obj) => {
return typeof obj?.then === 'function'
}
/**
* function isPromiseLike = (value){
* return value !== null && (typeof value === 'object'|| typeof value === 'function') && typeof value.then === 'function'
* }
*/
class MyPromise {
#state = PENDING
#value
#handlers = []
#setState(state, value) {
if (this.#state !== PENDING) return
this.#state = state
this.#value = value
this.#runTask()
}
#runTask() {
runMicrotasks(() => {
if (this.#state !== PENDING) {
this.#handlers.forEach((cb) => cb())
this.#handlers = []
}
})
}
constructor(executor) {
const resolve = (value) => {
this.#setState(FULFILLED, value)
}
const reject = (reason) => {
this.#setState(REJECTED, value)
}
// 只能捕获同步错误
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.#handlers.push(() => {
try {
const cb = this.#state === FULFILLED ? onFulfilled : onRejected
const res = typeof cb === 'function' ? cb(this.#value) : this.#value
// 是不是 符合promise/A+ 规范
if (isPromiseLike(res)) {
// 如果 then 中返回 promise
res.then(resolve, reject)
} else {
resolve(res)
}
} catch (error) {
reject(error)
}
})
this.#runTask()
})
}
catch(onRejected) {
return this.then(undefined, onRejected)
}
finally(onFinally) {
return this.then(
(res) => {
onFinally()
return res
},
(err) => {
onFinally()
throw err
}
)
}
static resolve(value) {
if (value instanceof Promise) return value
return new Promise((resolve, reject) => {
if (isPromiseLike(value)) {
value.then(resolve, reject)
} else {
resolve(value)
}
})
}
static reject(reason) {
return new Promise((_, reject) => {
reject(reason)
})
}
static try(cb, ...args) {
return new Promise((resolve, reject) => {
resolve(cb(...args))
})
}
static all(promises) {
promises = Array.from(promises)
return new MyPromise((resolve, reject) => {
const result = []
if (promises.length === 0) {
resolve(result)
}
let count = 0
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then((res) => {
result[index] = res
count++
if (count === promises.length) {
resolve(result)
}
}, reject)
})
})
}
static race(promises) {
promises = Array.from(promises)
return new MyPromise((resolve, reject) => {
// 这里不需要使用索引,只要能循环出每一项就行
for (const item of promises) {
MyPromise.resolve(item).then(resolve, reject)
}
})
}
static any(promises) {
promises = Array.from(promises)
return new MyPromise((resolve, reject) => {
const result = []
if (promises.length === 0) {
resolve(result)
}
let count = 0
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(resolve, (error) => {
result[index] = error
count++
if (count === promises.length) reject(new Error('All promises were rejected'))
})
})
})
}
static allSettled(promises) {
promises = Array.from(promises)
let count = 0,
result = []
return new MyPromise((resolve, reject) => {
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
(res) => {
result[index] = res
count++
if (count === promises.length) resolve(result)
},
(err) => {
result[index] = err
count++
if (count === promises.length) resolve(result)
}
)
})
})
}
}MutationObserver
MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。
var observer = new MutationObserver(callback)callback
一个回调函数,每当被指定的节点或子树以及配置项有 DOM 变动时会被调用。回调函数拥有两个参数:一个是描述所有被触发改动的 MutationRecord 对象数组,另一个是调用该函数的 MutationObserver 对象。
实例方法
MutationObserver.disconnect()方法告诉观察者停止观察变动。
MutationObserver.observe()方法配置了 MutationObserver 对象的回调方法以开始接收与给定选项匹配的 DOM 变化的通知。根据配置,观察者会观察 DOM 树中的单个 Node,也可能会观察被指定节点的部分或者所有的子孙节点。
mutationObserver.observe(target[, options])target
DOM 树中的一个要观察变化的 DOM Node (可能是一个 Element),或者是被观察的子节点树的根节点。
options
此对象的配置项描述了 DOM 的哪些变化应该报告给 MutationObserver 的 callback。当调用 observe() 时,childList、attributes 和 characterData 中,必须有一个参数为 true。否则会抛出 TypeError 异常。
options 的属性如下:
subtree 可选
当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target。默认值为 false。childList 可选
当为 true 时,监听 target 节点中发生的节点的新增与删除(同时,如果 subtree 为 true,会针对整个子树生效)。默认值为 false。attributes 可选
当为 true 时观察所有监听的节点属性值的变化。默认值为 true,当声明了 attributeFilter 或 attributeOldValue,默认值则为 false。attributeFilter 可选
一个用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。attributeOldValue 可选
当为 true 时,记录上一次被监听的节点的属性变化;可查阅监听属性值了解关于观察属性变化和属性值记录的详情。默认值为 false。characterData 可选
当为 true 时,监听声明的 target 节点上所有字符的变化。默认值为 true,如果声明了 characterDataOldValue,默认值则为 falsecharacterDataOldValue 可选
当为 true 时,记录前一个被监听的节点中发生的文本变化。默认值为 false
MutationObserver.takeRecords()返回已检测到但尚未由观察者的回调函数处理的所有匹配 DOM 更改的列表,使变更队列保持为空。此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改。
thenable 的执行时机
- 完成,所有注册的 thenable 进队列
- 调用 then,如果已完成,直接进队列
new Promise((resolve, reject) => {
resolve(2)
new Promise((resolve, reject) => {
resolve(5)
}).then((v) => console.log(v))
}).then((v) => console.log(v))new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
new Promise((resolve, reject) => {
resolve(5)
}).then((v) => console.log(v))
})
}).then((v) => console.log(v))const promise1 = Promise.resolve('first')
const promise2 = new Promise((resolve) => {
setTimeout(() => {
resolve('second')
}, 1000)
})
const promise3 = Promise.reject('third')
function handlePromise(promise) {
return promise
.then((value) => {
console.log(value)
return value
})
.catch((error) => {
console.log('Error', error)
return 'Error handler'
})
}
async function runPromises() {
try {
const result1 = await handlePromise(promise1)
console.log('result1', result1)
// const result2 = await handlePromise(promise2)
const result2 = handlePromise(promise2)
console.log('result2', result2)
const result3 = await handlePromise(promise3)
console.log('result3', result3)
} catch (error) {
console.error('Caught error', error)
}
}
runPromises()面试题
const promise = new Promise((resolve) => {
console.log('11111')
setTimeout(() => {
console.log('22222')
}, 0)
resolve()
console.log('resolve')
throw new Error('error')
console.log('error')
})
promise
.then(
() => {
console.log('33333')
setTimeout(() => {
console.log('44444')
}, 0)
},
() => {
console.log('reject')
}
)
.catch(() => {
console.log('catch')
})
console.log('55555')const promise1 = Promise.resolve('First')
const promise2 = Promise.resolve('Second')
const promise3 = Promise.reject('Third')
const promise4 = Promise.resolve('Fourth')
const runPromises = async () => {
const res1 = await Promise.all([promise1, promise2])
const res2 = await Promise.all([promise3, promise4])
return [res1, res2]
}
runPromises()
.then((res) => console.log(res))
.catch((err) => console.log(err))// Promise 的状态吸收
// https://www.bilibili.com/video/BV1pnrqYJEuF
Promise.resolve()
.then(() => {
console.log(0)
return Promise.resolve(4)
})
.then((res) => {
console.log(res)
})
Promise.resolve()
.then(() => {
console.log(1)
})
.then(() => {
console.log(2)
})
.then(() => {
console.log(3)
})
.then(() => {
console.log(5)
})
.then(() => {
console.log(6)
})// https://www.bilibili.com/video/BV1gdNszSEXb
async function asy1() {
console.log(1)
await asy2()
console.log(2)
}
const asy2 = async () => {
await setTimeout(() => {
Promise.resolve().then(() => {
console.log(3)
})
console.log(4)
}, 0)
}
const asy3 = async () => {
Promise.resolve().then(() => {
console.log(6)
})
}
asy1()
console.log(7)
asy3()async function asy1() {
console.log(1)
await asy2()
console.log(2)
}
const asy2 = async () => {
await (() => {
console.log(3)
})()
console.log(4)
}
const asy3 = async () => {
Promise.resolve().then(() => {
console.log(6)
})
}
asy1()
console.log(7)
asy3()