努力让学习成为一种习惯,自信来源于充分的准备
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享
前言
Javascript是一门单线程语言,异步操作是必不可少的。由于 Javascipt自身设计的缺陷。早期都是用回调函数的方式实现异步。但这种方式存在回调地狱、回调操作不统一等痛点,代码难以维护的同时也大大增加了心智负担。详情可以参考深入理解Promise-上(起源篇)
本篇文章将深入Promise本身,探究其原理并从零实现一个基于Promise A+规范,满足其核心功能的Promise
什么是Promise
回答这个问题需要从两个角度:1. Promise A+规范 2. 现代浏览器中的Promise即ES6的Promise
Promise A+规范
引入红宝书对这段历史的描述
为了解决异步场景的各种痛点,社区引入了期约机制。早期的期约机制在jQuery和Dojo中是以
Deferred API的形式出现 的。到了2010年,CommonJS项目实现的Promises/A规范日益流行起来。Q和Bluebird等第三方JavaScript期约库也越来越得到社区认可。这些库解决的问题是同一个,但实现方式多少有些差异(类似于浏览器间的DOM规范)虽然这些库的实现多少都有些不同。为弥合现有实现之间的差异,2012 年Promises/A+组织分叉(fork)了CommonJS的Promises/A建议,并以相同的名字制定了Promises/A+规范。这个规范最终成为了ECMAScript 6规范实现的范本
简单来说,Promises/A+组织依据社区内相对著名的第三方期约库,综合制定了更加完善且统一的期约规范 - Promise A+规范
我们来看看规范文档是怎么定义Promise的,为了方便,接下来统一把Promise A+规范简称为A+规范
promise是具有符合本规范的then方法的对象或函数
那么是不是说明以下这些都是Promise呢
const p = {
then() {}
}
const arr = []
arr.then = function () {}
function f () {}
f.then = function () {}
严格来说这只是“挂羊头卖狗肉”,因为Promise最关键的点在于then方法的行为。整个A+规范也都是在说明 then方法应该有哪些行为。换句话说,一个对象或者函数,它有一个then方法并且行为交互满足A+规范,那么它就是一个Promise
ES6 Promise
ES6将A+规范写入了语言标准。原生提供了Promise构造函数,通过它我们可以创建一个满足A+规范的 Promise对象。并且在此基础上,进一步扩展了能力(如Promise.all、Promise.race等)
有关ES6 Promise的API使用可以参考阮一峰的promise文档
从零实现A+规范的Promise(核心部分)
相信到这里,大家对Promise的来龙去脉有了一个感性的认识。接下来我们深入A+规范,从零自己实现一个 Promise,在这个过程,相信对 Promise的理解会进一步提升
相关文档
这里我会采用
es6的写法实现,原理都是一样,es6语法层面更简洁美观点
首先我们来实现Promise基本的特性
Promise构造函数内部必须传入一个执行器函数(executor),且该执行器会立刻执行- 每一个
Promise对象有三个内部状态:pending/fulfilled/rejected - 执行器函数会提供两个内置函数
resolve/reject,用于改变Promise对象的状态。执行器调用resolve则promise兑现(fulfilled状态),调用reject则promise拒绝(rejected状态);如果执行器抛异常同样拒绝。Promise对象状态一旦确定了,便不可再次更改
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
#state = PENDING
#result = undefined
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('is not a function')
}
const resolve = (result) => {
if (this.#state !== PENDING) return
this.#state = FULFILLED
this.#result = result
}
const reject = () => {
if (this.#state !== PENDING) return
this.#state = REJECTED
this.#result = result
}
try {
executor(resolve, reject)
} catch(error) {
reject(error)
}
}
}
这里额外提一个细节点,为什么不把resolve、reject函数作为实例方法,这样不就避免每次初始化构造函数都要重复定义,原因是这种写法的this指向的全局对象,如果我们要指定this为Promise实例需要bind:
executor(this.resolve.bind(this), this.reject.bind(this))也是返回一个新的函数,本质一样的
接下来便是A+规范中的核心:then的实现,我们一步一步来

对于上面这段描述其核心点是:onFulfilled、onRejected这两个函数什么时候调用,显然这两个函数在调用then的promise对象兑现/拒绝的时候调用,基于这个思路
我们完成第一版
class MyPromise {
// ....
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
// 需要注意,这里的this指向需要是最开始的promise实例,这里的`this.#state`也是它的状态
// 换言之这里需要使用箭头函数
if (this.#state === FULFILLED) {
onFulfilled?.(this.#result)
} else if (this.#state === REJECTED) {
onRejected?.(this.#result)
}
})
}
}
测试一下,没问题😆
const p = new MyPromise((resolve, reject) => {
resolve(2)
})
p.then((v) => {
console.log(v); // 2
})
const p2 = new MyPromise((resolve, reject) => {
reject('errorMsg')
})
p2.then(null, (e) => {
console.log(e); // errorMsg
})
这样第一步就完成了吗?不,当resolve/reject异步调用的时候就会存在问题,如下
const p = new MyPromise((resolve, reject) => {
setTimeout(resolve, 0, 1);
})
// then中的逻辑不会执行
p.then((v) => {
console.log(v);
})
上面这段代码的执行顺序是:MyPromise构造函数的executor -> p.then -> resolve,当 resolve执行的时候,p.then早已执行完成(then执行的时候,promise的实例状态还是pending,因此then中的onFulfilled/onRejected不会执行)
既然知道了原因,那么解决方案也有了,我们在resolve/reject函数内部调用onFulfilled/onRejected就好了,为了实现这点,我们需要用一个属性将它们保存起来用于在某个时机调用
class MyPromise {
// ..
#promiseHandler
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('is not a function')
}
const resolve = (result) => {
// ...
this.#run()
}
const reject = (result) => {
// ...
this.#run()
}
// ...
}
#run() {
if (this.#state === PENDING) return
if (!this.#promiseHandler) return
const {
onFulfilled,
onRejected
} = this.#promiseHandler
if (this.#state === FULFILLED) {
onFulfilled?.(this.#result)
} else if (this.#state === REJECTED) {
onRejected?.(this.#result)
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.#promiseHandler = {
onFulfilled,
onRejected
}
this.#run()
})
}
}
再测试之前的例子就可以了😄。好耶,我们接着往下走
const p = new MyPromise((resolve, reject) => {
setTimeout(resolve, 0)
})
p.then(() => {
console.log(1)
})
p.then(() => {
console.log(2)
})
p.then(() => {
console.log(3)
})
按照规范,上面的代码执行结果依次是:1、2、3。但是目前我们实现的版本输出的只有3,这是因为每次调用then方法,就会覆盖之前的#promiseHandler,于是onFulfilled/onRejected也被覆盖,执行的也就是最后一个。调整也很简单,我们只需要把之前的#promiseHandler改成数组结构,维护一个onFulfilled/onRejected队列即可
class MyPromise {
// ...
// 修改成数组结构
#promiseHandler = []
constructor(executor) {
// ...
}
#run() {
if (this.#state === PENDING) return
while(this.#promiseHandler.length) {
const {
onFulfilled,
onRejected
} = this.#promiseHandler.shift()
if (this.#state === FULFILLED) {
onFulfilled?.(this.#result)
} else if (this.#state === REJECTED) {
onRejected?.(this.#result)
}
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.#promiseHandler.push({
onFulfilled,
onRejected
})
this.#run()
})
}
}
再测试之前的例子,发现已经可以按照then的调用顺序输出结果了,😄又前进了一步
在前面实现的代码中,then方法返回的是一个新的Promise对象,接下来便是A+的规范的最后一个核心难点-then的链式调用
首先我们需要确认一个点:then方法返回的新的Promise对象的then何时会被调用。答案显而易见,当返回的新的 Promise对象状态更新的时候
我们可以把新Promise对象的resolve/reject也维护在#promiseHandler数组(如果不用数组维护,也会存在上面描述的问题)
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.#promiseHandler.push({
onFulfilled,
onRejected,
resolve,
reject
})
this.#run()
})
}
我们先对Promise.then中onFulfilled/onRejected做下梳理:
onFulfilled/onRejected不是函数,则将当前Promise对象的结果/原因穿透到下一层
Promise.resolve(1)
.then(null)
.then(v => {
console.log(v); // 1
})
Promise.reject('error')
.then(null, 1)
.then(null, (v) => {
console.log(v); // error
})
这里需要额外提的一点,如果onFulfilled/onRejected不是函数,则会替换成一个函数,具体如下
即会存在以下转化
let _onFulfilled
if (typeof onFulfilled !== 'function') {
// this.#result为之前兑现的值
_onFulfilled = () => this.#result
} else {
_onFulfilled = onFulfilled
}
let _onRejected
if (typeof onRejected !== 'function') {
_onRejected = () => {
// this.#result为之前拒绝的原因
throw this.#result
}
}
onFulfilled/onRejected是函数, 这里依据函数执行的返回值又有以下情况
返回值不是Promise对象, 则新Promise对象状态为fulfilled,并将返回值传递到下一个then方法中的onFulfilled回调中
Promise.resolve(1)
.then(v => 2)
.then(v => {
console.log(v); // 2 如果没有返回值则为undefined
})
Promise.reject(1)
.then(null, () => {
return 22
}).then((v2) => {
console.log(v2) // 22 如果没有返回值则为undefined
})
返回值是Promise对象,则新Promise对象状态取决于返回的Promise对象的状态,根据状态为fulfilled/rejected调用第二个then方法的onFulfilled/onRejected函数
const p1 = Promise.resolve(1)
const p2 = Promise.reject('error')
Promise.resolve(1)
.then(() => p1)
.then((v) => {
console.log(v); // 1
})
Promise.resolve(1)
.then(() => p2)
.then(null, (v) => {
console.log(v); // error
})
针对以上三种情况,我们继续完善自己Promise
function isPromiseLike(obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}
class MyPromise {
// ...
constructor(executor) {
// ...
}
#run() {
if (this.#state === PENDING) return
while(this.#promiseHandler.length) {
const {
onFulfilled,
onRejected,
resolve,
reject
} = this.#promiseHandler.shift()
if (this.#state === FULFILLED) {
let _onFulfilled = onFulfilled
if (typeof onFulfilled !== 'function') {
_onFulfilled = () => this.#result
}
try {
const result = _onFulfilled(this.#result)
if (isPromiseLike(result)) {
result.then(resolve, reject)
} else {
resolve(result)
}
} catch(e) {
reject(e)
}
} else if (this.#state === REJECTED) {
let _onRejected = onRejected
if (typeof onRejected !== 'function') {
_onRejected = () => {
throw(this.#result)
}
}
try {
const reason = _onRejected(this.#result)
if (isPromiseLike(reason)) {
reason.then(resolve, reject)
} else {
resolve(reason)
}
} catch(e) {
reject(e)
}
}
}
}
}
这里需要注意判断一个对象是否是Promise, 不能使用
obj instanceof Promise这种方式,promise是具有符合A+规范的then方法的对象或函数
到这里,我们基本已经实现了一个满足A+规范的promise,另外还有一个细节点:
这里我们可以借鉴vue源码中nextTick的实现
const runByMicroTask = (cb) => {
// node环境
if (typeof process === 'object' && typeof process.nextTick === 'function') {
process.nextTick(cb)
} else if (typeof MutationObserver === 'function') {
const textNode = document.createTextNode('')
const observer = new MutationObserver(cb)
observer.observe(textNode, {
characterData: true
})
textNode.data = 1
} else {
setTimeout(cb, 0);
}
}
我们可以测试下,下面代码输出结果为:1、3、2
console.log(1);
runByMicroTask(() => {
console.log(2);
})
console.log(3);
setimmediate理论上也可以,但是这个API不建议生产环境使用,详情可见 setImmediate
最后我们把这个功能点加到promise 逻辑中,并且整体优化下代码,把重复逻辑抽离出来。完整代码如下:
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
function isPromiseLike(obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}
class MyPromise {
#state = PENDING
#result = undefined
#promiseHandler = []
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('is not a function')
}
const resolve = (result) => {
this.#statusChangeHandler(FULFILLED, result)
}
const reject = (result) => {
this.#statusChangeHandler(REJECTED, result)
}
try {
executor(resolve, reject)
} catch(e) {
reject(e)
}
}
#statusChangeHandler(status, result) {
if (this.#state !== PENDING) return
this.#state = status
this.#result = result
this.#run()
}
#microTaskRun(cb) {
if (typeof process === 'object' && typeof process.nextTick === 'function') {
process.nextTick(cb)
} else if (typeof MutationObserver === 'function') {
const textNode = document.createTextNode('')
const observer = new MutationObserver(cb)
observer.observe(textNode, {
characterData: true
})
textNode.data = 1
} else {
setTimeout(cb, 0);
}
}
#run() {
if (this.#state === PENDING) return
while(this.#promiseHandler.length) {
this.#thenableHandler(this.#promiseHandler.shift())
}
}
#thenableHandler({onFulfilled, onRejected, resolve, reject}) {
this.#microTaskRun(() => {
let cb = this.#state === FULFILLED ? onFulfilled : onRejected
if (typeof cb !== 'function') {
cb = this.#state === FULFILLED ? () => this.#result : () => { throw this.#result }
}
try {
const r = cb(this.#result)
if (isPromiseLike(r)) {
r.then(resolve, reject)
} else {
resolve(r)
}
} catch(e) {
reject(e)
}
})
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.#promiseHandler.push({
onFulfilled,
onRejected,
resolve,
reject
})
this.#run()
})
}
}
最后
这里我们只是实现了一个满足Promise A+规范核心功能的promise,如果你用promises-aplus-tests测试库跑用例,会发现有不少边界情况的测例不会通过,主要在于这块我们并没有严格按照规范编写
我个人感觉不是很有必要,了解其核心思路即可。如果小伙伴们想看标准A+规范的实现可以自行查阅资料文章,另外这也并不是ES6中的promise。ES6在满足A+规范的前提下,还扩展丰富了其能力(Promise.resolve/reject/all/race等),这些将后续用一篇文章来手写
到这里,就是本篇文章的全部内容了
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享
如果你有疑问或者出入,评论区告诉我,我们一起讨论