请允许我丑话说在前头
就像这篇文章的标题一样,对于手写 Promise 这种面试题,我是极度抗拒的。
我理想中的前端开发,首先要有一双发现美的眼睛,和一双实现美的双手;时刻关注页面的交互和性能,有同理心,永远把 用户体验 放在第一位。
然而事与愿违。
当下的前端圈子很浮,浮到五花八门的技术名词满天飞;
也很卷,卷到好好的一场面试,你居然让我现场造一个 Promise 出来......
Promise 解决了什么问题?
无脑回答:回调地狱!
对,但也不全对。
在 ES6 之前,回调函数 是处理异步场景最常见的招数。
封装一个异步请求:
function request(url, options) {
var xhr = new XMLHttpRequest()
xhr.open(options.method, url)
xhr.onerror = function (e) {
options.fail(e)
}
xhr.ontimeout = function (e) {
options.fail(e)
}
xhr.onreadystatechange = function () {
if (this.readyState === XMLHttpRequest.DONE) {
if (this.status === 200) {
options.success(this.response)
}
}
}
xhr.send(options.data || null)
}
我们可以这样使用 request:
request('https://juejin.cn', {
method: 'GET',
success: function (res) {
console.log(res)
},
fail: function (err) {
console.log(err)
}
})
在我看来,回调函数的缺点主要有以下两点:
- 嵌套层级多了,代码可读性变得非常差,也就是我们前面讲到的 回调地狱;
- 每个任务都要进行一次额外的 错误处理,增加了代码的混乱程度。
传说中的回调地狱:
request('https://juejin.cn', {
method: 'GET',
success: function (res) {
console.log(res)
request('https://juejin.cn/pins', {
method: 'GET',
success: function (res) {
console.log(res)
request('https://juejin.cn/course', {
method: 'GET',
success: function (res) {
console.log(res)
},
fail: function (err) {
console.log(err)
}
})
},
fail: function (err) {
console.log(err)
}
})
},
fail: function (err) {
console.log(err)
}
})
其实稍加改造一下也是可以看的,比如这样:
function requestHome() {
request('https://juejin.cn', {
method: 'GET',
success: function (res) {
console.log(res)
requestPins()
},
fail: function (err) {
console.log(err)
}
})
}
function requestPins() {
request('https://juejin.cn/pins', {
method: 'GET',
success: function (res) {
console.log(res)
requestCourse()
},
fail: function (err) {
console.log(err)
}
})
}
function requestCourse() {
request('https://juejin.cn/course', {
method: 'GET',
success: function (res) {
console.log(res)
},
fail: function (err) {
console.log(err)
}
})
}
requestHome()
不过这种改造方式引进了新的问题:代码逻辑不连续、反复横跳。
于是,ES6 为我们带来了好用的 Promise!
我们使用 Promise 重构一下 request 函数:
function requestPlus(url, { method, data }) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(method, url)
xhr.onerror = e => reject(e)
xhr.ontimeout = e => reject(e)
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
resolve(xhr.response)
}
}
}
xhr.send(data || null)
})
}
接下来,我们再使用 requestPlus 走一遍请求流程:
requestPlus('https://juejin.cn', { method: 'get' })
.then(res => {
console.log(res)
return requestPlus('https://juejin.cn/pins', { method: 'get' })
})
.then(res => {
console.log(res)
return requestPlus('https://juejin.cn/course', { method: 'get' })
})
.catch(err => {
console.log(err)
})
Wow!是不是清新了许多呢?
回调地狱不复存在了,取而代之的,是层级简单、赏心悦目的 Promise 调用链。
针对回调函数的两个问题,Promise 做了以下改进:
- 链式调用:变嵌套为扁平,减少了缩进;
- 错误传递:最后的
catch捕获异常即可。
来自面试官的灵魂拷问
既然 Promise 这么好用,那么请手写一个 Promise 吧!
What?我没听错吧?
ECMA 发明 Promise 是来减轻开发者负担的,你却把我叫来造火箭......
好吧,我忍了,这 Promise,我写!
手写 Promise,从何写起?
要想手写 Promise,需要先搞清楚 Promise 是什么?
它的英文名叫做 承诺。
什么是承诺?一时半会儿没法实现的(异步的本质),先画个大饼,这就叫承诺!
我们可以这样画一个大饼,哦不,是许一个承诺:
const 承诺 = new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('我许你的承诺实现了')
} else {
reject('对不起,我爱上了别人')
}
}, 2000)
})
const 被满足的喜悦 = value => {
console.log(value)
}
const 被拒绝的悲伤 = reason => {
console.log(reason)
}
承诺.then(被满足的喜悦, 被拒绝的悲伤)
承诺有三种状态,分别是:
pending:悬而未决的;fulfilled:满足的;rejected:拒绝的。
状态的流动形式有且只有以下两种:
resolve:从pending到fulfilled;reject:从pending到rejected。
相信大家对 Promise 应该有了初步的认识,不过真正实现它绝非易事,我们需要借一下 Promises/A+ 规格文档的东风。
跟着 Promises/A+ 写 Promise
Promise 首先是一个类
class MyPromise {}
new Promise(executor) 必须接受一个函数
class MyPromise {
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('我只接受一个函数')
}
}
}
new MyPromise(executor) 会生成一个对象,对象有 then 方法
class MyPromise {
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('我只接受一个函数')
}
}
then() {}
}
new MyPromise(executor) 中的 executor 会立即执行
class MyPromise {
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('我只接受一个函数')
}
executor()
}
then() {}
}
new MyPromise(executor) 中的 executor 接受 resolve 和 reject 两个函数
class MyPromise {
#resolve = () => {}
#reject = () => {}
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('我只接受一个函数')
}
executor(this.#resolve, this.#reject)
}
then() {}
}
#resolve 和 #reject 是私有化的实例方法,不希望暴露给类外面的 promise 实例使用;采用箭头函数是为了防止出现 this 丢失的情况,保证 this 指向的是 promise 实例。
2.1 promise 的状态
一个 promise 必须处于 pending、fulfilled、rejected 三种状态之一。
2.1.1 当一个 promise 处于 pending 状态时,它可能会转换为 fulfilled 或 rejected 状态。
2.1.2 当一个 promise 处于 fulfilled 状态时,它不能转换为任何别的状态,且必须有一个 value,不能被改变。
2.1.3 当一个 promise 处于 rejected 状态时,它不能转换为任何别的状态,且必须有一个 reason,不能被改变。
class MyPromise {
#state = 'pending'
#resolve = value => {
if (this.#state !== 'pending') {
return
}
this.#state = 'fulfilled'
}
#reject = reason => {
if (this.#state !== 'pending') {
return
}
this.#state = 'rejected'
}
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('我只接受一个函数')
}
executor(this.#resolve, this.#reject)
}
then() {}
}
2.2 then 方法
一个 promise 的 then 方法接受两个参数。
class MyPromise {
#state = 'pending'
#resolve = value => {
if (this.#state !== 'pending') {
return
}
this.#state = 'fulfilled'
}
#reject = reason => {
if (this.#state !== 'pending') {
return
}
this.#state = 'rejected'
}
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('我只接受一个函数')
}
executor(this.#resolve, this.#reject)
}
then(onFulfilled, onRejected) {}
}
2.2.1 onFulfilled 和 onRejected 都是可选参数。如果它们不是一个函数,必须被忽略。
class MyPromise {
#state = 'pending'
#resolve = value => {
if (this.#state !== 'pending') {
return
}
this.#state = 'fulfilled'
}
#reject = reason => {
if (this.#state !== 'pending') {
return
}
this.#state = 'rejected'
}
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('我只接受一个函数')
}
executor(this.#resolve, this.#reject)
}
then(onFulfilled = null, onRejected = null) {
if (typeof onFulfilled === 'function') {}
if (typeof onRejected === 'function') {}
}
}
2.2.2 如果 onFulfilled 是一个函数,它必须在 promise 处于 fulfilled 状态之后被调用,并将 promise 的 value 作为第一个参数,且只能被调用一次。
2.2.3 如果 onRejected 是一个函数,它必须在 promise 处于 rejected 状态之后被调用,并将 promise 的 reason 作为第一个参数,且只能被调用一次。
2.2.4 在执行上下文仅包含平台代码之前,不能调用 onFulfilled 和 onRejected。
2.2.5 onFulfilled 和 onRejected 必须被作为函数调用,且没有 this 值。
2.2.6 同一个 promise 的 then 可以被多次调用。
- 当
promise处于fulfilled状态时,多个onFulfilled必须按then的调用顺序依次执行; - 当
promise处于rejected状态时,多个onRejected必须按then的调用顺序依次执行。
const addMicrotask = callback => {
if (typeof queueMicrotask === 'function') {
queueMicrotask(callback)
} else {
process.nextTick(callback)
}
}
class MyPromise {
#state = 'pending'
#queue = []
#resolve = value => {
if (this.#state !== 'pending') {
return
}
this.#state = 'fulfilled'
addMicrotask(() => {
this.#queue.forEach(arr => {
const onFulfilled = arr[0]
if (typeof onFulfilled === 'function') {
onFulfilled.call(undefined, value)
}
})
})
}
#reject = reason => {
if (this.#state !== 'pending') {
return
}
this.#state = 'rejected'
addMicrotask(() => {
this.#queue.forEach(arr => {
const onRejected = arr[1]
if (typeof onRejected === 'function') {
onRejected.call(undefined, reason)
}
})
})
}
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('我只接受一个函数')
}
executor(this.#resolve, this.#reject)
}
then(onFulfilled = null, onRejected = null) {
const arr = []
if (typeof onFulfilled === 'function') {
arr[0] = onFulfilled
}
if (typeof onRejected === 'function') {
arr[1] = onRejected
}
this.#queue.push(arr)
}
}
至此,我们实现了一个基础版的 Promise,一起体验一下:
const promise = new MyPromise(resolve => {
console.log(1)
resolve('hi')
console.log(2)
})
console.log(3)
promise.then(value => console.log(`1-${value}`))
promise.then(value => console.log(`2-${value}`))
promise.then(value => console.log(`3-${value}`))
console.log(4)
如果最终打印的顺序是 1 -> 2 -> 3 -> 4 -> '1-hi' -> '2-hi' -> '3-hi',那这个基础版的 Promise 就成功了。
写在结尾
我们还没有完成 then 的链式调用,篇幅已经很长了,下集再写吧。
各位看官觉得,可以实现一个基础版的 Promise 的前端开发,值多少 k 呢?