【小小前端】手写一个很简单的异步编程解决方案Promise及其链式调用

1,492 阅读7分钟

前端异步一直是个热门话题,作为新一代异步编程解决方案的Promise,相比传统异步编程,不仅解决了恐怖的回调地狱,在写法上也是相当方便简洁。如此牛*的功能让人不禁地想一探究竟,在各大网络公司面试中,手写Promise也是常见的笔试题了,就让我们一起来简单探讨一下Promise的实现吧!

引子


什么是Promise?

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

Promise简单用法

// 定义一个Promise对象
let promise = new Promise((resolve,reject) => {
    true&&resolve(true) || reject(false)
});

promise.then(res=>{
    console.log(res)
}).catch(err=>{
    throw err
})

如此一个低级简单的Promise就完成了。

正文


思考一下,需要实现哪些功能

  1. 定义一个Promise
  2. 在Promise内部,需要一个构造函数(constructor),构造函数有两个参数:resolvereject,构造函数内部需要一个状态变量state(pending、resolved、rejected),一个异步成功的回参res
  3. 既然是Promise那必须得有then方法,有两个参数分别是onFulfilledonRejected
  4. 有了then,自然有catchfinally等一系列。。。。。。

声明一个Promise

既然是手写,那么我们直接将原来的Promise对象覆盖,这里采用ES6的class写法。

声明构造函数

  • Promise存在三个状态(state)pending、fulfilled、rejected
  • pending(等待态)为初始态,并可以转化为fulfilled(成功态)和rejected(失败态)
  • 不管成功还是失败,最终得到的res或者reason都无法改变
class Promise {
    // 构造函数
    constructor(executor){
        // 状态控制state,默认值值为pending、成功resolved、失败rejected
        this.state = 'pending';
        // 成功返回值
        this.res = '';
        // 失败返回原因
        this.reason = '';
        // resolve方法
        let resolve = (res) => {
            // 如果状态state为pending,将状态变为resolved,并将返回值赋给res
            this.state === 'pending' && (this.state = 'resolved') && (this.res = res);
        };
        
        let reject = (reason) => {
            // 如果状态state为pending,将状态变为rejected,并返回错误原因
            this.state === 'pending' && (this.state = 'rejected') && (this.reason = reason);
        };
        
        // 如果executor报错的话,直接执行reject
        try {
            executor(resolve,reject)
        } catch (error) {
            reject(error)
        }
    }
}

实现then方法

then(onFulfilled,onRejected){
    // state为resolved,执行onFulfilled
    this.state === 'resolved' && (()=>{
        onFulfilled(this.res)
    })();
    // state为rejected,执行onRejected
    this.state === 'rejected' && (()=>{
        onRjected(this.reason)
    })();
}

牛刀小试

此时,一个最基本最简单最低级的Promise就完成了,让我们先测试一下结果,随机一个数,如果大于0.5则成功,如果小于则失败。

let promise = new Promise((resolve,reject)=>{
    let random = Math.random();
    if(random>0.5){
        resolve(random)
    } else{
        reject(random)
    }
});

promise.then(res=>{
    console.log('成功:' + res)
},err=>{
    console.log('失败:' + err)
})

刷新控制台:

看起来没什么毛病,不过如果你认为这就结束了?

还是这该死的异步

有人问,Promise不就是解决异步的,怎么又说到异步,这里要说的异步就是指定时器了,比如setTimeout,为什么这样说,这就涉及到js的事件循环机制(eventLoop)了,这里暂且不提。

先看一个例子:

let promise = new Promise((resolve,reject)=>{
    let random = Math.random();
    if(random>0.5){
        setTimeout(() => {
            resolve(random)
        }, 1000);
    } else{
        setTimeout(() => {
            reject(random)
        }, 1000);
    }
});

promise.then(res=>{
    console.log('成功:' + res)
},err=>{
    console.log('失败:' + err)
})

此时,不管怎么刷新,控制台就是不打印结果,不管正确还是错误,这是为什么呢?

首先我们定义了一个Promise对象的实例,在实例中定义了一个定时器,一秒后返回,当运行到定时器之后,定时器挂起,继续执行下面的promise.then,而此时因为定时器的缘故,不管是resolve还是reject都没有运行,也就是说state的状态任然没有改变,所以在then方法中,没有对应的项,如果再次执行then方法,同理依然无法运行,那么问题来了,如何解决因为定时器导致的状态未变化和多次执行then方法呢?

改造构造函数

首先为了多次执行then方法,我们定义两个空数组,分别存放成功或者失败方法,然后在then方法中,如果状态依然是pending,则将成功或者失败的函数存入对应的数组,当定时器时间过了之后,在resolve或者reject中循环对应数组并执行函数。

constructor(executor){
    // 原先代码不变
    // ......
    // 添加数组
    // 成功列表
    this.resolvedList = [];
    // 失败列表
    this.rejectedList = [];
    // 改造一下resolve和reject
    let resolve = (res) => {
        this.state === 'pending' && (this.state = 'resolved') && (this.res = res);
        this.state === 'resolved' && this.resolvedList.forEach(fn => {
            fn();
        })
    };
    // 失败
    let reject = (reason) => {
        this.state === 'pending' && (this.state = 'rejected') && (this.reason = reason);
        this.state === 'rejected' && this.rejectedList.forEach(fn => {
            fn();
        })
    };
}

改造then方法

由于定时器的原因,造成先执行then,那么在then中将对应函数存起:

then(onFulfilled,onRejected){
    this.state === 'pending' && (() =>{
        this.resolvedList.push(()=>{
            console.log('resolvedList.push')
            onFulfilled(this.res);
        });
        this.rejectedList.push(()=>{
            onRejected(this.reason)
        });
        return
    })()
    // state为resolved,执行onFulfilled
    this.state === 'resolved' && (()=>{
        onFulfilled(this.res)
    })();
    // state为rejected,执行onRejected
    this.state === 'rejected' && (()=>{
        onRjected(this.reason)
    })();
}

调用then方法

promise.then(
    res => {
        console.log("成功1:" + res);
    },
    err => {
        console.log("失败1:" + err);
    }
);
promise.then(
    res => {
        console.log("成功2:" + res);
    },
    err => {
        console.log("失败2:" + err);
    }
);

刷新控制台:

此时控制台已经可以正常打印。

then方法的链式调用

为了解决回调地狱,在Promise中,我们是这样的写法:new Promise().then().then(),道理其实很简单,第一个then方法返回的值也是一个Promise对象,那么就能继续使用then方法。

当我们在第一个then中return了一个参数(参数未知,需判断)。这个return出来的新的promise就是onFulfilled()或onRejected()的值:

  • 返回值是普通值如:数值、字符串等,直接将其作为callBackPromise成功的结果
  • 如果是一个Promise,则取其结果作为callBackPromise成功的结果

一个判断返回值的函数callBackPromise

  • res 不能是null
  • 声明了then,如果取then报错,则走reject()
function callBackFunction(callBackPromise,res, resolve, reject){
    // 判断callBackPromise === res ,如果等于会造成死循环
    if(callBackPromise === res){
        return reject(new TypeError('注意死循环了!'));
    }
    // 防止重复调用
    let reCall;
    if(res !== null && (typeof res === 'Object' || typeof res === 'function')){
        try {
            // 链式,调用上一次结果的then
            let then = res.then;
            typeof then === 'function' && then.call(res, nextRes => {
                if(reCall){
                    return 
                }
                reCall = true;
                callBackPromise(callBackPromise,nextRes,resolve,reject)
            },err => {
                if(reCall)
                return;
                reject(err);
            });
            resolve(res)
        }catch(e){
            if(reCall)
            return;
            reject(e)
        }
    }else{
        reject(res)
        // resolve(res)
    }
}

继续改造then方法

then(onFulfilled,onRejected){
    // 注意此处用到的var,如果用let会造成callBackFunction报错,原因是let没有变量提升
    var callBackPromise = new Promise((resolve,reject) => {
        this.state === 'pending' && (() =>{
            this.resolvedList.push(()=>{
                let res = onFulfilled(this.res);
                callBackFunction(callBackPromise,res,resolve,reject)
            });
            this.rejectedList.push(()=>{
                let res = onRejected(this.reason)
                callBackFunction(callBackPromise,res,resolve,reject)
            });
            return
        })()
        this.state === 'resolved' && (()=>{
            let res = onFulfilled(this.res);
            callBackFunction(callBackPromise,res,resolve,reject)
        })();
        this.state === 'rejected' && (()=>{
            let res = onRejected(this.reason);
            callBackFunction(callBackPromise,res,resolve,reject)
        })();
    })
    return callBackPromise;
}

测试一下,如果打印为空多刷新几次,因为没有实现catch方法:

promise.then(res => {
    console.log('第一次回调',res)
    return '第一次返回'
})
.then(res2=>{
    console.log('第二次回调',res2)
    return '第二次回调'
})
.then(res3=>{
    console.log('第三次回调',res3)
})

可以看到此时已经可以进行链式操作。

后记

到此,一个基本的链式调用已经完成,然而Promise的博大精深远不及此,诸如onFulfilled,onRejected的可选性,catch、finally、all等方法的实现都还没有完成,需要不断完善,如果上述代码中有错误的还请各位大佬指出,轻喷。

参考链接