前言
今天,咱们就来手写一个Promise吧
看到掘金里有一篇文章,45道Promise面试,读完觉得很好,也正是因为写完这些题目觉得自己深深的不足,才促成了本文的出现,表示衷心感谢;大家也可以去试试看,自己到底掌握程度如何(看完本文,代码层面解决此文章的所有题目)
目标
实现一个符合 Promise A+ 规范的个人版promise(通过promises-aplus-tests测试)
- 特点
- 循序渐进实现四个版本,并且每个版本都有测试用例
- 个人整理思维导图,清晰明了
问题驱动
代码题
- 1.1 牛客网
const promise = new Promise((resolve, reject) => {
resolve('success1');
reject('error');
resolve('success2');
});
promise.then((res) => {
console.log('then:', res);
}).catch((err) => {
console.log('catch:', err);
})
- 1.2 腾讯
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
简答题
- 谈谈对promise的理解
- Promise的问题?处理办法?
- 方法实现
- Promise.resolve(value)
- .catch
- .all
- .race
开始
Promise A+是什么
不照搬照套定义,简言之,因为Promise已是已是业内通用,所以必然要保证在使用你实现的promise的过程中行为的可靠性,其实也就是一个【面向接口编程】的感觉吧,标准化才稳定,官话建议点进链接看看,小菜整理后
- 在new Promise是需要传递一个执行器函数,executor 这个函数默认就会被执行 立即执行
- 每个promise 都有三个状态 pending 等待态 fulfilled 成功态 rejected 失败态
- 默认创建一个promise 是等待态 默认提供给你两个函数 resolve让promise变成成功态,reject让promise变成失败态
- 每个promise的实例都具备一个then方法 then方法中传递两个参数1.成功的回调 2.失败的回调
- 如何让promise变成失败态 reject() / 可以抛出一个错误
- 如果多次调用成功或者失败 只会执行第一次, 一旦状态变化了 就不能在变成成功或者失败了
附送整理思维导图
是否符合标准,则需要通过
promises-aplus-tests
进行校验,文末会详细讲解
第一版实现
- 第一点 new Promise传入的回调函数,也就是图中的executor是会同步执行的
- 第二点 每个promise都有三个状态,默认是PENDING(等待态)
- 第三点 为executor传两个回调,用于改变状态
- 第五点 状态的控制
- 首先 定义三个状态常量便于控制
const PENDING = 'PENDING' // 等待态
const FULFILLED = 'FULFILLED' // 成功态
const REJECTED = 'REJECTED' /
- 定义回调,传入
// 只有状态是等待态的时候 才可以更新状态
let resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
}
}
let reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
}
}
- 第4点,定义then方法,参数是用户传过来的 成功的回调
onFulfilled
和 失败的回调onRejected
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value)
}
if (this.status === REJECTED) {
onRejected(this.reason)
}
}
- 汇总
const PENDING = 'PENDING' // 等待态
const FULFILLED = 'FULFILLED' // 成功态
const REJECTED = 'REJECTED' //
class Promise {
constructor(executor) {
this.status = PENDING; // 默认是等待态
this.value = undefined; // 用于记录成功的数据
this.reason = undefined; // 用于记录失败的原因
// 只有状态是等待态的时候 才可以更新状态
let resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
}
}
let reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
}
}
// executor 执行的时候 需要传入两个参数,给用户来改变状态的
try {
executor(resolve, reject);
} catch (e) { // 表示当前有异常,那就使用这个异常作为promise失败的原因
reject(e)
}
}
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value)
}
if (this.status === REJECTED) {
onRejected(this.reason)
}
}
}
module.exports = Promise
测试用例
会输出 fail reason
重点想下为什么不是fail 我失败了
要理解非pengding状态下不会改变任何东西
let Promise = require('./promise')
let promise = new Promise((resolve,reject)=>{
reject('reason');
resolve('value')
throw new Error('我失败了');
})
promise.then((success)=>{
console.log('success',success)
},(err)=>{
console.log('fail',err)
});
遗留问题
当executor执行异步逻辑时,会存在then方法调用时状态还是pengding的情况,因为异步的回调会入异步队列中,后于同步代码执行。
xmind总结
第二版实现
- 异步问题基于订阅发布设计模式进行解决,then方法中pengding状态进行处理,将事件分别压入对应事件存储队列(订阅)
- 状态改变函数(resolve,reject)中遍历调用对应事件存储队列(发布)
- 注意这里用了一个箭头函数包裹,其实是一个AOP面向切片的思想(spring两大支柱,怀念java哈哈),这样我们就可以在调用用户传来的函数前后执行自己的逻辑,建议好好体会
- 定义回调队列
this.onResolvedCallbacks = []; // 存放成功时的回调
this.onRejectedCallbacks = []; // 存放失败时的回调
- then中新增pengding状态处理
if(this.status === PENDING){ // 发布订阅
this.onResolvedCallbacks.push(()=>{
// TODO ...
onFulfilled(this.value);
});
this.onRejectedCallbacks.push(()=>{
onRejected(this.reason)
})
}
- 改写状态改变函数,新增清空队列操作
// 只有状态是等待态的时候 才可以更新状态
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());
}
}
测试用例
let Promise = require('./promise')
let promise = new Promise((resolve,reject)=>{
setTimeout(() => { // 异步的
resolve('value') //此时如果调用了resolve 就让刚才存储的成功的回调函数去执行
}, 1000);
})
// 同一个promise实例 可以then多次
// 核心就是发布订阅模式
promise.then((success)=>{ // 如果调用then的时候没有成功也没有失败。我可以先保存成功和失败的回调
console.log('success',success)
},(err)=>{
console.log('fail',err)
});
promise.then((success)=>{
console.log('success',success)
},(err)=>{
console.log('fail',err)
});
xmind总结
第三版(最终版)
- 实现链式调用:then返回一个promise
- 重点在于处理用户调用then时传过来的onFulfilled, onRejected的返回值x
- x 是一个普通值 就会让下一个promise变成成功态
- x 有可能是一个promise,我需要采用这个promise的状态
- 实现对别人实现的Promise的兼容性处理
- 值穿透特性实现:如果用户调用then时传过来的处理函数不是函数而是一个值类型,则向下传递
实现链式调用
挺简单的,该写下then方法,new一个新的promise返回就可以了
then(onFulfilled, onRejected) {
// 可选参数的处理
onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val;
onRejected = typeof onRejected === 'function'?onRejected:err=>{throw err}
// 递归
let promise2 = new Promise((resolve, reject) => {
# 原有逻辑
})
return promise2;
}
处理用户调用then的返回值x及兼容Promise
这是关键,特点再加个xmind
- 获取到用户的返回值,重点看思路:
- 因为用户可能会返回一个promise1,而这个promise又有可能返回一个promise,简言之,无限有规律循环,则肯定是定义最小单元函数
- 最小单元函数递归思路:终止条件+最小单元
- 最小单元无疑是用户返回一个promise,终止条件是:用户返回一个普通值;
- 参数定义的思路:
- 因为需要判断是不是返回了同一个promise实例(如果和promise2 是同一个人 x 永远不能成功或者失败,所以就卡死了,我们需要直接报错即可),所以参数需要传递then要返回的promise
- 用户的返回值、成功、失败回调肯定要传
- 细节处理
- 要做容错处理,即在递归的方法外面加一层trycatch
- 要考虑用户“发布”时,如果订阅了很多事件,这些事件之间的顺序需要通过异步队列进行保证(即一个promise实例多次then,promise.then();promise.then();注意,这个链式调用不是一个概念,链式调用是promise.then().then())
then改写
then(onFulfilled, onRejected) {
// 可选参数的处理
onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val;
onRejected = typeof onRejected === 'function'?onRejected:err=>{throw err}
// 递归
let promise2 = new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === REJECTED) {
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;
}
resolvePromise实现
const resolvePromise = (promise2, x, resolve, reject) => {
// 判断 可能你的promise要和别人的promise来混用
// 可能不同的promise库之间要相互调用
if (promise2 === x) { // x 如果和promise2 是同一个人 x 永远不能成功或者失败,所以就卡死了,我们需要直接报错即可
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
}
// ------ 我们要判断x的状态 判断x 是不是promise-----
// 1.先判断他是不是对象或者函数
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
// x 需要是一个对象或者是函数
let called; // 为了考虑别人promise 不健壮所以我们需要自己去判断一下,如果调用失败不能成功,调用成功不能失败,不能多次调用成功或者失败
try{
let then = x.then; // 取出then方法 这个then方法是采用defineProperty来定义的
if(typeof then === 'function'){
// 判断then是不是一个函数,如果then 不是一个函数 说明不是promise
// 只能认准他是一个promise了
then.call(x, y =>{ // 如果x是一个promise 就采用这个promise的返回结果
if(called) return;
called = true
resolvePromise(promise2, y, resolve, reject); // 继续解析成功的值
},r=>{
if(called) return;
called = true
reject(r); // 直接用r 作为失败的结果
})
}else{
// x={then:'123'}
resolve(x);
}
}catch(e){
if(called) return;
called = true
reject(e); // 去then失败了 直接触发promise2的失败逻辑
}
} else {
// 肯定不是promise
resolve(x); // 直接成功即可
}
}
综上总结
console.log('-------------- my ---------------')
// 宏
const PENDING = 'PENDING' // 等待态
const FULFILLED = 'FULFILLED' // 成功态
const REJECTED = 'REJECTED' //
const resolvePromise = (promise2, x, resolve, reject) => {
// 判断 可能你的promise要和别人的promise来混用
// 可能不同的promise库之间要相互调用
if (promise2 === x) { // x 如果和promise2 是同一个人 x 永远不能成功或者失败,所以就卡死了,我们需要直接报错即可
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
}
// ------ 我们要判断x的状态 判断x 是不是promise-----
// 1.先判断他是不是对象或者函数
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
// x 需要是一个对象或者是函数
let called; // 为了考虑别人promise 不健壮所以我们需要自己去判断一下,如果调用失败不能成功,调用成功不能失败,不能多次调用成功或者失败
try{
let then = x.then; // 取出then方法 这个then方法是采用defineProperty来定义的
if(typeof then === 'function'){
// 判断then是不是一个函数,如果then 不是一个函数 说明不是promise
// 只能认准他是一个promise了
then.call(x, y =>{ // 如果x是一个promise 就采用这个promise的返回结果
if(called) return;
called = true
resolvePromise(promise2, y, resolve, reject); // 继续解析成功的值
},r=>{
if(called) return;
called = true
reject(r); // 直接用r 作为失败的结果
})
}else{
// x={then:'123'}
resolve(x);
}
}catch(e){
if(called) return;
called = true
reject(e); // 去then失败了 直接触发promise2的失败逻辑
}
} else {
// 肯定不是promise
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 { // try + catch 只能捕获同步异常
executor(resolve, reject);
} catch (e) {
console.log(e);
reject(e)
}
}
// 只要x 是一个普通值 就会让下一个promise变成成功态
// 这个x 有可能是一个promise,我需要采用这个promise的状态
then(onFulfilled, onRejected) {
// 可选参数的处理
onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val;
onRejected = typeof onRejected === 'function'?onRejected:err=>{throw err}
// 递归
let promise2 = new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === REJECTED) {
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;
}
}
Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
})
return dfd;
}
module.exports = Promise;
测试 promises-aplus-tests
promisesaplus.com/(即Promise A+) 是一个介绍promise如何实现的一个网站,并提供了一个promises-aplus-tests的npm全局包用于测试,简单三步走
- 全局安装:
npm install promises-aplus-tests -g
- 在自己实现的Promise上挂载一个静态属性deferred,值是一个函数,返回一个有promise属性的对象,promise属性值是一个对象;(有点晕?不用麻烦了,简单来说就是把下面代码加在自己实现的promise文件里就可以了,上面那个最终版的也贴心的加好了)
Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
})
return dfd;
}
- 执行测试命令
promises-aplus-tests promise.js
如图则成功
扩展 常用方法实现
直接上代码啦,相信能坚持到现在的小伙伴肯定比本南方小菜强,看代码秒懂系列
静态方法
- all 此处废话一句,因为面试超级常考 - 用法:接收promise数组,返回一个promise实例,并且其resolve中可以拿到:一个包含所有请求的结果的数组且顺序和传过来的数组一一对应 - 思路:所有异步顺序问题,都要想想计时器,即定义一个方法,在其中进行判断,如果满足条件才向下执行(很像函数科里化) 1. 因返回可以then,所以必然是返回一个promise实例 2. 遍历数组,then拿到值 3. 定义一个“计时器”函数,用于记录返回值个数,达到了传进来的数组个数才调用用户的then方法
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
let results = []; let i = 0;
function processData(index, data) {
results[index] = data; // let arr = [] arr[2] = 100
if (++i === promises.length) {
resolve(results);
}
}
for (let i = 0; i < promises.length; i++) {
let p = promises[i];
p.then((data) => { // 成功后把结果和当前索引 关联起来
processData(i, data);
}, reject);
}
})
}
- race
Promise.race = function (promises) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
let p = promises[i];
p.then(resolve, reject);
}
})
}
- reject
Promise.reject = function (reason) {
return new Promise((resolve, reject) => {
reject(reason)
})
}
- resolve
Promise.resolve = function (value) {
return new Promise((resolve, reject) => {
resolve(value);
})
}
实例方法
- catch
Promise.prototype.catch = function (onrejected) {
return this.then(null, onrejected)
}
回到原来问题
知道原理害怕面试题吗?来吧
- 第一版的实现就已经解决问题了,promise只能状态被改变一次,所以答案是:
then success1
- 挺经典的题目,更显得原理的重要性
- Promise.resolve(1) 相当于返回一个状态为成功的promise实例,详见【扩展 常用方法实现】
new Promise((resolve, reject) => {
resolve(1)
}).then(2)
.then(Promise.resolve(3))
.then(console.log)
2. 此处resolve用户传了个普通值/对象,当传非函数时,promise内部忽略传递值并传一个默认函数(贴心的截个图,val=>val的形式,不过还是建议如果不熟悉去看看第三版)值传递
new Promise((resolve, reject) => {
resolve(1)
}).then(val=>val)
.then(val=>val)
.then(console.log)
3. 第一版就可以看到,在最开始的resolve(1)时,内部属性this.val已经是1了,后面均无改变val的操作,故最后一次传来一个log函数,就打印出来了
4. 答案:1
附送 (如有侵权,请大佬直接联系我)
恭喜您坚持下来了,建议可以自己跑一跑,毕竟测试用例都写了,现在,再去试试自己的实力吧~~,再次表示感谢45道Promise面试
总结
很多时候,不仅要知其然,还要知其所以然;尽管我们在如今资源丰富、文档普及的时代,学会一个工具的使用还是很快的,成为一个cv工程师很简单,但也就很简单的失去了拥有那种计算机行业的快乐的能力,【干一行爱一行】才是最关键的码农精神所在;