本文首发于个人我的网站:litgod.net前端
关于手写 Promise,想必你们都十分熟悉。基本上如今不论是大厂仍是小厂,手写 promise 已经成为了面试必考知识点。据说你还不太会?那么走着,带你从零开始解锁 Promise!node
首先,咱们以常见的 Promise 面试题为切入点,咱们看看面试官们都爱考什么:ios
这几个问题由浅入深,咱们一个一个来看:git
在 Promise 出现之前,在咱们处理多个异步请求嵌套时,代码每每是这样的。。。github
let fs = require('fs')
fs.readFile('./name.txt','utf8',function(err,data){
fs.readFile(data, 'utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
console.log(data);
})
})
})
复制代码
为了拿到回调的结果,咱们必须一层一层的嵌套,能够说是至关恶心了。并且基本上咱们还要对每次请求的结果进行一系列的处理,使得代码变的更加难以阅读和难以维护,这就是传说中臭名昭著的回调地狱~产生回调地狱的缘由归结起来有两点:面试
缘由分析出来后,那么问题的解决思路就很清晰了:ajax
Promise 正是用一种更加友好的代码组织方式,解决了异步嵌套的问题。npm
咱们来看看上面的例子用 Promise 实现是什么样的:编程
let fs = require('fs')
function read(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) reject(err);
resolve(data);
})
})
}
read('./name.txt').then((data)=>{
return read(data)
}).then((data)=>{
return read(data)
}).then((data)=>{
console.log(data);
},err=>{
console.log(err);
})
复制代码
臃肿的嵌套变得线性多了有木有?没错,他就是咱们的异步神器 Promise!axios
让咱们再次回归刚才的问题,Promise为咱们解决了什么问题? 在传统的异步编程中,若是异步之间存在依赖关系,就须要经过层层嵌套回调的方式知足这种依赖,若是嵌套层数过多,可读性和能够维护性都会变得不好,产生所谓的“回调地狱”,而 Promise 将嵌套调用改成链式调用,增长了可阅读性和可维护性。也就是说,Promise 解决的是异步编码风格的问题。 那 Promise 的业界实现都有哪些呢? 业界比较著名的实现 Promise 的类库有 bluebird、Q、ES6-Promise。
咱们想要手写一个 Promise,就要遵循 Promise/A+ 规范,业界全部 Promise 的类库都遵循这个规范。
其实 Promise/A+ 规范对如何实现一个符合标准的 Promise 类库已经阐述的很详细了。每一行代码在 Promise/A+ 规范中都有迹可循,因此在下面的实现的过程当中,我会尽量的将代码和 Promise/A+ 规范一一对应起来。
下面开始步入正题啦~
咱们先来回顾下最简单的 Promise 使用方式:
const p1 = new Promise((resolve, reject) => {
console.log('create a promise');
resolve('成功了');
})
console.log("after new promise");
const p2 = p1.then(data => {
console.log(data)
throw new Error('失败了')
})
const p3 = p2.then(data => {
console.log('success', data)
}, err => {
console.log('faild', err)
})
复制代码
控制台输出:
"create a promise"
"after new promise"
"成功了"
"faild Error: 失败了"
复制代码
以上简单介绍了 Promise 的一些主要的使用方法,结合 Promise/A+ 规范,咱们能够分析出 Promise 的基本特征:
- promise 有三个状态:
pending
,fulfilled
,orrejected
;「规范 Promise/A+ 2.1」new promise
时, 须要传递一个executor()
执行器,执行器当即执行;executor
接受两个参数,分别是resolve
和reject
;- promise 的默认状态是
pending
;- promise 有一个
value
保存成功状态的值,能够是undefined/thenable/promise
;「规范 Promise/A+ 1.3」- promise 有一个
reason
保存失败状态的值;「规范 Promise/A+ 1.5」- promise 只能从
pending
到rejected
, 或者从pending
到fulfilled
,状态一旦确认,就不会再改变;- promise 必须有一个
then
方法,then 接收两个参数,分别是 promise 成功的回调 onFulfilled, 和 promise 失败的回调 onRejected;「规范 Promise/A+ 2.2」- 若是调用 then 时,promise 已经成功,则执行
onFulfilled
,参数是promise
的value
;- 若是调用 then 时,promise 已经失败,那么执行
onRejected
, 参数是promise
的reason
;- 若是 then 中抛出了异常,那么就会把这个异常做为参数,传递给下一个 then 的失败的回调
onRejected
;
按照上面的特征,咱们试着勾勒下 Promise 的形状:
// 三个状态:PENDING、FULFILLED、REJECTED
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
class Promise {
constructor(executor) {
// 默认状态为 PENDING
this.status = PENDING;
// 存放成功状态的值,默认为 undefined
this.value = undefined;
// 存放失败状态的值,默认为 undefined
this.reason = undefined;
// 调用此方法就是成功
let resolve = (value) => {
// 状态为 PENDING 时才能够更新状态,防止 executor 中调用了两次 resovle/reject 方法
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
}
}
// 调用此方法就是失败
let reject = (reason) => {
// 状态为 PENDING 时才能够更新状态,防止 executor 中调用了两次 resovle/reject 方法
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
}
}
try {
// 当即执行,将 resolve 和 reject 函数传给使用者
executor(resolve,reject)
} catch (error) {
// 发生异常时执行失败逻辑
reject(error)
}
}
// 包含一个 then 方法,并接收两个参数 onFulfilled、onRejected
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value)
}
if (this.status === REJECTED) {
onRejected(this.reason)
}
}
}
复制代码
写完代码咱们能够测试一下:
const promise = new Promise((resolve, reject) => {
resolve('成功');
}).then(
(data) => {
console.log('success', data)
},
(err) => {
console.log('faild', err)
}
)
复制代码
控制台输出:
"success 成功"
复制代码
如今咱们已经实现了一个基础版的 Promise,可是还不要高兴的太早噢,这里咱们只处理了同步操做的 promise。若是在 executor()
中传入一个异步操做的话呢,咱们试一下:
const promise = new Promise((resolve, reject) => {
// 传入一个异步操做
setTimeout(() => {
resolve('成功');
},1000);
}).then(
(data) => {
console.log('success', data)
},
(err) => {
console.log('faild', err)
}
)
复制代码
执行测试脚本后发现,promise 没有任何返回。
由于 promise 调用 then 方法时,当前的 promise 并无成功,一直处于 pending 状态。因此若是当调用 then 方法时,当前状态是 pending,咱们须要先将成功和失败的回调分别存放起来,在executor()
的异步任务被执行时,触发 resolve 或 reject,依次调用成功或失败的回调。
结合这个思路,咱们优化一下代码:
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
// 存放成功的回调
this.onResolvedCallbacks = [];
// 存放失败的回调
this.onRejectedCallbacks= [];
let resolve = (value) => {
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// 依次将对应的函数执行
this.onResolvedCallbacks.forEach(fn=>fn());
}
}
let reject = (reason) => {
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// 依次将对应的函数执行
this.onRejectedCallbacks.forEach(fn=>fn());
}
}
try {
executor(resolve,reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value)
}
if (this.status === REJECTED) {
onRejected(this.reason)
}
if (this.status === PENDING) {
// 若是promise的状态是 pending,须要将 onFulfilled 和 onRejected 函数存放起来,等待状态肯定后,再依次将对应的函数执行
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
});
// 若是promise的状态是 pending,须要将 onFulfilled 和 onRejected 函数存放起来,等待状态肯定后,再依次将对应的函数执行
this.onRejectedCallbacks.push(()=> {
onRejected(this.reason);
})
}
}
}
复制代码
测试一下:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功');
},1000);
}).then(
(data) => {
console.log('success', data)
},
(err) => {
console.log('faild', err)
}
)
复制代码
控制台等待 1s
后输出:
"success 成功"
复制代码
ok!大功告成,异步问题已经解决了!
熟悉设计模式的同窗,应该意识到了这实际上是一个发布订阅模式,这种收集依赖 -> 触发通知 -> 取出依赖执行
的方式,被普遍运用于发布订阅模式的实现。
咱们都知道,promise 的优点在于能够链式调用。在咱们使用 Promise 的时候,当 then 函数中 return 了一个值,不论是什么值,咱们都能在下一个 then 中获取到,这就是所谓的then 的链式调用。并且,当咱们不在 then 中放入参数,例:promise.then().then()
,那么其后面的 then 依旧能够获得以前 then 返回的值,这就是所谓的值的穿透。那具体如何实现呢?简单思考一下,若是每次调用 then 的时候,咱们都从新建立一个 promise 对象,并把上一个 then 的返回结果传给这个新的 promise 的 then 方法,不就能够一直 then 下去了么?那咱们来试着实现一下。这也是手写 Promise 源码的重中之重,因此,打起精神来,重头戏来咯!
有了上面的想法,咱们再结合 Promise/A+ 规范梳理一下思路:
- then 的参数
onFulfilled
和onRejected
能够缺省,若是onFulfilled
或者onRejected
不是函数,将其忽略,且依旧能够在下面的 then 中获取到以前返回的值;「规范 Promise/A+ 2.2.一、2.2.1.一、2.2.1.2」- promise 能够 then 屡次,每次执行完 promise.then 方法后返回的都是一个“新的promise";「规范 Promise/A+ 2.2.7」
- 若是 then 的返回值 x 是一个普通值,那么就会把这个结果做为参数,传递给下一个 then 的成功的回调中;
- 若是 then 中抛出了异常,那么就会把这个异常做为参数,传递给下一个 then 的失败的回调中;「规范 Promise/A+ 2.2.7.2」
- 若是 then 的返回值 x 是一个 promise,那么会等这个 promise 执行完,promise 若是成功,就走下一个 then 的成功;若是失败,就走下一个 then 的失败;若是抛出异常,就走下一个 then 的失败;「规范 Promise/A+ 2.2.7.三、2.2.7.4」
- 若是 then 的返回值 x 和 promise 是同一个引用对象,形成循环引用,则抛出异常,把异常传递给下一个 then 的失败的回调中;「规范 Promise/A+ 2.3.1」
- 若是 then 的返回值 x 是一个 promise,且 x 同时调用 resolve 函数和 reject 函数,则第一次调用优先,其余全部调用被忽略;「规范 Promise/A+ 2.3.3.3.3」
咱们将代码补充完整:
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
const resolvePromise = (promise2, x, resolve, reject) => {
// 本身等待本身完成是错误的实现,用一个类型错误,结束掉 promise Promise/A+ 2.3.1
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
// Promise/A+ 2.3.3.3.3 只能调用一次
let called;
// 后续的条件要严格判断 保证代码能和别的库一块儿使用
if ((typeof x === 'object' && x != null) || typeof x === 'function') {
try {
// 为了判断 resolve 过的就不用再 reject 了(好比 reject 和 resolve 同时调用的时候) Promise/A+ 2.3.3.1
let then = x.then;
if (typeof then === 'function') {
// 不要写成 x.then,直接 then.call 就能够了 由于 x.then 会再次取值,Object.defineProperty Promise/A+ 2.3.3.3
then.call(x, y => { // 根据 promise 的状态决定是成功仍是失败
if (called) return;
called = true;
// 递归解析的过程(由于可能 promise 中还有 promise) Promise/A+ 2.3.3.3.1
resolvePromise(promise2, y, resolve, reject);
}, r => {
// 只要失败就失败 Promise/A+ 2.3.3.3.2
if (called) return;
called = true;
reject(r);
});
} else {
// 若是 x.then 是个普通值就直接返回 resolve 做为结果 Promise/A+ 2.3.3.4
if (called) return;
called = true;
resolve(x);
}
} catch (e) {
// Promise/A+ 2.3.3.2
if (called) return;
called = true;
reject(e)
}
} else {
// 若是 x 是个普通值就直接返回 resolve 做为结果 Promise/A+ 2.3.4
resolve(x)
}
}
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks= [];
let resolve = (value) => {
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach(fn=>fn());
}
}
let reject = (reason) => {
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn=>fn());
}
}
try {
executor(resolve,reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
//解决 onFufilled,onRejected 没有传值的问题
//Promise/A+ 2.2.1 / Promise/A+ 2.2.5 / Promise/A+ 2.2.7.3 / Promise/A+ 2.2.7.4
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
//由于错误的值要让后面访问到,因此这里也要跑出个错误,否则会在以后 then 的 resolve 中捕获
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
// 每次调用 then 都返回一个新的 promise Promise/A+ 2.2.7
let promise2 = new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
//Promise/A+ 2.2.2
//Promise/A+ 2.2.4 --- setTimeout
setTimeout(() => {
try {
//Promise/A+ 2.2.7.1
let x = onFulfilled(this.value);
// x多是一个proimise
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
//Promise/A+ 2.2.7.2
reject(e)
}
}, 0);
}
if (this.status === REJECTED) {
//Promise/A+ 2.2.3
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e)
}
}, 0);
}
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e)
}
}, 0);
});
this.onRejectedCallbacks.push(()=> {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0);
});
}
});
return promise2;
}
}
复制代码
测试一下:
const promise = new Promise((resolve, reject) => {
reject('失败');
}).then().then().then(data=>{
console.log(data);
},err=>{
console.log('err',err);
})
复制代码
控制台输出:
"失败 err"
复制代码
至此,咱们已经完成了 promise 最关键的部分:then 的链式调用和值的穿透。搞清楚了 then 的链式调用和值的穿透,你也就搞清楚了 Promise。
Promise/A+规范提供了一个专门的测试脚本,能够测试所编写的代码是否符合Promise/A+的规范。
首先,在 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
复制代码
promises-aplus-tests 中共有 872 条测试用例。以上代码,能够完美经过全部用例。
虽然上述的 promise 源码已经符合 Promise/A+ 的规范,可是原生的 Promise 还提供了一些其余方法,如:
下面具体说一下每一个方法的实现:
默认产生一个成功的 promise。
static resolve(data){
return new Promise((resolve,reject)=>{
resolve(data);
})
}
复制代码
这里须要注意的是,promise.resolve 是具有等待功能的。若是参数是 promise 会等待这个 promise 解析完毕,在向下执行,因此这里须要在 resolve 方法中作一个小小的处理:
let resolve = (value) => {
// ======新增逻辑======
// 若是 value 是一个promise,那咱们的库中应该也要实现一个递归解析
if(value instanceof Promise){
// 递归解析
return value.then(resolve,reject)
}
// ===================
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach(fn=>fn());
}
}
复制代码
测试一下:
Promise.resolve(new Promise((resolve, reject) => {
setTimeout(() => {
resolve('ok');
}, 3000);
})).then(data=>{
console.log(data,'success')
}).catch(err=>{
console.log(err,'error')
})
复制代码
控制台等待 3s
后输出:
"ok success"
复制代码
默认产生一个失败的 promise,Promise.reject 是直接将值变成错误结果。
static reject(reason){
return new Promise((resolve,reject)=>{
reject(reason);
})
}
复制代码
Promise.prototype.catch 用来捕获 promise 的异常,就至关于一个没有成功的 then。
Promise.prototype.catch = function(errCallback){
return this.then(null,errCallback)
}
复制代码
finally 表示不是最终的意思,而是不管如何都会执行的意思。 若是返回一个 promise 会等待这个 promise 也执行完毕。若是返回的是成功的 promise,会采用上一次的结果;若是返回的是失败的 promise,会用这个失败的结果,传到 catch 中。
Promise.prototype.finally = function(callback) {
return this.then((value)=>{
return Promise.resolve(callback()).then(()=>value)
},(reason)=>{
return Promise.resolve(callback()).then(()=>{throw reason})
})
}
复制代码
测试一下:
Promise.resolve(456).finally(()=>{
return new Promise((resolve,reject)=>{
setTimeout(() => {
resolve(123)
}, 3000);
})
}).then(data=>{
console.log(data,'success')
}).catch(err=>{
console.log(err,'error')
})
复制代码
控制台等待 3s
后输出:
"456 success"
复制代码
promise.all 是解决并发问题的,多个异步并发获取最终的结果(若是有一个失败则失败)。
Promise.all = function(values) {
if (!Array.isArray(values)) {
const type = typeof values;
return new TypeError(`TypeError: ${type} ${values} is not iterable`)
}
return new Promise((resolve, reject) => {
let resultArr = [];
let orderIndex = 0;
const processResultByKey = (value, index) => {
resultArr[index] = value;
if (++orderIndex === values.length) {
resolve(resultArr)
}
}
for (let i = 0; i < values.length; i++) {
let value = values[i];
if (value && typeof value.then === 'function') {
value.then((value) => {
processResultByKey(value, i);
}, reject);
} else {
processResultByKey(value, i);
}
}
});
}
复制代码
测试一下:
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('ok1');
}, 1000);
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('ok2');
}, 1000);
})
Promise.all([1,2,3,p1,p2]).then(data => {
console.log('resolve', data);
}, err => {
console.log('reject', err);
})
复制代码
控制台等待 1s
后输出:
"resolve [ 1, 2, 3, 'ok1', 'ok2' ]"
复制代码
Promise.race 用来处理多个请求,采用最快的(谁先完成用谁的)。
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
// 一块儿执行就是for循环
for (let i = 0; i < promises.length; i++) {
let val = promises[i];
if (val && typeof val.then === 'function') {
val.then(resolve, reject);
} else { // 普通值
resolve(val)
}
}
});
}
复制代码
特别须要注意的是:由于Promise 是没有中断方法的,xhr.abort()、ajax 有本身的中断方法,axios 是基于 ajax 实现的;fetch 基于 promise,因此他的请求是没法中断的。
这也是 promise 存在的缺陷,咱们可使用 race 来本身封装中断方法:
function wrap(promise) {
// 在这里包装一个 promise,能够控制原来的promise是成功仍是失败
let abort;
let newPromise = new Promise((resolve, reject) => { // defer 方法
abort = reject;
});
let p = Promise.race([promise, newPromise]); // 任何一个先成功或者失败 就能够获取到结果
p.abort = abort;
return p;
}
const promise = new Promise((resolve, reject) => {
setTimeout(() => { // 模拟的接口调用 ajax 确定有超时设置
resolve('成功');
}, 1000);
});
let newPromise = wrap(promise);
setTimeout(() => {
// 超过3秒 就算超时 应该让 proimise 走到失败态
newPromise.abort('超时了');
}, 3000);
newPromise.then((data => {
console.log('成功的结果' + data)
})).catch(e => {
console.log('失败的结果' + e)
})
复制代码
控制台等待 1s
后输出:
"成功的结果成功"
复制代码
promisify 是把一个 node 中的 api 转换成 promise 的写法。 在 node 版本 12.18 以上,已经支持了原生的 promisify 方法:const fs = require('fs').promises
。
const promisify = (fn) => { // 典型的高阶函数 参数是函数 返回值是函数
return (...args)=>{
return new Promise((resolve,reject)=>{
fn(...args,function (err,data) { // node中的回调函数的参数 第一个永远是error
if(err) return reject(err);
resolve(data);
})
});
}
}
复制代码
若是想要把 node 中全部的 api 都转换成 promise 的写法呢:
const promisifyAll = (target) =>{
Reflect.ownKeys(target).forEach(key=>{
if(typeof target[key] === 'function'){
// 默认会将原有的方法 所有增长一个 Async 后缀 变成 promise 写法
target[key+'Async'] = promisify(target[key]);
}
});
return target;
}
复制代码
写了将近两万字,到这里,咱们终于能够给手写 promise 作一个结尾了。咱们先是从 promise 的使用方法入手,构造出了 promise 的大体框架,而后根据 promise/A+ 规范填充代码,重点实现了 then 的链式调用和值的穿透;而后使用测试脚本对所写的代码是否符合规范进行了测试;最后完成了 Promise 的 API 的实现。弄懂 promise 其实并不复杂,归根结底仍是孰能生巧,没事仍是要多加练习才行吖。
查看完整代码请戳:github.com/qiruohan/ar…
因为篇幅过长,因此这篇文章主要讲了面试题的一、二、三、四、6,关于第五点我会在讲 EventLoop 的文章中再进行系统的梳理,相信在你看过 Promise 的源码以后,再理解 EventLoop,也会更加好理解了。
计划输出的相关内容文章:
若是你对个人文章感兴趣,欢迎关注我噢!若是你对文章有任何的疑问,也欢迎给我留言~
9k字 | Promise/async/Generator实现原理解析
Promise的源码实现(完美符合Promise/A+规范)
欢迎你们关注个人公众号,一块儿研究前端技术,期待与你共同进步。