共计 10626 个字符,预计需要花费 27 分钟才能阅读完成。
Promise/A+ 规范,源码分析
GitHub
Promise 是前端大厂面试的一道常考题,掌握 Promise 用法及其相关原理,对你的面试一定有很大帮助。这篇文章主要讲解 Promise 源码实现,如果你还没有掌握 Promise 的功能和 API,推荐你先去学习一下 Promise 的概念和使用 API,学习知识就要脚踏实地,先把基础搞好才能深刻理解源码的实现。
这里推荐阮一峰老师的文章
ES6 入门 -Promise 对象
如果你已经掌握了 Promise 的基本用法,我们进行下一步
Promise/A+ 规范
说到 Promise/A+ 规范,很多同学可能很不理解这是一个什么东西,下面给出两个地址,不了解的同学需要先了解一下,对我们后续理解源码很有帮助,先看两遍,有些地方看不懂也没关系,后续我们可以通过源码来回头再理解,想把一个知识真的学会,就要反复琢磨,从【肯定 -> 否定 -> 再肯定】不断地深入理解,直到完全掌握。
Promise/A+ 规范英文地址
Promise/A+ 规范中文翻译
如果你看过了 Promise/A+ 规范,我们继续,我会带着大家按照规范要求,一步一步的来实现源码
Promise/A+【2.1】
2.1Promise 状态
一个 promise 必须处于三种状态之一:请求态(pending),完成态(fulfilled),拒绝态(rejected)
2.1.1 当 promise 处于请求状态(pending)时
- 2.1.1.1 promise 可以转为 fulfilled 或 rejected 状态
2.1.2 当 promise 处于完成状态(fulfilled)时
- 2.1.2.1 promise 不能转为任何其他状态
2.1.2.2 必须有一个值,且此值不能改变
2.1.3 当 promise 处于拒绝状态(rejected)时
- 2.1.3.1 promise 不能转为任何其他状态
- 2.1.3.2 必须有一个原因(reason),且此原因不能改变
我们先找需求来完成这一部分代码,一个简单的小架子
// 2.1 状态常量
const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';
// Promise 构造函数
function MyPromise(fn) {
const that = this;
this.state = PENDING;
this.value = null;
this.resolvedCallbacks = [];
this.rejectedCallbacks = [];
function resolve() {if (that.state === PENDING) {}}
function reject() {if (that.state === PENDING) {}}
}
上面这段代码完成了 Promise 构造函数的初步搭建,包含:
- 三个状态的常量声明【请求态、完成态、拒绝态】
- this.state 保管状态、this.value 保存唯一值
- resolvedCallbacks 和 rejectedCallbacks 用于保存 then 中的回调,因为当执行完 Promise 时状态可能还是等待中,这时候应该把 then 中的回调保存起来用于状态改变时使用
- 给 fn 的回调函数 reslove、reject
- resolve、reject 确保只有 ’pedding’ 状态才可以改变状态
下面我们来完成 resolve 和 reject
function resolve(value) {if (that.state === PENDING) {
that.state = RESOLVED
that.value = value
that.resolvedCallbacks.map(cb => cb(that.value))
}
}
function reject(value) {if (that.state === PENDING) {
that.state = REJECTED
that.value = value
that.rejectedCallbacks.map(cb => cb(that.value))
}
}
- 更改 this.state 的状态
- 给 this.value 赋值
- 遍历回调数组并执行,传入 this.value
记下来我们需要来执行新建 Promise 传入的函数体
try {fn(resolve, reject);
} catch (e){reject(e)
}
在执行过程中可能会遇到错误,需要捕获错误传给 reject
Promise/A+【2.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.1 如果 onRejected 不是函数,必须忽略
2.2.2 如果 onFulfilled 是函数:
- 2.2.2.1 此函数必须在 promise 完成 (fulfilled) 后被调用, 并把 promise 的值作为它的第一个参数
- 2.2.2.2 此函数在 promise 完成 (fulfilled) 之前绝对不能被调用
- 2.2.2.2 此函数绝对不能被调用超过一次
2.2.3 如果 onRejected 是函数:
- 2.2.3.1 此函数必须在 promise rejected 后被调用, 并把 promise 的 reason 作为它的第一个参数
- 2.2.3.2 此函数在 promise rejected 之前绝对不能被调用
- 2.2.3.2 此函数绝对不能被调用超过一次
现根据这些要求我们先实现个简单的 then 函数:
MyPromise.prototype.then = function (onFulfilled, onRejected) {
const that = this
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected =
typeof onRejected === 'function'
? onRejected
: r => {throw r}
if (that.state === PENDING) {that.resolvedCallbacks.push(onFulfilled)
that.rejectedCallbacks.push(onRejected)
}
if (that.state === RESOLVED) {onFulfilled(that.value)
}
if (that.state === REJECTED) {onRejected(that.value)
}
}
- 首先判断了传进来的 onFulfilled 和 onRejected 是不是一个函数类型,如果不是就创建一个透传数据的函数
- 判断状态,如果是 ’pending’ 就把函数追加到对应的队列中,如果不是 ’pending’, 直接执行对应状态的函数【resolves => onFulfilled, rejected => onRejected】
如上我们就完成了一个简易版的 promise,但是还不能完全满足 Promise/A+ 规范,接下来我们继续完善
2.2.4 在执行上下文堆栈(execution context)仅包含平台代码之前,不得调用 onFulfilled 和 onRejected
2.2.5 onFulfilled and onRejected must be called as functions (i.e. with no this value)
2.2.6 then 可以在同一个 promise 里被多次调用
— 2.2.6.1 如果 / 当 promise 完成执行(fulfilled), 各个相应的 onFulfilled 回调 必须根据最原始的 then 顺序来调用
— 2.2.6.2 如果 / 当 promise 被拒绝(rejected), 各个相应的 onRejected 回调 必须根据最原始的 then 顺序来调用
2.2.7 then 必须返回一个 promise
promise2 = promise1.then(onFulfilled, onRejected);
- 2.2.7.1 如果 onFulfilled 或 onRejected 返回一个值 x, 运行 Promise Resolution Procedure [[Resolve]](promise2, x)
- 2.2.7.2 如果 onFulfilled 或 onRejected 抛出一个异常 e,promise2 必须被拒绝(rejected)并把 e 当作原因
- 2.2.7.3 如果 onFulfilled 不是一个方法,并且 promise1 已经完成(fulfilled), promise2 必须使用与 promise1 相同的值来完成(fulfiled)
- 2.2.7.4 如果 onRejected 不是一个方法,并且 promise1 已经被拒绝(rejected), promise2 必须使用与 promise1 相同的原因来拒绝(rejected)
首先我们先把 resolve 和 rejected 完善一下
function resolve(value) {if (value instanceof MyPromise) {return value.then(resolve, reject)
}
setTimeout(() => {if (that.state === PENDING) {
that.state = RESOLVED
that.value = value
that.resolvedCallbacks.map(cb => cb(that.value))
}
}, 0)
}
function reject(value) {setTimeout(() => {if (that.state === PENDING) {
that.state = REJECTED
that.value = value
that.rejectedCallbacks.map(cb => cb(that.value))
}
}, 0)
}
参考 2.2.2 和 2.2.3
- 对于 resolve 函数来说,首先需要判断传入的值是否为 Promise 类型
- 为了保证函数执行顺序,需要将两个函数体代码使用 setTimeout 包裹起来
接下来根据规范需求继续完善 then 函数里的代码:
if (that.state === PENDING) {return (promise2 = new MyPromise((resolve, reject) => {that.resolvedCallbacks.push(() => {
try {const x = onFulfilled(that.value);
resoluteProcedure(promise2, x, resolve, reject)
} catch (r) {reject(r);
}
});
that.rejectedCallbacks.push(() => {
try {const x = onRejected(that.value);
resoluteProcedure(promise2, x, resolve, reject)
} catch {reject(r)
}
})
}));
that.reolvedCallbacks.push(onFulfilled);
that.rejectedCallbacks.push(onRejeted);
}
if (that.state === RESOLVED) {return (promise2 = new MyPromise((resolve, reject) => {setTimeout(() => {
try {const x = onFulfilled(that.value)
resolutionProcedure(promise2, x, resolve, reject)
} catch (reason) {reject(reason)
}
})
}))
}
if (that.state === REJECTED) {return (promise2 = new MyPromise((resolve, reject) => {setTimeout(() => {
try {const x = onRejected(that.value)
resolutionProcedure(promise2, x, resolve, reject)
} catch (reason) {reject(reason)
}
})
}))
}
- 首先我们返回了一个新的 Promise 对象,并在 Promise 中传入了一个函数
- 函数的基本逻辑还是和之前一样,往回调数组中 push 函数
- 同样,在执行函数的过程中可能会遇到错误,所以使用了 try…catch 包裹
- 规范规定,执行 onFulfilled 或者 onRejected 函数时会返回一个 x,并且执行 Promise 解决过程,这是为了不同的 Promise 都可以兼容使用,比如 JQuery 的 Promise 能兼容 ES6 的 Promise
Promise/A+【2.3】
2.3 Promise 解决程序
2.3.1 如果 promise 和 x 引用同一个对象,则用 TypeError 作为原因拒绝(reject)promise。
2.3.2 如果 x 是一个 promise, 采用 promise 的状态
- 2.3.2.1 如果 x 是请求状态(pending),promise 必须保持 pending 直到 xfulfilled 或 rejected
- 2.3.2.2 如果 x 是完成态(fulfilled),用相同的值完成 fulfillpromise
- 2.3.2.2 如果 x 是拒绝态(rejected),用相同的原因 rejectpromise
2.3.3 另外,如果 x 是个对象或者方法
- 2.3.3.1 让 x 作为 x.then
- 2.3.3.2 如果取回的 x.then 属性的结果为一个异常 e, 用 e 作为原因 reject 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 拒绝(reject)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 作为 reason 拒绝(reject)promise
- 2.3.3.4 如果 then 不是一个函数,用 x 完成(fulfill)promise
2.3.4 如果 x 既不是对象也不是函数,用 x 完成(fulfill)promise
如果一个 promise 被一个 thenable resolve, 并且这个 thenable 参与了循环的 thenable 环,
[[Resolve]](promise, thenable)的递归特性最终会引起 [[Resolve]](promise, thenable) 再次被调用。
遵循上述算法会导致无限递归,鼓励(但不是必须)实现检测这种递归并用包含信息的 TypeError 作为 reason 拒绝(reject)
这部分规范主要描述了 resolutionProcedure 函数的规范,下面我们来实现 resolutionProcedure 这个函数,我先我么你关注 2.3.4 下面那段话,简单的来说规定了 x 不能与 promise2 相等,这样会发生循环引用的问题,如下栗子:
let p = new MyPromise((resolve, reject) => {resolve(1)
})
let p1 = p.then(value => {return p1})
所以我们需要先进行检测,代码如下:
function resolutionProcedure(promise2, x, resolve, reject) {if (promise2 === x) {return reject(new TypeError('Error'))
}
}
接下来我们判断 x 的类型
if (x instanceof MyPromise) {x.then(function (value) {resolutionProcedure(promise2, value, resolve, reject)
}, reject)
}
如果 x 为 Promise 的话,需要判断以下几个情况:
- 如果 x 处于等待态,Promise 需保持为等待态直至 x 被执行或拒绝
- 如果 x 处于其他状态,则用相同的值处理 Promise
最后我们来完成剩余的代码:
let called = false
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then
if (typeof then === 'function') {
then.call(
x,
y => {if (called) return
called = true
resolutionProcedure(promise2, y, resolve, reject)
},
e => {if (called) return
called = true
reject(e)
}
)
} else {resolve(x)
}
} catch (e) {if (called) return
called = true
reject(e)
}
} else {resolve(x)
}
- 首先创建一个变量 called 用于判断是否已经调用过函数
- 然后判断 x 是否为对象或者函数,如果都不是的话,将 x 传入 resolve 中
- 如果 x 是对象或者函数的话,先把 x.then 赋值给 then,然后判断 then 的类型,如果不是函数类型的话,就将 x 传入 resolve 中
- 如果 then 是函数类型的话,就将 x 作为函数的作用域 this 调用之,并且传递两个回调函数作为参数,第一个参数叫做 resolvePromise,第二个参数叫做 rejectPromise,两个回调函数都需要判断是否已经执行过函数,然后进行相应的逻辑
- 以上代码在执行的过程中如果抛错了,将错误传入 reject 函数中
测试 Promise
有专门的测试脚本可以测试所编写的代码是否符合 PromiseA+ 的规范
首先,在 promise 实现的代码中,增加以下代码:
Promise.defer = Promise.deferred = function () {let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
安装测试脚本:
npm install -g promises-aplus-tests
如果当前的 promise 源码的文件名为 promise.js
那么在对应的目录执行以下命令:
promises-aplus-tests promise.js
共有 872 条测试用例,可以完美通过
符合 Promise/A+ 规范完整代码
这样我们就完成了符合 Promise/A+ 规范的源码,下面是整个代码:
const PENDING = 'pending';
const RESOLVED = 'resolve';
const REJECTED = 'rejected';
function Promise(fn) {
let that = this;
that.status = 'PENDING';
that.value = undefined;
that.resolvedCallbacks = [];
that.rejectedCallbacks = [];
function resolve(value) {if (that.status === 'PENDING') {
that.status = 'RESOLVED';
that.value = value;
that.resolvedCallbacks.forEach(function (fn) {fn();
})
}
}
function reject(value) {if (that.status === 'PENDING') {
that.status = 'REJECTED';
that.value = value;
that.rejectedCallbacks.forEach(function (fn) {fn();
})
}
}
try {fn(resolve, reject);
} catch (e) {reject(e);
}
}
function resolutionProcedure(promise2, x, resolve, reject) {
// 有可能这里返回的 x 是别人的 promise 要尽可能允许其他人乱写
if (promise2 === x) {// 这里应该报一个循环引用的类型错误
return reject(new TypeError('循环引用'));
}
// 看 x 是不是一个 promise promise 应该是一个对象
let called; // 表示是否调用过成功或者失败
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// 可能是 promise 看这个对象中是否有 then 如果有 姑且作为 promise 用 try catch 防止报错
try {
let then = x.then;
if (typeof then === 'function') {
// 成功
then.call(x, function (y) {if (called) return // 避免别人写的 promise 中既走 resolve 又走 reject 的情况
called = true;
resolutionProcedure(promise2, y, resolve, reject)
}, function (err) {if (called) return
called = true;
reject(err);
})
} else {resolve(x) // 如果 then 不是函数 则把 x 作为返回值.
}
} catch (e) {if (called) return
called = true;
reject(e)
}
} else { // 普通值
return resolve(x)
}
}
Promise.prototype.then = function (onFulfilled, onRejected) {
// 成功和失败默认不传给一个函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) {return value;}
onRejected = typeof onRejected === 'function' ? onRejected : function (err) {throw err;}
let that = this;
let promise2; // 新增: 返回的 promise
if (that.status === 'RESOLVED') {promise2 = new Promise(function (resolve, reject) {setTimeout(function () { // 用 setTimeOut 实现异步
try {let x = onFulfilled(that.value); // x 可能是普通值 也可能是一个 promise, 还可能是别人的 promise
resolutionProcedure(promise2, x, resolve, reject) // 写一个方法统一处理
} catch (e) {reject(e);
}
})
})
}
if (that.status === 'REJECTED') {promise2 = new Promise(function (resolve, reject) {setTimeout(function () {
try {let x = onRejected(that.value);
resolutionProcedure(promise2, x, resolve, reject)
} catch (e) {reject(e);
}
})
})
}
if (that.status === 'PENDING') {promise2 = new Promise(function (resolve, reject) {that.resolvedCallbacks.push(function () {setTimeout(function () {
try {let x = onFulfilled(that.value);
resolutionProcedure(promise2, x, resolve, reject)
} catch (e) {reject(e);
}
})
});
that.rejectedCallbacks.push(function () {setTimeout(function () {
try {let x = onRejected(that.value);
resolutionProcedure(promise2, x, resolve, reject)
} catch (e) {reject(e);
}
})
});
})
}
return promise2;
}
Promise.defer = Promise.deferred = function () {let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
module.exports = Promise;
总结
以上就是符合 Promise/A+ 规范的源码,ES6 的 Promise 其实并不是向我们这样通过 js 来实现,而是在底层实现,并且还扩展了很多新的方法:
- Promise.prototype.catch()
- Promise.prototype.finally()
- Promise.all()
- Promise.race()
- Promise.allSettled()
- Promise.any()
- Promise.resolve()
- Promise.reject()
- Promise.try()
这里就不一一介绍啦,大家可以参考阮一峰老师的文章 ES6 入门 -Promise 对象
这篇文章给大家讲解的 Promise/A+ 规范的源码,希望大家能多读多写,深刻的体会一下源码的思想,对以后的开发也很有帮助。
感谢大家的阅读,觉得还不错,辛苦点一下关注,谢谢!