手把手教你使用ts 一步一步的去完成一个Promise

8,858 阅读13分钟

promise

前奏

笔者公司前端小组,线上出现了因为Promise用错了而导致的问题。 本文的出发点不仅是了解Promise,主要目的是跟着特性去写一个Promise。 所以前提是你已经掌握了Promise的基本特性,如果没有接触过的话,请点击学习

正文

熟悉es6的Promise特性。

1.特性概览

  • 链式调用。

  • 内部三种状态。

内部三种状态分别为pending、fullfiled、rejected,初始化状态为pending。状态变化可以从pending转化fullfiled或rejected。无其他转化方式。

2.出现的背景

解决ajax回调地狱。

ajax回调地狱,本质是希望不可控制的异步变成同步形式。如:

ajax1({...,success(res1){
    ...
}})

ajax2({...,params:{res1},success(res2){ // error no res1
    ...
}})

当我们的ajax2需要用到ajax1的时候,我们不得不使用嵌套式:

ajax1({...,success(res1){
    ajax2({...,params:{res1},success(res2){
        /// doing something
    }})
}})

这种写法的最大问题就是当嵌套层数很多当时候,代码会变得难以维护。

那么 how is Promise的写法 ajax改造:

p1 = function() {
  return  new Promise(function(resolve){
        ajax1({...,success(res1){
            resolve(res1)
        }})
    })
}

p2 = function(){
   return new Promise(function(resolve){
        ajax2({...,params:{res1},success(res2){
            /// doing something
        }})
    })
}

那么最终的写法则变成

p1().then(res=>{
    return p2()
}).then(res2=>{
    // doing something
})

3 具体特性。

  • 1.了解特性首推官方的Promise A+,点击
  • 2.然后是用法,阮一峰大大的es6 Promise特别详细。点击

这里根据Promise A+ 把接下来的几个定义函数做在约定。

1.new Promise().then(),传递的函数分别叫 onFulfilled、onRejected

开始手写

1.Promise是个函数

所以我们最先开始的就是传递一个函数

function Promise(executor) {
    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!';
    }
}

2.Promise 初始状态。

promise有三种状态,可以用ts的枚举

enum pStatus  {
    pending = 'pending',
    fulled = 'fullfilled',
    rejected = 'rejected'
}

然后我们需要定义一些属性。

function Promise() {
    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!';
    }

    this.status = pStatus.pending; // 默认状态

    this.resovlecbs = []; // 回调的resolve函数  主要来自于Promise.prototype.then
    this.rejectcbs = []; //  回调的reject函数  主要来自于Promise.prototype.then
    this.value; // 记录的resolve值
    this.error; // 记录的reject值
}

我们知道Promise传递的函数,是直接会在主线程的执行的,所以我们需要直接执行它。

function Promise() {
    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!';
    }

    this.status = pStatus.pending; // 默认状态

    this.resovlecbs = []; // 回调的resolve函数  主要来自于Promise.prototype.then
    this.rejectcbs = []; //  回调的reject函数  主要来自于Promise.prototype.then
    this.value; // 记录的resolve值
    this.error; // 记录的reject值
    
    try {
        executor(resolve,reject); // 传递的函数的执行。
    } catch (error) {
        reject(error); // 捕获的异常会直接执行reject。
    }
}

3.开始Promise的链式结构,达到异步结果变同步,解决回调地狱。

首先需要说明的是链式的结构的原理是不断返回新的Promise,也就是说then的结果是

Promise.prototype.then = function() {
    return new Promise(function(resolve,reject){
        xxx...
    })
}

首先我们需要明确,Promise具体特性是什么?

1.then 传入的值分别是 resolve的回调和 reject状态回调。

2.传递值,将上一个then的值一直往下传。

3.符合同层先来先到,异层必定上层先执行的策略。

为了了解第三个特性的详细意思之前,让我们看一个例子:

var p1 = new Promise(function(resolve,reject){
    resolve('p1')
});

var p2 = new Promise(function(resolve,reject){
    resolve('p2')
});

p1.then(()=>{
    console.log('p11')
}).then(()=>{
    console.log('p12')
})

p2.then(()=>{
    console.log('p21')
}).then(()=>{
    console.log('p22')
})

相信大家都知道顺序为 p11 => p21 => p12 => p22。原因的话涉及到宏微任务的特性,请参考这篇文章,点击学习。 想必大家已经明白第三点了。

那么如何实现?

分步骤:

1) 传递给Promise的函数,完成后我们才会执行then传递的函数。也就是

new Promise(function(resolve,reject){
    resolve('xxx') // (1)只有执行完这个才会执行后面then的  onFulfilled函数
}).then(function(){
    ...xxx  // (2)这是第二步
})

所以then传递onFulfilled函数和onRejected函数都是在resolve中执行的。所以then其实只是去保存then传递的函数而已,而保存的地方则是Promise主函数内部的resolvecbs和rejectcbs这两个数组。

我觉得可能会有人问为什么会是数组?

因为你可能会这么写:

var p = new Promise(...);
p.then(function(){ ... },...);
p.then(function(){ ... },...);
p.then(function(){ ... },...);

这种非链路,其实都是把onFullfilled,保存到Promise内部,所以需要数组。

然后就是Promise内部到resolve函数和reject函数。这两个函数会做为 用户传入的函数的参数传入。 本质内部就是去遍历执行reslovecbs的函数项。并且改变状态,还有就是将传入的值记录下来,这些值会传给onFullfilled,并由onFullfilled决定是否要继续传递下去。也即是:

then(function onFullfilled(value){ // value 来自于resolve传递的参数。
    return value  // return 则表示续传  下一个then是否能拿到。
})

我们添加下,大致如下:

function Promise() {
    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!';
    }

    this.status = pStatus.pending; // 默认状态

    this.resovlecbs = []; // 回调的resolve函数  主要来自于Promise.prototype.then
    this.rejectcbs = []; //  回调的reject函数  主要来自于Promise.prototype.then
    this.value; // 记录的resolve值
    this.error; // 记录的reject值
    
    const resolve = (value:object)=>{ // resolve做的三件事
        this.value = value; // 记录值  then 的  onFullfilled会用它
        
        this.resovlecbs.forEach((item:Function)=>{
            item(value); // 这个就是  onFullfilled函数,会用上面的value
        })
        this.status = pStatus.fulled; // 把状态改变为 fullfilled

    }
    
    // ... reject同理
    
    try {
        executor(resolve,reject);
    } catch (error) {
        reject(error);
    }
}

resolve中执行的是resolveCbs数组存放的函数。而这些函数是来自于then推送的。 但是值得注意的是,函数除了执行then传递的onFullfiled函数和onRejected函数,还要将这两个返回的值,传递下去,所以要执行下一个Promise的resolve,因为resolve的第一个特性就是记录值。 所以then是这样的。

Promise.prototype.then = function (onFullfilled:Function=noop,onRejected:Function=noop) {

    let scope = this;
    return new Promise(function(resolve = noop,reject = noop){
         scope.resovlecbs.push((value)=>{
            handlerRes(onFullfilled,value,resolve);
        })
        scope.rejectcbs.push((error)=>{
            handlerRes(onRejected,error,reject);
        })
    });
}

export function handlerRes(handler,message,next){
    let res 
    if(isFunc(handler)){
        res = handler(message);
    }
    next(res); // 执行下一个函数的resolve
}

可以看到这里是把 then传递的函数onFullfilled和onRejected分别推入 实例的 resovlecbs 数组和 rejectcbs()达到resolve和onFullfilled的同步执行的效果。

且不仅是onRresolved被执行,同时被执行的还有下一个Promise的 resolve。

这样已经实现了then 链的顺序执行了。

对于构造函数new Promise(),的几个步骤是 创建一个空对象,并将Promise内部执行的所有属性都挂载到这个对象上。也就是this的所有属性。

传递都效果图如下:

但是上面的写法会有两个问题:

  • 1.无法达到前面说的符合同层先来先到,异层必定上层先执行的策略。,这种效果,正式event loop的队列。 所以我们可以使用微任务或宏任务。 这里是了简化代码结构使用setTimeout来模拟,如果感兴趣可以去了解下这个npm库asap,点击这里

  • 2.目前我们的then函数的写法是直接把函数推入到resolvecbs数组,等待resolve去执行,但是这种方式不hack,如果我们先执行了resolve后,我们在执行then。比如:

var p1 = new Promise(function(resolve,reject){
    resolve('p1')
});

p1.then(()=>{
    console.log('p11')
}).

这时候我们会先执行resolve, 完成了resolvecbs的遍历执行,然后才去通过then,对resolvecbs进行搜集。name后面搜集的函数就永远不会执行了。所以我们必须判断状态。

hack写法:

Promise.prototype.then = function (onFullfilled:Function=noop,onRejected:Function=noop) {


    let scope = this;
    return new Promise(function(resolve = noop,reject = noop){
        if(scope.status === pStatus.pending) { // pending则等待执行
            scope.resovlecbs.push((value)=>{
                handlerRes(onFullfilled,value,resolve);
            })
            scope.rejectcbs.push((error)=>{
                handlerRes(onRejected,error,reject);
            })
        } else if(scope.status===pStatus.fulled) { // fullfilled则直接执行
            handlerRes(onFullfilled,scope.value,resolve);
        } else { // rejectd 直接执行
            handlerRes(onRejected,scope.error,reject);
        }
    });
}

这里将多种情况考虑进入了,然而你觉得已经结束了吗? 我们来看下Promise A+怎么说的。

翻译下就是:

  • 如果 onFulfilled 不是函数且 promise1 成功执行, promise2 必须成功执行并返回相同的值。
  • 如果 onRejected 不是函数且 promise1 拒绝执行, promise2 必须拒绝执行并返回相同的值。

什么意思? 让我们看一个例子:

new Promise(function(resolve){
    resolve('test')
}).then().then(value=>{
    console.log(value)
})

上述情况,会传递吗?

答案是会。 这是Promise A+的标准。 所以我们在then中必须兼容了。让我们再次改造下then函数。

Promise.prototype.then = function (onFullfilled:Function,onRejected:Function) {

    let scope = this;
    return new Promise(function(resolve = noop,reject = noop){

        const resolveHandler = function(value){
            if(isFunc(onFullfilled)) {
                handlerRes(onFullfilled,value,resolve);
            } else {
                resolve(value)
            }
        }
        const rejectHanlder = function(error) {
            if(isFunc(onRejected)){
                handlerRes(onRejected,error,resolve);
            } else {
                reject(error);
            }
        }

        try {
            if(scope.status === pStatus.pending) {
                scope.resovlecbs.push((value)=>{
                    resolveHandler(value)
                })
                scope.rejectcbs.push((error)=>{
                    rejectHanlder(error);
                })
            } else if(scope.status===pStatus.fulled) {
                resolveHandler(scope.value);
            } else { // rejectd
                rejectHanlder(scope.error);
            }
        } catch (error) {
            reject(error);
        }
    });
}
function handlerRes(handler,message,next){
    let res 
    if(isFunc(handler)){
        res = handler(message);
    }
    next(res); // 执行下一个函数的resolve
}

好了,到了这里基本已经完成大部分了,然而,关于回调地狱的问题依然没有解决。让我们看看我们是怎么处理回调地狱的。

promise1.then(function(){
    return promise2
}).then(value=>{
    console.log(value) // 应该需要拿到promise2的结果
})

目前我们处理then的onFullfilled函数的结果是在handlerRes这个函数中进行的,所以我们必须对这个函数进行改造,来适应return类型为promise的处理。

function handlerRes(handler,message,nextResolve,nextReject,Promise){
    let res 
    if(isFunc(handler)){
        res = handler(message);
    }
    
    if(res && res instanceof Promise) {
        if(res.status===pStatus.pending){
            res.then(value=>{
                nextResolve(value)
            },err=>{
                nextReject(err)
            })
        }
    } else {
        nextResolve(res);
    }
}

上面已经添加了对Promise的处理,这样ok了吗? 如果promise2也是个深层次的promise则会出问题。 如promise2.then(value=>{}); value如果是promise的实例, 这个时候,我们的handlerRes还是会问题的。

所以我们需要递归的处理,让我们改造下:

export function deepGet(res,Promise2,nextResolve,nextReject){
    if(res && res instanceof Promise2) {
        if(res.status===pStatus.pending){
            res.then(value=>{
                deepGet(value,Promise2,nextResolve,nextReject)
            },err=>{
                nextReject(err)
            })
        }
    } else {
        nextResolve(res);
    }
}

export function handlerRes(handler,message,nextResolve,nextReject,Promise2){
    let res 
    if(isFunc(handler)){
        res = handler(message);
    }
    deepGet(res,Promise2,nextResolve,nextReject)
}

完整的代码示例如下:

Promise2.prototype.then = function (onFullfilled:Function,onRejected:Function) {

    let scope = this;
    return new Promise2(function(resolve = noop,reject = noop){

        const resolveHandler = function(value){
            if(isFunc(onFullfilled)) {
                handlerRes(onFullfilled,value,resolve,reject,scope.constructor);
            } else {
                resolve(value)
            }
        }
        const rejectHanlder = function(error) {
            if(isFunc(onRejected)){
                handlerRes(onRejected,error,resolve,reject,scope.constructor);
            } else {
                reject(error);
            }
        }

        try {
            if(scope.status === pStatus.pending) {
                scope.resovlecbs.push((value)=>{
                    resolveHandler(value)
                })
                scope.rejectcbs.push((error)=>{
                    rejectHanlder(error);
                })
            } else if(scope.status===pStatus.fulled) {
                resolveHandler(scope.value);
            } else { // rejectd
                rejectHanlder(scope.error);
            }
        } catch (error) {
            reject(error);
        }
    });
}

export function deepGet(res,Promise2,nextResolve,nextReject){
    if(res && res instanceof Promise2) {
        if(res.status===pStatus.pending){
            res.then(value=>{
                deepGet(value,Promise2,nextResolve,nextReject)
            },err=>{
                nextReject(err)
            })
        }
    } else {
        nextResolve(res);
    }
}

export function handlerRes(handler,message,nextResolve,nextReject,Promise2){
    let res 
    if(isFunc(handler)){
        res = handler(message);
    }
    deepGet(res,Promise2,nextResolve,nextReject)
}

then函数的编写完成了,让我们再回来看看Promise本身。

Promise是微任务,这里为了方便, 对Promise本身添加宏任务间隔。reject同理。

function Promise(executor:any) {

    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!';
    }

    this.status = pStatus.pending;

    this.resovlecbs = [];
    this.rejectcbs = [];
    this.value;
    this.error;

    const resolve = (value:object)=>{
        this.value = value;
        setTimeout(()=>{
            this.resovlecbs.forEach((item:Function)=>{
                item(value);
            })
            this.status = pStatus.fulled;
        },0)
    }

    const reject = (error:Error)=>{
        this.error = error;

        setTimeout(()=>{ 
            this.status = pStatus.rejected;
            if(this.rejectcbs.length ===0){
                throw this.error;
            }  else {
                this.rejectcbs.forEach((item:Function)=>{
                    item(error);
                })
            }
        },0)
       // if(this.rejectcbs.length === 0 ) throw error;
    } 

    try {
        executor(resolve,reject);
    } catch (error) {
        reject(error);
    }
}

然而这依然并非是最终版本,因为这无法解决,多次resolve会重复执行 resolvecbs的问题。 所以resolve函数的内容必须旨在pending的状态下才执行。 比如有人会这么做:

new Promise(function(resolve){
    reslove('ddd')
    resolve('ttt')
}).then(value=>{
    console.log(value)
})

为了只打印一个值,我们必须要在resolve函数做个判断,只有pending的时候会

function Promise(executor:any) {

    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!';
    }

    this.status = pStatus.pending;

    this.resovlecbs = [];
    this.rejectcbs = [];
    this.value;
    this.error;

    const resolve = (value:object)=>{
        
        setTimeout(()=>{
            if(this.status===pStatus.pending){ // 避免重复执行。
                this.value = value;
                this.resovlecbs.forEach((item:Function)=>{
                    item(value);
                })
                this.status = pStatus.fulled; // 状态改变
            }
        },0)
    }

    const reject = (error:Error)=>{
        

        setTimeout(()=>{ // why
            if(this.status===pStatus.pending){ // 添加了判断 避免重复执行
                this.error = error;
                this.status = pStatus.rejected; //状态改变
                if(this.rejectcbs.length ===0){
                    throw this.error;
                }  else {
                    this.rejectcbs.forEach((item:Function)=>{
                        item(error);
                    })
                }
            }
        },0)
       // if(this.rejectcbs.length === 0 ) throw error;
    } 

    try {
        executor(resolve,reject);
    } catch (error) {
        reject(error);
    }
}

基本完美了,然而还是一个Promise A+标准的处理问题。当resolve(value)的value是个Promise的话,如:

let p1 = new Promise(function(resolve,reject){
    resolve('test')
})

new Promise(function(resolve,reject){
    resolve(p1)
}).then(value=>{
    console.log(value) // 需要打印test。
})

目前我们会直接吧p1这个实例直接返回给then的onFullfilled。继续改造下。

export default function Promise(executor:any)  {

    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!';
    }

    this.status = pStatus.pending;

    this.resovlecbs = [];
    this.rejectcbs = [];
    this.value;
    this.error;

    const resolve = (value:object)=>{

        if( value instanceof Promise) { // 这里直接判断
            return value['then'](resolve, reject);
        }
        
        setTimeout(()=>{
            if(this.status===pStatus.pending){
                this.value = value;
                this.resovlecbs.forEach((item:Function)=>{
                    item(value);
                })
                this.status = pStatus.fulled;
            }
        },0)
    }

    const reject = (error:Error)=>{

        setTimeout(()=>{ // 
            if(this.status===pStatus.pending){
                this.error = error;
                this.status = pStatus.rejected;
                if(this.rejectcbs.length ===0){
                    throw this.error;
                }  else {
                    this.rejectcbs.forEach((item:Function)=>{
                        item(error);
                    })
                }
            }
        },0)
    } 

    try {
        executor(resolve,reject);
    } catch (error) {
        reject(error);
    }
}

4.Promise的catch和finally函数。

catch函数和finally函数其实是语法糖,我们完全可以用then替代的。读者大大们思考下。。

下面给出代码:

Promise.prototype.catch = function(catchcb:Function) {
    return this.then(undefined, catchcb); // 本质是then
}


Promise.prototype.finally = function (callback) {
   return this.then((value)=>{ // 本质是then
        callback();
        return value;
   },callback);
}

所以下面这种写法

p.then(onResolve,onReject).catch(onCatch).finally(onFinal);

其实是等于

p.then(onResolve,onReject).then(undefined,onCatch).then(onFinal,onFinal);

5.Promise.resolve。

阮一峰给出了这个函数的四种处理方式。

  • 1.传递的是Promise,
  • 2.传递的是thenable的对象 如 { then:function(){} }
  • 3.传递是非thenbale的值
  • 4.什么也没传。

需要注意的是Promise.resolve,传递出来的一定是promise。 笔者的写法是

Promise.resolve = function(handler){
    if(  isObject(handler)  && 'constructor' in handler && handler.constructor=== this) { // handler 是 Promise
        return handler;
    } else if (isObject(handler) && isFunc(handler.then) ){ // thenable
        return new this(handler.then.bind(handler));
    }  else { // 非thenable
        return new this(function(resolve){
            resolve(handler);
        })
    }   
}

可以看到如果是:

情况1,则直接原封不动的返回。

情况2则返回一个Promise,且把对象的then函数,作为参数传递进入Promise。

情况3 直接把handler resolve掉。

6.Promise.reject.

Promise.reject可不像resolve这么麻烦。完全把传递的值直接传递出来。

Promise.reject = function() {
    const args = Array.prototype.slice.call(arguments);
    return new this((resolve, reject) => reject(args.shift()));
}

7.Promise.all。

首先是用法:

const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return new Promise(function(resolve,reject){
    setTimeout(()=>{
        resolve(id);
    },id);
  })
});


Promise.all(promises).then(function (posts) {
  console.log(posts);
})

首先传入的是数组。其次就是所有的数组,其次就是数组存在的Promise全部都执行完后才会进入all的 then函数中。 所以需要一个标记记录实时记录所有已经完成的promise。

然后就是传入的数组 可能有promise也有可能传递的并非是Promise,所以需要hack。 区分是否存在then函数。

Promise.all = function(arr) {

    if( !isArray(arr) ){
        throw 'all函数 传递的参数不为Array!!!';
    }

    let args = Array.prototype.slice.call(arr);
    let resArr = Array.call(null,Array(arr.length)).map(()=>null); // 记录所有的结果
    let handlerNum = 0; // 处理标记

    return new this((resolve,reject)=>{
        for(let i = 0;i<args.length;i++){
            let ifunc = args[i];
            if(ifunc && isFunc(ifunc.then)  ) { //是否存在then函数。
                ifunc.then(value=>{
                    resArr[i] = value;
                    handlerNum ++; // 标记添加
                    if(handlerNum>=arr.length){ // 彻底完成
                        resolve(resArr) // 完成后的数组
                    }
                },error=>{
                    reject(error);
                });
            } else { // 非thenable
                resArr[i] = ifunc;
                handlerNum ++; // 标记添加
                if(handlerNum>=arr.length){ // 彻底完成
                    resolve(resArr) // 完成后的数组
                }
            }
        }
    });
}

8. Promise.race。

直接上代码吧,大致就是跑的最快的会作为结果传回

Promise2.race = function(arr) {
    if( !isArray(arr) ){
        throw 'race函数 传递的参数不为Array!!!';
    }

    let args = Array.prototype.slice.call(arr);
    let hasResolve = false;

    return new this((resolve,reject)=>{
        for(let i = 0;i<args.length;i++){
            let ifunc = args[i];
            if(ifunc && isFunc(ifunc.then)  ) {
                ifunc.then(value=>{
                    !hasResolve &&  resolve(value)
                },error=>{
                    !hasResolve && reject(error);
                });
            } else {
                hasResolve = true;
                !hasResolve && resolve(ifunc)
            }
        }
    })

}

源码地址

分析同事的问题。

源代码大致如下

let test = function() {
    return new Promise((resolve,reject)=>{
        reject(new Error('test'))
    })
}

Promise.resolve('new').then(res=>{
    test().then(res2=>{
        ...
    })
}).catch(err=>{
    // use err
    console.log(err)
})

遇到的问题是,最后的catch里面拿不到err。

文中我们已经说过,catch只是then的语法糖,而then的值的传递,是靠onFullfilled的return 和 onRejected的return 传递了。问题这是在then里面缺少了return。

总结:

  • resolve做的三件事情。

    1)记录传递的值,准备传递。

    2)执行then的onFullfilled函数

    3)更换状态。

  • then的执行是根据状态作出不同的相应的。

  • catch和finally只是then的语法糖,finally并非是最后执行的意思,而是一定会执行的意思。

  • Promise.resolve可以快捷的创建一个Promise,他返回的一定是一个Promise。

想要表达的更清楚点,所以导致内容过长。。。。

有任何问题,请在评论区提问,我会尽快答复。