[JavaScript] 根据promiseA+规范手写实现promise

写在前面:关于promise的应用这里就不一一赘述了,本篇文章主要从代码层面,根据promiseA+规范,实现一个promise,文末有完整代码。

官方文档的阅读是很重要的,由于是英文写的,读起来比较困难,建议先熟悉promise的用法后再阅读官方文档,本篇文章并不会逐字翻译官方文档的内容。

这里贴上官方文档的地址:Promises/A+ (promisesaplus.com)

我们开始吧~

1. 基础概念

在开始实现之前我们先知道几个重要的基础概念,下面是一个基本的promise例子

let promise = new Promise((resolve, reject) => {
    resolve("success");
    // reject("Error");
    // throw new Error("Error by throw")
}).then((value) => {
    console.log(value);
}, (reason) =>{
    console.log(reason);
})

1.1 执行器executor

在上面例子中,我们可以看到,Promise是一个构造函数,可以通过new实例化,实例化时会执行参数中的方法,这个方法就是执行器,执行器有两个参数,他们都是函数,一个是resolve方法,用于处理成功时的状态,一个是reject方法,用于处理失败时的状态

1.2 promise的三种状态

在文档中的2.1中写道:promise有三种状态:pendingfulfilledrejected,pending可以转变为另外两种状态,但是另外两种状态无法再改变成其他状态

在fulfilled状态时,promise必须有value,value可以是任意数据类型,并且value是不可改变的,value如果是引用数据类型,则只是引用的地址不可改变。

在rejected状态时同理,promise必须有reason,reason可以理解为失败状态的原因。

1.3 then方法

文档2.2中写道:一个promise必须有then方法来接受value或reason,then方法的参数是两个回调函数:onFulfilledonRejected,这两个参数是可选的且必须是函数。

onFulfilled在promise变成fulfilled状态后被调用,并且只能被调用一次,他的第一个参数是前面说的value

onRejected在promise变成rejected状态后被调用,也只能被调用一次,他的第一个参数是前面说的reason

2. 初步实现

到这里就可以实现一个最基本的promise了,使用的是ES6实现

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

// resolve和reject定义在constructor中,是因为每个promise都要有自己的resolve和reject,定义在外面就会变成每个pormise共用了
class myPromise {
    // 执行器传入
	constructor(executor) {
        // 初始化状态为pending
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined; 
        
        // 定义resolve
		const resolve = (value) => {
            if (this.status === PENDING) {
                this.value = value;
            	this.status = FULFILLED; 
            }
        }
        
        // 定义reject
        const reject = (reason) => {
            if (this.status === PENDING) {
                this.reason = reason;
            	this.status = REJECTED; 
            }
        }
        
        // 这里的try-catch是为了实现在paomise实例化时抛出异常,会调用rejected状态的回调函数的情况。
        try {
            executor(resolve, reject);
        } catch(e) {
            reject(e);
        }
    }
    
    then(onFulfilled, onRejected) {
        if (this.status === FULFILLED) {
            onFulfilled(this.value);
        }
        
        if (this.status === REJECTED) {
            onRejected(this.reason);
        }
    }
}

// 导出
module.exports = myPromise;

测试一下:新建一个js文件,导入我们自己写的myPromise

let myPromise = require("./artical");

let promise1 = new myPromise((resolve, reject) => {
    // 依次调用下面三行代码
    resolve("onFulfilled");
    // reject("onRejected")
    // throw new Error("onRejected throw Error")
}).then((value) => {
    console.log(value);
}, (reason) => {
    console.log(reason)
})

小结: 实现一个最简单的promise,需要知道以下几个概念,将其串联起来即可

  1. promise的3个状态
  2. 执行器executor
  3. then方法和他的两个回调参数
  4. 执行器用try-catch包裹,用reject接管catch中的异常抛出

3. 处理异步和多次调用

3.1 异步的情况

还是用上面的测试用例,改成使用原生的promise

let promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
       resolve("onFulfilled"); 
    }, 2000) 
}).then((value) => {
    console.log(value); // 2s后输出"onFulfilled"
}, (reason) => {
    console.log(reason)
})

原生的promise是可以支持异步调用的情况的,我们自己写的promise还不行,这是由于js代码有一套运行规则,运行到异步任务时,先挂起异步任务也就是上面的setTimeout部分,此时promise的状态依旧是pending,然后接着去执行then中的代码,由于我们没有处理pending状态的方法,什么都不会发生。

另外,promise是可以多次调用then的,多次调用时要求按then的顺序从前到后依次调用。

因此我们需要处理一下pending状态 ,主要思路是发布-订阅模式:

  1. 使用数组存储then中的回调函数,即订阅
  2. 由于需要保存回调函数中传入的this.value或this.reason,因此保存时使用匿名函数包裹回调函数,形成一个闭包
  3. 异步最终也会调用resolve或reject,因此在这两个方法中进行按序调用数组中的回调函数,即发布

3.2 实现

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

// resolve和reject定义在constructor中,是因为每个promise都要有自己的resolve和reject,定义在外面就会变成每个pormise共用了
class myPromise {
	constructor(executor) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined; 

        // 存储then的回调函数参数
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];
        
		const resolve = (value) => {
            if (this.status === PENDING) {
                this.value = value;
            	this.status = FULFILLED; 
            }
            
            // 发布
            this.onFulfilledCallbacks.forEach(fn => fn());
        }
        
        const reject = (reason) => {
            if (this.status === PENDING) {
                this.reason = reason;
            	this.status = REJECTED; 
            }

            // 发布
            this.onRejectedCallbacks.forEach(fn => fn());            
        }
        
        // 这里的try-catch是为了实现在paomise实例化时抛出异常,会调用rejected状态的回调函数的情况。
        try {
            executor(resolve, reject);
        } catch(e) {
            reject(e);
        }
    }
    
    then(onFulfilled, onRejected) {
        if (this.status === FULFILLED) {
            onFulfilled(this.value);
        }
        
        if (this.status === REJECTED) {
            onRejected(this.reason);
        }

        // 订阅
        if (this.status === PENDING) {
            this.onFulfilledCallbacks.push(() => {
                onFulfilled(this.value);
            });

            this.onRejectedCallbacks.push(() => {
                onRejected(this.reason);
            });
        }
    }
}

module.exports = myPromise;

测试一下:

let myPromise = require("./artical");

let promise1 = new myPromise((resolve, reject) => {
    setTimeout(() => {
        resolve("promise1")
    }, 2000)
});

promise1.then((value) => {
    console.log("第一次调用" + value + ".then");
}, (reason) => {
    console.log(reason)
})

promise1.then((value) => {
    console.log("第二次调用" + value + ".then")
})

4. 链式调用

​ 在继续实现之前,我们需要先理解一下链式调用

4.1 成功和失败的条件

上面说道,promise成功时会走then中onFulfilled回调,失败时会走onRejected回调,最开始的成功失败是通过resolve和reject控制的,而在链式调用中,走哪一个回调需要具体分析

走onFulfilled的条件:(不管是在成功回调,或者是失败回调)

  1. then中通过return传递结果(注意函数默认返回undefined)
  2. then中通过新的promise调用resolve,即使resolve是异步的

走onRejected的条件:(不管是在成功回调,或者是失败回调)

  1. then中通过新的promise调用reject
  2. then中在onFulfilled回调中抛出异常

如果遇到catch,也可以把catch看成是成功回调为null的then方法

4.2 链式调用的实现思路

  1. 链式调用的本质其实是then方法会返回一个promise,在官方文档中指定了返回promise2,这是一个命名,我们按照官方文档来就好。
  2. onFulfilled和onRejected回调有返回值,这个返回值可能是普通值,也可能是一个promise,如果是promise则需要另外处理,把这个返回值定义为x
  3. 对于走4.1中的走onRejected的第二种情况,要用try-catch处理
  4. 定义一个新方法resolvePromise处理x,需要传递的参数有promise2,promise2的resolve和reject,以及x,并执行该方法(为什么要传递这些参数,后面会讲)
  5. 对于promise2,没有定义完成时是不能作为参数传递的,因此需要将resolvePromise变为异步任务,使得promise2先完成定义

4.3 实现

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

const resolvePromise = (promise2, x, resolve, reject) => {
    // 测试打印一下拿到的数据
    console.log("promise2:" + promise2);
    console.log("x:" + x);
    console.log("resolve:" + resolve);
    console.log("reject:" + reject);
}

// resolve和reject定义在constructor中,是因为每个promise都要有自己的resolve和reject,定义在外面就会变成每个pormise共用了
class myPromise {
	constructor(executor) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined; 

        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];
        
		const resolve = (value) => {
            if (this.status === PENDING) {
               this.value = value;
            this.status = FULFILLED; 
            }
            
            this.onFulfilledCallbacks.forEach(fn => fn());
        }
        
        const reject = (reason) => {
            if (this.status === PENDING) {
               this.reason = reason;
            this.status = REJECTED; 
            }

            this.onRejectedCallbacks.forEach(fn => fn());            
        }
        
        // 这里的try-catch是为了实现在paomise实例化时抛出异常,会调用rejected状态的回调函数的情况。
        try {
            executor(resolve, reject);
        } catch(e) {
            reject(e);
        }
    }
    
    then(onFulfilled, onRejected) {
        let promise2 = new myPromise((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)
                    }
                })
            }
    
            if (this.status === PENDING) {
                // 这里并不直接调用resolvePromise,因此不需要异步处理
                this.onFulfilledCallbacks.push(() => {
                    try {
                        let x = onFulfilled(this.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
    
                this.onRejectedCallbacks.push(() => {
                    try {
                        let x = onRejected(this.reason);
                    resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e)
                    }
                });
            }
        })

        return promise2
    }
}

module.exports = myPromise;

测试一下,看看resolvePromise能拿到什么

let myPromise = require("./artical");

let promise1 = new myPromise((resolve, reject) => {
    setTimeout(() => {
        resolve("promise1")
    }, 2000)
    
});

let promise2 = promise1.then((value) => {
    return value + "-> then -> promise2";
})
.then((value) => {
    console.log(value);
}, () => {
    
})

5. resolvePromise方法

这一部分可以详细阅读文档,一步一步跟着来

  1. 为防止陷入死循环而报错,我们需要先判断x与调用then的promise是否是相同的引用,如果是,则调用reject抛出异常。说得再详细一点,x是在then中调用onFulfilled的返回值,而then也是一个promise调用的,如果返回值与这个promise相同,就会陷入死循环。
  2. 看x是不是promise,首先判断它是不是一个对象或function,如果是再判断x.then是不是一个function,如果都满足则说明x是promise,如果有一个条件不满足则resolve(x)就可以了。文档中还需要注意的一点是,x.then()可能被截持而抛出异常,此时就需要try-catch处理一下。
  3. 文档中说道:x是一个promise时,要以x为this执行里面的then方法,then方法中成功回调参数为y,失败回调参数为r
  4. 用called变量控制当成功失败的回调都被调用时,应只执行第一个回调
  5. 实现透传:then中没有参数,也会把值传给下一个then,即回调函数有默认值
  6. 递归x是一个promise时,里面如果又返回一个promise怎么办,所以在3.的基础上,应该递归调用resolvePromise

实现(完整代码):

const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected for promise'));
    }

    let called = false;

    if ((typeof x === "object" && typeof x !== null) || typeof x === "function") {
        try {
            let then = x.then;
        
            if (typeof then === "function") {
                then.call(x, (y) => {
                    if (called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, (r) => {
                    if (called) return;
                    called = true;
                    reject(r);
                });
            } else {
                resolve(x);
            }
        } catch(e) {
            if (called) return;
            called = true;
            reject(e)
        }
    } else {
        resolve(x);
    }
}

class myPromise {
    constructor (exector) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined;

        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];

        const resolve = (value) => {
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
            }

            // 发布
            this.onFulfilledCallbacks.forEach(fn => fn())
        }

        const reject = (reason) => {
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
            }

            // 发布
            this.onRejectedCallbacks.forEach(fn => fn())
        }

        try {
            exector(resolve, reject);
        } catch(e) {
            reject(e);
        }
    }

    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => {return value};
        onRejected = typeof onRejected === "function" ? onRejected : reason => {throw reason};
        
        let promise2 = new myPromise((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.onFulfilledCallbacks.push(() => {
                    try {
                        let x = onFulfilled(this.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch(e) {
                        reject(e);
                    }
                });
                this.onRejectedCallbacks.push(() => {
                    try {
                        let x = onRejected(this.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch(e) {
                        reject(e);
                    }
                })
            } 
        })
        return promise2
    }

    catch(rejcetedCallback) {
        return this.then(null, rejcetedCallback);
    }
}

module.exports = myPromise;

本篇文章就到这里啦,如有错误,欢迎指正!

posted @ 2021-10-18 14:15  是棕啊  阅读(89)  评论(0编辑  收藏  举报