深入理解promise
深入理解 Promise
一位高端的前端攻城狮曾经说过,一位不愿意透露姓名的顶级摸鱼工程师曾经说过,学习 Promise 最好的方式就是先阅读它的规范定义。那么哪里可以找到 Promise 的标准定义呢?
答案是 Promises/A+ 规范。
假设你已经打开了上述的规范定义的页面并尝试开始阅读(不要因为是英文的就偷偷关掉,相信自己,你可以的),规范在开篇描述了 Promise 的定义,与之交互的方法,然后强调了规范的稳定性。关于稳定性,换言之就是:我们可能会修订这份规范,但是保证改动微小且向下兼容,所以放心地学吧,这就是权威标准,五十年之后你再去谷歌 Promise,出来的规范还是这篇 😂。
好的,让我们回到规范。从开篇的介绍看,到底什么是 Promise?
A promise represents the eventual result of an asynchronous operation.
Promise 就是表示一个异步操作的最终结果
划重点!!这里其实引出了 JavaScript 引入 Promise 的动机:异步。
学习一门新技术,最好的方式是先了解它是如何诞生的,以及它所解决的问题是什么。Promise 跟我们说的异步编程有什么联系呢?Promise 到底解决了什么问题?
要回答这些问题,我们需要先回顾下没有 Promise 之前,异步编程存在什么问题?
异步编程
由于 web 是单线程的架构,决定了 javascript 的异步编程模型是基于消息队列和事件循环
看图说话
我们的异步任务的回调函数会被放入消息队列,然后等待主线程上的同步任务执行完成,执行栈为空时,由事件循环机制调度进执行栈继续执行。
这导致了 JavaScript 异步编程的一大特点:异步回调,比如网络请求,
// 成功的异步回调函数
function resolve(response) {
console.log(response)
}
// 失败的异步回调函数
function reject(error) {
console.log(error)
}
let xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => resolve(xhr.response)
xhr.ontimeout = e => reject(e)
xhr.onerror = e => reject(e)
xhr.open('Get', 'http://xxx')
xhr.send()虽然可以通过简单的封装使得异步回调的方式变得优雅一些,比如,
$.ajax({
url: 'https://xxx',
method: 'GET',
fail: () => {},
success: () => {},
})但是仍然没有办法解决业务复杂后的“回调地狱”的问题,比如多个依赖请求,
$.ajax({
success(res1) {
$.ajax({
success(res2) {
$.ajax({
success(res3) {
// do something...
},
})
},
})
},
})这种线性的嵌套回调使得异步代码变得难以理解和维护,也给人很大的心智负担。 所以我们需要一种技术,来解决异步编程风格的问题,这就是 Promise 的动机。
了解 Promise 背景和动机有利于我们理解规范,现在让我们重新回到规范的定义
规范
Promise A+ 规范首先定义了 Promise 的一些相关术语和状态。
Terminology,术语
- “promise” ,一个拥有
then方法的对象或函数,其行为符合本规范 - “thenable”,一个定义了
then方法的对象或函数 - “value”,任何 JavaScript 合法值(包括
undefined,thenable和promise) - “exception”,使用
throw语句抛出的一个值 - “reason”,表示一个
promise的拒绝原因
State,状态
promise 的当前状态必须为以下三种状态之一:Pending,Fulfilled,Rejected
- 处于 Pending 时,promise 可以迁移至 Fullfilled 或 Rejected
- 处于 Fulfilled 时,promise 必须拥有一个不可变的终值且不能迁移至其他状态
- 处于 Rejected 时,promise 必须拥有一个不可变的拒绝原因且不能迁移至其他状态
所以 Promise 内部其实维护了一个类似下图所示的状态机
Promise 在创建时处于 Pending(等待态),之后可以变为 Fulfilled(执行态)或者 Rejected(拒绝态),一个承诺要么被兑现,要么被拒绝,这一过程是不可逆的。
定义了相关的术语和状态后,是对 then 方法执行过程的详细描述。
Then
一个 promise 必须提供一个 then 方法以访问其当前值、终值和拒绝原因。
then 方法接受两个参数
promise.then(onFulfilled, onRejected)- onFulfilled,在 promise 执行结束后调用,第一个参数为 promise 的终值
- onRejected,在 promise 被拒绝执行后调用,第一个参数为 promise 的拒绝原因
对于这两个回调参数和 then 的调用及返回值,有如下的一些规则,
- onFulfilled 和 onRejected 都是可选参数。
- onFulfilled 和 onRejected 必须作为函数被调用,调用的
this应用默认绑定规则,也就是在严格环境下,this等于undefined,非严格模式下是全局对象(浏览器中就是window)。关于this的绑定规则如果不了解的可以参考我之前的一篇文章 《可能是最好的 this 解析了…》,里面有非常详细地介绍。 - onFulfilled 和 onRejected 只有在执行环境堆栈仅包含平台代码时才可被调用。由于 promise 的实施代码本身就是平台代码(JavaScript),这个规则可以这么理解:就是要确保这两个回调在 then 方法被调用的那一轮事件循环之后异步执行。这不就是微任务的执行顺序吗?所以 promise 的实现原理是基于微任务队列的。
then方法可以被同一个 promise 调用多次,而且所有的成功或拒绝的回调需按照其注册顺序依次回调。所以 promise 的实现需要支持链式调用,可以先想一下怎么支持链式调用,稍后我们会有对应的实现。then方法必须返回一个 promise 对象。
针对第 5 点,还有如下几条扩展定义,我们将返回值与 promise 的解决过程结合起来,
promise2 = promise1.then(onFulfilled, onRejected)then 的两个回调参数可能会抛出异常或返回一个值,
5.1 如果 onFulfilled 或者 onRejected 抛出一个异常 e,那么返回的 promise2 必须拒绝执行,并返回拒绝的原因 e。
5.2 如果 onFulfilled 或者 onRejected 返回了一个值 x,会执行 promise 的解决过程
- 如果
x和返回的 promise2 相等,也就是 promise2 和x指向同一对象时,以TypeError作为拒绝的原因拒绝执行 promise2 - 如果
x是 promise,会判断x的状态。如果是等待态,保持;如果是执行态,用相同的值执行 promise2;如果是拒绝态,用相同的拒绝原因拒绝 promise2 - 如果
x是对象或者函数,将x.then赋值给then;如果取x.then的值时抛出错误e,则以e为拒绝原因拒绝 promise2。如果then是函数,将x作为函数的this,并传递两个回调函数 resolvePromise, rejectPromise 作为参数调用函数
读到这里,相信你跟我一样已经迫不及待想要实现一个 Promise 了,既然了解了原理和定义,我们就来手写一个 Promise 吧。
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
function resolve(value) {
return value
}
function reject(err) {
throw err
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(
new TypeError('Chaining cycle detected for promise #<Promise>')
)
}
let called
if ((typeof x === 'object' && x != null) || typeof x === 'function') {
try {
let then = x.then
if (typeof then === 'function') {
then.call(
x,
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
function resolve(value) {
return value
}
function reject(err) {
throw err
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(
new TypeError('Chaining cycle detected for promise #<Promise>')
)
}
let called
if ((typeof x === 'object' && x != null) || typeof x === 'function') {
try {
let then = x.then
if (typeof then === 'function') {
then.call(
x,
(y) => {
if (called)
return
called = true
resolvePromise(promise2, y, resolve, reject)
},
(r) => {
if (called)
return
called = true
reject(r)
}
)
}
else {
resolve(x)
}
}
catch (e) {
if (called)
return
called = true
reject(e)
}
}
else {
resolve(x)
}
}
class Promise {
constructor(executor) {
this.status = PENDING
this.value = undefined
this.reason = undefined
this.resolveCallbacks = []
this.rejectCallbacks = []
let resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
this.resolveCallbacks.forEach(fn => fn())
}
}
let reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
this.rejectCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
}
catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : resolve
onRejected = typeof onRejected === 'function' ? onRejected : reject
let promise2 = new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
}
catch (e) {
reject(e)
}
}, 0)
}
if (this.status === REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
}
catch (e) {
reject(e)
}
}, 0)
}
if (this.status === PENDING) {
this.resolveCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
}
catch (e) {
reject(e)
}
}, 0)
})
this.rejectCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
}
catch (e) {
reject(e)
}
}, 0)
})
}
})
return promise2
}
}