我们使用类的方式来实现一个 Promise
概念及作用
在《JavaScript 高级程序设计第四版》中是这样描述 Promise的:期约(也就是 Promise)是对尚不存在结果的一个替身。
在ES6 中增加引用数据类型 Promise,通过 new关键字来实例化。它需要传入一个执行器(executor)函数作为参数(必传,否则会出错)。
所以这里我们先定义一个 Promise类:
class Promise {
constructor(executor) {
if (isUndef(executor) || !isFunction(executor)) {
throw new TypeError('Promise resolver undefined is not a function')
}
}
}
它有两个作用:
-
抽象地表示一个异步操作
-
状态代表
Promise是否完成- 成功就会有一个私有的内部值(value)
- 拒绝就会有一个私有的内部理由(reason)
无论是值还是理由,都是包含原始值或者对象的不可修改的引用。二者是可选的,且默认值是
undefined
class Promise {
constructor(executor) {
if (isUndef(executor) || !isFunction(executor)) {
throw new TypeError('Promise resolver undefined is not a function')
}
this.value = undefined;
this.reason = undefined;
}
}
状态
Promise是一个有状态的对象,一共三种状态:
pending待定:Promise 的初始状态fulfilled完成rejected拒绝
这三种状态的关系为:
pending-->fulfilledpending-->rejected
这两种落定过程都是不可逆的。
Promise的状态是私有的,不能直接通过 JS 检测到。
那这里的状态实现如下:
const PENGDING = 'PENDING'
const FULLFILLED = 'FULLFILLED'
const REJECT = 'REJECT'
class Promise {
constructor(executor) {
if (isUndef(executor) || !isFunction(executor)) {
throw new TypeError('Promise resolver undefined is not a function')
}
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
}
}
执行器函数
由于 Promise的状态是私有的,所以只能在内部进行操作。它将会在执行器函数中完成。执行器函数的职责有两个:
- 初始化
Promise的异步行为 - 控制状态的最终转换--它的实现又是通过调用它的两个函数参数实现的
resolve:调用后状态切换为完成;reject:调用后状态切换为拒绝,并会抛出错误;
const PENGDING = 'PENDING'
const FULLFILLED = 'FULLFILLED'
const REJECT = 'REJECT'
class Promise {
constructor(executor) {
if (isUndef(executor) || !isFunction(executor)) {
throw new TypeError('Promise resolver undefined is not a function')
}
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
const resolve = (value) => {
if (this.status === PENDING) {
this.status = FULLFILLED
this.value = value
}
}
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECT
this.reason = reason
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
}
需要知道的是:执行器函数是同步执行的,这是因为执行器函数本身是 Promise的初始化程序
⚠️注意:
这里的resolve和reject方法使用箭头函数的意义,并不是为了简便,更重要的在于this指向问题。
静态方法
这里只写几个常用的方法
Promise.resolve
Promise并非一开始就必须处于待定状态,然后调用执行器函数才能改变最终态。可以调用其静态 resolve()方法实例化一个已完成的 Promise。
- 使用这个方法可以将任何一个值都转换为一个
Promise。 - 如果传入了一个
Promise,那它的行为就类似于一个空包装,直接返回这个Promise - 如果这个值是
thenable,即带有then方法的值,返回的promise对象会在其内部调用该值的then方法,并将resolve和reject作为参数传入
⚠️注意:该方法能够包装任何非 Promise 值,包括错误对象,并将其转为已完成的 Promise
Promise.resolve = function (value) {
if (value instanceof Promise) {
return value
}
if (
(value instanceof Object) &&
(value.then) &&
(typeof value.then === 'function')
) {
return new Promise((resolve, reject) => {
value.then(resolve, reject)
})
}
return new Promise(resolve => {
resolve(value)
})
}
Promise.reject
同上一个方法类似,它会实例化一个被拒绝的 Promise并抛出一个异步错误。且这个错误不能通过 try/catch进行捕获,只能通过 reject来捕获。
Promise.reject = function(reason) {
return new Promise((resolve, reject) => {
reject(reason)
})
}
Promise.all
all主要是将一组 Promise全部解决完成之后再返回一个 Promise实例,结果为所有 Promise回调结果的集合(按照迭代器顺序)。
在 MDN 上我们可以知道该方法的返回值需要遵循以下三点:
- 如果传入的参数是一个空的可迭代对象(可以理解为一个空数组之类的),那么返回一个已完成(already resolved)状态的 Promise。这里是一个同步的操作
- 如果传入的参数包含不是
Promise的值,那么最终它将会被按照顺序放在最终的返回集合当中 - 假如其中有一个
Promise被reject掉了,那整个过程就会抛出异常,而异常信息就是第一个被reject掉的信息。
被 reject之后再被 reject的 Promise不会影响到最终的 reject reason。且其他 Promise的reject操作都会被静默处理掉
Promise.all = function (values) {
return new Promise((resolve, reject) => {
const array = []
let index = 0;
if (!Array.isArray(values)) {
reject(new TypeError('Argument is not iterable'))
}
if (values.length === 0) {
return resolve(values)
}
function processData(key, data) {
array[key] = data
if (++index === values.length) {
resolve(array)
}
}
values.forEach((item, index) => {
Promise.resolve(item).then(data => {
processData(index, data)
}, reject)
})
})
}
Promise.race
这个方法返回了一个包装 Promise,是一组集合中最先解决或者失败的 Promise镜像。它需要遵循以下两点:
- 只要是第一个状态落定的
Promise,就会包装其解决值或者拒绝理由并返回一个新的Promise - 如果传入的参数是空集合,那么返回的
Promise的状态将永远Pending - 如果迭代包含一个或者多个非承诺值和/或已解决/拒绝的承诺,则
Promise.race将解析为迭代中找到的第一个值
Promise.race = function (values) {
return new Promise((resolve, reject) => {
if (!Array.isArray(values)) {
reject(new TypeError('Argument is not iterable'))
}
if (values.length > 0) {
values.forEach(item => {
Promise.resolve(item).then(resolve, reject)
})
}
})
}
实例方法
我们熟知的 .then、.catch、.finally方法都挂载在 Promise的原型对象上。我们一个个来看。
then
它可以说是 Promise上处理程序的主要方法,接收两个参数:
onResolved:即进入到完成状态时会调用的方法,传入值obRejected:即进入到拒绝状态时会调用的方法,传入理由
注意这两个参数都是可选的。且传给 then函数的任何非函数类型的参数都会被静默忽略(即会把将值或者失败理由返回,毕竟是要处理数据的)。而如果只想提供 obRejected参数,那么就要在onResolved参数的位置上传入 null或者 undefined。这有助于避免在内存中创建出多余的对象出来。
Promise.prototype.then = function (onResolved, obRejected) {
// 判断两个参数是可选参数
onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : data => data
onReject = typeof onReject === 'function' ? onReject : err => { throw err; }
if (this.status === FULLFILLED) {
onResolved(this.value)
}
if (this.status === REJECT) {
obRejected(this.reason)
}
}
上面这部分的代码块其实可以看出来:Promise只会执行 成功态和拒绝态其中的一个。
而还需要注意的一个重点是:then方法会返回一个新的 Promise实例。
Promise.prototype.then = function (onResolved, obRejected) {
return new Promise((resolve, reject) => {})
}
那么返回一个新的 Promise 实例也是 Promises/A+中的规范,也正因为有这一点,所以我们才可以使用链式调用。
而这个新的 Promise我们暂且将其称为 promise2,原来的成为 promise,规范中定义 promise1无论是 resolve还是 reject(返回一个值 x)都会执行 Promise 的解决过程:[[Resolve]](promise2, x),只有这俩里抛出异常才会被拒绝执行。
那这里的 Promise 解决过程要如何实现呢?
- 首先根据规范我们可以知道这是一个抽象的操作,它主要针对
promise2和promise1中落定状态的返回值x之间的关系 - 解析
promise2和x之间的关系及x的类型,判断使用promise2里的resolve和reject-
x和promise2相等,TypeError抛出异常 -
如果
x是一个对象或者函数时,有以下几种场景:- 假设
x.then是一个函数,那么x就是一个promise对象,针对这种场景,还需要防止多次调用成功和失败的方法 - 返回它可能就是一个正常的函数或者对象,我们只需要直接
resolve掉即可
- 假设
-
如果
x是一个普通值,则以x为参数执行promise
-
// 定义该方法名为:resolvePromise
/**
* 根据 x 和 promise2,使用 promise2 里的 resolve 和 reject 完成 then 调用之后的逻辑
* @param {Promise} promise2 promise1.then 返回的新 Promise 对象
* @param {[type]} x promise1 中 onFullfilled 或者 onReject 的返回值
* @param {[type]} resolve promise2 中的 resolve 方法
* @param {[type]} reject promise2 中的 reject 方法
*/
function resolvePromise(
promise2,
x,
resolve,
reject
) {
let called;
if ((typeof x === 'object' && x !== null) || (typeof x === 'function')) {
try {
const then = x.then;
if (typeof then === 'function') {
// 当 x 是 promise 的时候递归调用该方法
then.call(
x,
y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject)
},
r => {
if (called) return;
reject(r)
}
)
} else {
if (called) return;
called = true;
resolve(x)
}
} catch (error) {
if (called) return;
called = true
reject(error)
}
} else {
if (called) return
called = true
resolve(x);
}
}
完成之后,我们在异步的部分来使用。
catch
catch方法用于给 Promise添加一个拒绝处理程序。而它实际上就是一个语法糖,调用它就相当于调用 Promise.prototype.then(null, onRejected)。也就是说:
promise1.then(val => {
console.log('then', val)
})
.catch(err => {
console.log('catch', err)
})
// 等价于
promise1.then(
null, // 或者 undefined
error => { console.log(error) }
)
所以呢,catch的最终实现:
Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected)
}
finally
finally方法用于给 Promise添加 onFinally处理程序,这个处理程序在 Promise转换为完成或者拒绝状态时都会执行。
不过它没有办法知道 Promise 的状态是完成还是拒绝,所以这个方法主要用来添加清理代码。
onFinally方法被设计为一个与状态无关的方法,所以一般情况下它都会将其上一层的 Promise原样的返回出去。所以 onFinally方法是不接受任何参数的。
Promise.prototype.finally = function (cb) {
return this.then(cb, cb)
}
可以看到其实在实现该方法的过程中我们并没有对当前的 Promise状态进行判断。因为此时,如果返回的是一个 Pending的 Promise或者 onFinally处理程序抛出了错误(显示地抛出或者返回了一个 reject),都会返回相应的 Promise。
这种 Pending的场景其实并不常见,因为只要 Promise一解决,新的 Promise仍然会原样的返回初始的 Promise
const p1 = Promise.resolve('foo')
// 忽略完成的值
const p2 = p1.finally(
() => new Promise((resolve, reject) => setTimeout(() => resolve('bar'), 100))
)
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(() => setTimeout(console.log, 0, p2), 200)
// 200ms 后
// Promise <resolved>: foo
Promise 中的异步
then 中的异步
到现在为止,我们的 Promise都还只是同步的,并没有任何异步的部分,但是实际上原生的 Promise中 then方法里的函数参数都会被包裹在一个异步操作中执行。
添加异步的原因也很明确,就是为了保证返回的新 Promise已经是处于生成状态的
Promise.prototype.then = function (onResolved, obRejected) {
onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : data => data
onReject = typeof onReject === 'function' ? onReject : err => { throw err; }
return new Promise((resolve, reject) => {
if (this.status === FULLFILLED) {
setTimeout(() => {
onResolved(this.value)
})
}
if (this.status === REJECT) {
setTimeout(() => {
obRejected(this.reason)
})
}
})
}
这里其实也是
Promises/A+规范里提到的(2.2.4):
onResolved和obRejected必须在执行环境堆栈仅包含平台代码时才能被调用。在这里的平台代码指的是引擎、环境以及 Promise 的实施代码。它要确保这两个方法在调用时,是异步的,且应该在
then方法被调用的那一轮事件循环之后的新执行栈中进行。这个事件队列可以采用宏任务(macro-task) 机制,比如
setTimeout或者setImmediate,也可以使用微任务(micro-task) 机制来实现,比如MutationObserver或者process.nextTick。而我们这里就使用了
setTimeout
还有一点需要注意的是,当 Promise的状态为 pending时,我们这里并没有兜底方案,所以这里应该还需要加入 pending状态时的处理:
pending状态时,我们可以把then中的函数都存储到一个数组中(多次调用then)- 当状态落定转换之后,在对应的
resolve和reject函数中循环执行数组中的事件 - 这实际上是一个发布-订阅的过程
结合 then部分说到的 resolvePromise方法来实现最终的效果(注意,前面说过如果抛出异常则 promise2必须拒绝执行,所以需要使用 try..catch来完成这部分的实现):
class Promise {
constructor(executor) {
if (isUndef(executor) || !isFunction(executor)) {
throw new TypeError('Promise resolver undefined is not a function')
}
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [] // 成功回调函数
this.onRejectedCallbacks = [] // 失败回调函数
const resolve = (value) => {
if (this.status === PENDING) {
this.status = FULLFILLED
this.value = value
// 发布
this.onResolvedCallbacks.forEach(fn => fn())
}
}
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECT
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
}
Promise.prototype.then = function (onResolved, obRejected) {
onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : data => data
onReject = typeof onReject === 'function' ? onReject : err => { throw err; }
const promise = new Promise((resolve, reject) => {
if (this.status === FULLFILLED) {
setTimeout(() => {
try {
const x = onResolved(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
}, 0)
}
if (this.status === REJECT) {
setTimeout(() => {
try {
const x = obRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
}, 0)
}
if (this.status === PENDING) {
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = obRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
}, 0)
})
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onResolved(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
}, 0)
})
}
})
return promise
}
宏任务和微任务
因为事件循环机制的存在,不同的异步任务被分为两类:宏任务和微任务。 而在我们的 Promise 中:
- 宏任务:
setTimeout——进入到宏任务的队列中 - 微任务:
.then——进入到微任务的队列中
那么对于宏任务和微任务是怎么执行的呢?
如果当前的执行栈已经执行完同步的任务,那么主线程就会先查看微任务队列,如果队列中有任务则会执行队列中的任务直至清空队列;如果不存在,就会去查看宏任务队列,将其里面的事件按照顺序依次加入当前的执行栈中。这是一个循环往复的过程(事件循环 Event Loop)。
源码
const PENGDING = 'PENDING'
const FULLFILLED = 'FULLFILLED'
const REJECT = 'REJECT'
// 定义该方法名为:resolvePromise
/**
* 根据 x 和 promise2,使用 promise2 里的 resolve 和 reject 完成 then 调用之后的逻辑
* @param {Promise} promise2 promise1.then 返回的新 Promise 对象
* @param {[type]} x promise1 中 onFullfilled 或者 onReject 的返回值
* @param {[type]} resolve promise2 中的 resolve 方法
* @param {[type]} reject promise2 中的 reject 方法
*/
function resolvePromise(
promise2,
x,
resolve,
reject
) {
let called;
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
if (
(typeof x === 'object' && x !== null) ||
(typeof x === 'function')
) {
try {
const 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;
reject(r)
}
)
} else {
if (called) return;
called = true;
resolve(x)
}
} catch (error) {
if (called) return;
called = true
reject(error)
}
} else {
if (called) return
called = true
resolve(x);
}
}
class Promise {
constructor(executor) {
this.status = PENGDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.status === PENGDING) {
this.status = FULLFILLED
this.value = value
this.onResolvedCallbacks.forEach(fn => fn())
}
}
const reject = (reason) => {
if (this.status === PENGDING) {
this.status = REJECT
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
static resolve (value) {
if (value instanceof Promise) {
return value
}
if (
(value instanceof Object) &&
(value.then) &&
(typeof value.then === 'function')
) {
return new Promise((resolve, reject) => {
value.then(resolve, reject)
})
}
return new Promise(resolve => {
resolve(value)
})
}
static reject (reason) {
return new Promise((resolve, reject) => {
reject(reason)
})
}
static all (values) {
return new Promise((resolve, reject) => {
const array = []
let index = 0;
if (!Array.isArray(values)) {
reject(new TypeError('Argument is not iterable'))
}
if (values.length === 0) {
return resolve(values)
}
function processData(key, data) {
array[key] = data
if (++index === values.length) {
resolve(array)
}
}
values.forEach((item, index) => {
Promise.resolve(item).then(data => {
processData(index, data)
}, reject)
})
})
}
static race (values) {
return new Promise((resolve, reject) => {
if (!Array.isArray(values)) {
reject(new TypeError('Argument is not iterable'))
}
if (values.length > 0) {
values.forEach(item => {
Promise.resolve(item).then(resolve, reject)
})
}
})
}
}
Promise.prototype.then = function (onFullfilled, onReject) {
onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : data => data
onReject = typeof onReject === 'function' ? onReject : err => { throw err; }
const promise2 = new Promise((resolve, reject) => {
if (this.status === FULLFILLED) {
setTimeout(() => {
try {
const x = onFullfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
}
if (this.status === REJECT) {
setTimeout(() => {
try {
const x = onReject(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
}
if (this.status === PENGDING) {
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onReject(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFullfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
}
})
return promise2
}
Promise.prototype.catch = function (onReject) {
return this.then(null, onReject)
}
Promise.prototype.finally = function (cb) {
return this.then(cb, cb)
}
Promise.deferred = function () {
let dfd = {}
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}
module.exports = Promise
跑测试
要测试我们自己的 Promise是否符合 Promises/A+规范,需要使用官方提供的测试工具 promises-aplus-tests来进行测试。除此之外,还需要实现一个 deferred的静态方法,否则测试也跑不起来。
deferred
Promise.deferred = function () {
let dfd = {}
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}
安装测试工具
npm install promises-aplus-tests -D
然后在项目目录中执行 promises-alpus-tests promise.js进行测试。当然我们也可以将命令直接配置到 package.json中去。
跑一下:
就这样,测试跑通。
Promise 的
any方法和allSettled没写,后面再说吧