深入浅出之Promise

1,659 阅读13分钟

前言

今天,咱们就来手写一个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的过程中行为的可靠性,其实也就是一个【面向接口编程】的感觉吧,标准化才稳定,官话建议点进链接看看,小菜整理后

  1. 在new Promise是需要传递一个执行器函数,executor 这个函数默认就会被执行 立即执行
  2. 每个promise 都有三个状态 pending 等待态 fulfilled 成功态 rejected 失败态
  3. 默认创建一个promise 是等待态 默认提供给你两个函数 resolve让promise变成成功态,reject让promise变成失败态
  4. 每个promise的实例都具备一个then方法 then方法中传递两个参数1.成功的回调 2.失败的回调
  5. 如何让promise变成失败态 reject() / 可以抛出一个错误
  6. 如果多次调用成功或者失败 只会执行第一次, 一旦状态变化了 就不能在变成成功或者失败了 附送整理思维导图
    是否符合标准,则需要通过promises-aplus-tests进行校验,文末会详细讲解

第一版实现

  1. 第一点 new Promise传入的回调函数,也就是图中的executor是会同步执行的
  2. 第二点 每个promise都有三个状态,默认是PENDING(等待态)
  3. 第三点 为executor传两个回调,用于改变状态
  4. 第五点 状态的控制
  • 首先 定义三个状态常量便于控制
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总结

第二版实现

  1. 异步问题基于订阅发布设计模式进行解决,then方法中pengding状态进行处理,将事件分别压入对应事件存储队列(订阅)
  2. 状态改变函数(resolve,reject)中遍历调用对应事件存储队列(发布)
  3. 注意这里用了一个箭头函数包裹,其实是一个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总结

第三版(最终版)

  1. 实现链式调用:then返回一个promise
  2. 重点在于处理用户调用then时传过来的onFulfilled, onRejected的返回值x
    1. x 是一个普通值 就会让下一个promise变成成功态
    2. x 有可能是一个promise,我需要采用这个promise的状态
  3. 实现对别人实现的Promise的兼容性处理
  4. 值穿透特性实现:如果用户调用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

  1. 获取到用户的返回值,重点看思路:
    1. 因为用户可能会返回一个promise1,而这个promise又有可能返回一个promise,简言之,无限有规律循环,则肯定是定义最小单元函数
    2. 最小单元函数递归思路:终止条件+最小单元
    3. 最小单元无疑是用户返回一个promise,终止条件是:用户返回一个普通值;
    4. 参数定义的思路:
      1. 因为需要判断是不是返回了同一个promise实例(如果和promise2 是同一个人 x 永远不能成功或者失败,所以就卡死了,我们需要直接报错即可),所以参数需要传递then要返回的promise
      2. 用户的返回值、成功、失败回调肯定要传
  2. 细节处理
    1. 要做容错处理,即在递归的方法外面加一层trycatch
    2. 要考虑用户“发布”时,如果订阅了很多事件,这些事件之间的顺序需要通过异步队列进行保证(即一个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)
}

回到原来问题

知道原理害怕面试题吗?来吧

  1. 第一版的实现就已经解决问题了,promise只能状态被改变一次,所以答案是:then success1
  2. 挺经典的题目,更显得原理的重要性
    1. 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工程师很简单,但也就很简单的失去了拥有那种计算机行业的快乐的能力,【干一行爱一行】才是最关键的码农精神所在;

我是【南方小菜】,最爱代码世界;继续努力,未来可期;

深入浅出系列

js

深入浅出之ajax

后端

深入浅出之node服务器

vue生态

深入浅出vue-plugins-v-lazy