面试官:来,实现一个Promise!

752 阅读9分钟

在前端面试和日常的前端开发中,我们会一直用到Promise。在面试中,我被要求手写Promise,手写Promise.all(),实现一个限制并发Promise最大数目的函数,还有很多其他关于Promise的顺序问题。在前端开发中,我们使用Promsie来获取数据并确保我们的代码按照正确顺序运行。

在面试中被问到Promises时,我总是会害怕。我知道怎么使用Promises和一些基本的规则,但如果事情变得花里胡哨,我就开始会担心我会答错。我在网上搜索并且阅读了几个关于Promise的实现,这些实现极其复杂,而且我看不出为什么其中的代码能够实现某些特定的功能。所以我决定写这篇文章,实现一个简单版的Promise,一步一步地去实现,希望你能从这次书写Promise的旅程中受益。

这并不是严格按照Promise A+标准实现的Promise,而是我为了更好地理解Promise而做的小小练习。如果你对于Promise是什么以及它们是如何使用的感到毫无头绪,我建议你花几分钟阅读MDN对Promise的解释

让我们开始吧!在开头,我们会学习如何创造和消费Promise,然后我们会实现基础版的Promise,并逐步完善,使我们的Promise可以支持异步和链式调用。

怎么创建出一个Promise?

Promise确保我们的代码以正确的顺序运行,创建Promise其实只有一种方式,但用起来却有两种。

1.创建并立刻消费

运行以下代码会创建并立刻消费Promise

// promise is triggered to resolve here, when creating
const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('resolved');
    }, 1000);
});

promise.then((res) => {
    console.log(res);
});

注意:setTimeOut模拟的是耗时的异步操作,类似于调用后端这种。

2.写一个函数返回一个Promise,只有函数调用,Promise才会resolve

const getPromise = function() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(res);
        }, 1000);
    });
};

// promise is triggered to resolve here, when calling the function
getPromise().then((res) => {
    console.log(res);
});

这看起来很简单,却是我第一个恍然大悟的时刻。一个新创建出的Promise会立刻执行。如果我们不想Promise立刻执行,我们可以把它包在一个函数里,什么时候想执行,就调用函数。

现在我们知道我们要做的第一件事是什么:我们需要写一个构造函数(constructor),构造函数接收有两个参数的函数,一个参数是resolve,另一个参数是reject,这两个参数被用来确定Promise的状态。

怎么消费一个Promise?

Promise被创建出来后可以被消费,有三种主要的方式来消费Promise:then()catch(),和finally()

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(res);
    }, 1000);
});

promise.then((res) => {
    console.log(res);
}, (err) => {
    console.log(err);
});
promise.catch((err) => {
    console.log(err);
});
promise.finally(() => {
    console.log("finally over");
});

.then(res => onFulfilled(res), err => onRejected(err))接收两个由消费者定义的函数,这两个函数在Promise被解析或被拒绝后调用。

.catch(err => onRejected(err))接收一个函数,在Promise被拒绝后调用。

.finally(() => onSettled())不论Promise是被解析还是被拒绝, onSettled都会被调用。finally在catch和then之后调用, onSettled不接任何参数。

在这篇文章中,我们只实现then方法,因为其他两个和then很相似。

现在,让我们看看我们需要做什么

Promise有以下特性:

  • constructor((resolve, reject) => {}) handler有两个参数resolvereject
  • .then(res => onFulfilled(res), err => onRejected(err))有两个参数,onFulfilledonRejected,在Promise被解析或被拒绝后,Promise的消费者会调用这两个函数之一。
  • Promise的状态:等待,兑现和拒绝。
  • Promise的value:结果result会被传给onFulfilled,错误error会被传给onRejected

Promise的基础版本

当创建一个Promise的时候,构造器接收一个处理器函数handler,handler执行,在Promise完成的时候调用resolve,在Promise出错的时候调用reject。因此,在Promise类中,构造器需要创建被传给handlerresolvereject。 我们根据Promise的状态调用回调函数,关于Promise的状态有两条规则:

  • 所有的Promise开始的时候都是pending状态。

  • 一旦Promise的状态被更改为fulfilledrejected,之后状态无法再更改。 这是我们的第一版Promise,如果你看见任何明显的bug,请先不要在意,我们会在之后解决这些bug

// version 1
class Promise {
    constructor(handler) {
        this.status = "pending";
        this.value = null;
      
        const resolve = value => {
            if (this.status === "pending") {
                this.status = "fulfilled";
                this.value = value;
            }
        };
        const reject = value => {
            if (this.status === "pending") {
                this.status = "rejected";
                this.value = value;
            }
        };
      
        try {
            handler(resolve, reject);
        } catch (err) {
            reject(err);
        }
    }
  
    then(onFulfilled, onRejected) {
        if (this.status === "fulfilled") {
            onFulfilled(this.value);
        } else if (this.status === "rejected") {
            onRejected(this.value);
        }
    }
}

// testing code
const p1 = new Promise((resolve, reject) => {
    resolve('resolved!');
});
const p2 = new Promise((resolve, reject) => {
    reject('rejected!')
})
p1.then((res) => {
    console.log(res);
}, (err) => {
    console.log(err);
});
p2.then((res) => {
    console.log(res);
}, (err) => {
    console.log(err);
});

// 'p1 resolved!'
// 'p2 rejected!'

这是一个很好的开始,但是你可能会注意到有个明显的bug。这一版不支持异步调用,但Promise的主要用途正在于此。如果我们把测试代码改成如下并且使用setTimeout来兑现Promise,代码并没有输出。

const p3 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('resolved!'), 1000);
});
p3.then((res) => {
    console.log(res);
},(err) => {
    console.log(err);
});
// no console log output, our Promise didn't work
//没有输出,我们的Promise不起作用。

这是因为在我们调用.then的时候,我们的Promise的状态是pendingonFulfilledonRejected都没有被调用。我们需要支持异步操作!

Promise的升级版-支持异步操作

为了支持异步,我们需要把onFulfilledonRejected存起来,一旦Promise的状态改变,我们就立刻执行这些函数。 同一个Promise可以调用.then()多次,因此我们会用到两个数组onFulfilledCallbacks 和onRejectedCallbacks来存储函数并在Promise兑现或拒绝的时候立刻调用这些函数。

以下是我们的第二版:

class Promise {
    constructor(handler) {
        this.status = "pending";
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];

        const resolve = value => {
            if (this.status === "pending") {
                this.status = "fulfilled";
                this.value = value;
                this.onFulfilledCallbacks.forEach(fn => fn(value));
            }
        };

        const reject = value => {
            if (this.status === "pending") {
                this.status = "rejected";
                this.value = value;
                this.onRejectedCallbacks.forEach(fn => fn(value));
            }
        };

        try {
            handler(resolve, reject);
        } catch (err) {
            reject(err);
        }
    }

    then(onFulfilled, onRejected) {
        if (this.status === "pending") {
            this.onFulfilledCallbacks.push(onFulfilled);
            this.onRejectedCallbacks.push(onRejected);
        }

        if (this.status === "fulfilled") {
            onFulfilled(this.value);
        }

        if (this.status === "rejected") {
            onRejected(this.value);
        }
    }
}

// testing code
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('resolved!'), 1000);
});
p3.then((res) => {
    console.log(res);
}, (err) => {
    console.log(err);
});

// ' resolved!'

我们现在有了很大的进步!我觉得如果如果你能在面试中写出这个版本,你的面试官会很满意。但是我们可以实现链式调用来让他们更加刮目相看。

更好的Promise -实现链式调用

我们知道可以像下面这样对Promise进行链式调用。

const p = new Promise((resolve, reject) => {
    setTimeout(() => resolve('resolved first one'), 1000);
});
p.then((res) => {
    console.log(res);
    return res + ' do some calculation';
}).then(res => {
    console.log(res);
});
// 'resolved first one'

我们的第二版Promise在这样链式调用的时候会打印出resolved first one并且抛错--Uncaught TypeError: Cannot read property ‘then’ of undefined,这是因为我们的then方法没有返回任何值。

让我们把.then()修改一下,.then()应该返回一个promiseonFulfilledonRejected的返回值用来确定这个promise的状态。

then(onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
        if (this.status === "pending") {
            this.onFulfilledCallbacks.push(() => {
                try {
                    const fulfilledFromLastPromise = onFulfilled(this.value);
                    resolve(fulfilledFromLastPromise);
                } catch (err) {
                    reject(err);
                }
            });
            this.onRejectedCallbacks.push(() => {
                try {
                    const rejectedFromLastPromise = onRejected(this.value);
                    reject(rejectedFromLastPromise);
                } catch (err) {
                    reject(err);
                }
            });
        }

        if (this.status === "fulfilled") {
            try {
                const fulfilledFromLastPromise = onFulfilled(this.value);
                resolve(fulfilledFromLastPromise);
            } catch (err) {
                reject(err);
            }

        }

        if (this.status === "rejected") {
            try {
                const rejectedFromLastPromise = onRejected(this.value);
                reject(rejectedFromLastPromise);
            } catch (err) {
                reject(err);
            }
        }
    });
}

现在如果我们用同样的代码测试我们的promise,两个console.log输出都有了!这太好了!但我们还没有完全完成。要是onFulfilled和onRejected的返回值是一个promise怎么办?现实中的使用场景是:我从一个服务器中fetch了一些数据,在我得到response后,我需要使用response中的数据来从另一个服务器中fetch额外的数据。

我们现在的Promise无法正确运行以下代码:

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('resolved first one'), 1000);
})

p1.then((res) => {
    console.log(res);
    return new Promise(resolve => {
        setTimeout(() => resolve('resolved second one'), 1000);
    });
}).then(res => {
    console.log(res);
});

// ideally, it should 
// 1 sec later, log 'resolved first one'
// 1 sec later, log 'resolved second one'
// 理想情况下
// 1秒钟后,输出'resolved first one'
// 一秒钟后,输出'resolved second one'

因为如果OnFulfilled和onRejected的返回值是Promise,我们的Promise不知道如何处理。

如何处理这种情况?调用.then()

好吧,我的头开始痛了...坚持住!我保证这已经到最后了。

让我们整理出我们现在有的三个Promise:

  • "第一个Promise" -- 最初的Promise,也就是上面代码中的p1
  • "第二个Promise" -- 由then()返回的Promise
  • "第三个Promise" -- 由onFulfilled/Onrejected返回的Promise。

.then()里,我们创造了一个新的Promise(也就是第二个Promise),如果onFulfilled返回一个Promise(也就是第三个Promise),我们会调用这个Promise.then(resolve,reject),并且我们把第二个Promiseresolvereject传入,这样在第三个Promise确定状态的时候第二个Promise也会确定状态。

下面是改进后的.then():

then(onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
        if (this.status === "pending") {
            this.onFulfilledCallbacks.push(() => {
                try {
                    const fulfilledFromLastPromise = onFulfilled(this.value);
                    if (fulfilledFromLastPromise instanceof Promise) {
                        fulfilledFromLastPromise.then(resolve, reject);
                    } else {
                        resolve(fulfilledFromLastPromise);
                    }
                } catch (err) {
                    reject(err);
                }
            });
            this.onRejectedCallbacks.push(() => {
                try {
                    const rejectedFromLastPromise = onRejected(this.value);
                    if (rejectedFromLastPromise instanceof Promise) {
                        rejectedFromLastPromise.then(resolve, reject);
                    } else {
                        reject(rejectedFromLastPromise);
                    }
                } catch (err) {
                    reject(err);
                }
            });
        }

        if (this.status === "fulfilled") {
            try {
                const fulfilledFromLastPromise = onFulfilled(this.value);
                if (fulfilledFromLastPromise instanceof Promise) {
                    fulfilledFromLastPromise.then(resolve, reject);
                } else {
                    resolve(fulfilledFromLastPromise);
                }
            } catch (err) {
                reject(err);
            }

        }

        if (this.status === "rejected") {
            try {
                const rejectedFromLastPromise = onRejected(this.value);
                if (rejectedFromLastPromise instanceof Promise) {
                    rejectedFromLastPromise.then(resolve, reject);
                } else {
                    reject(rejectedFromLastPromise);
                }
            } catch (err) {
                reject(err);
            }
        }
    });
}

我们终于完成了,让我们看看我们代码的最终版本!

最终版本

class Promise {
    constructor(handler) {
        this.status = "pending";
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];

        const resolve = value => {
            if (this.status === "pending") {
                this.status = "fulfilled";
                this.value = value;
                this.onFulfilledCallbacks.forEach(fn => fn(value));
            }
        };

        const reject = value => {
            if (this.status === "pending") {
                this.status = "rejected";
                this.value = value;
                this.onRejectedCallbacks.forEach(fn => fn(value));
            }
        };

        try {
            handler(resolve, reject);
        } catch (err) {
            reject(err);
        }
    }

    then(onFulfilled, onRejected) {
        return new Promise((resolve, reject) => {
            if (this.status === "pending") {
                this.onFulfilledCallbacks.push(() => {
                    try {
                        const fulfilledFromLastPromise = onFulfilled(this.value);
                        if (fulfilledFromLastPromise instanceof Promise) {
                            fulfilledFromLastPromise.then(resolve, reject);
                        } else {
                            resolve(fulfilledFromLastPromise);
                        }
                    } catch (err) {
                        reject(err);
                    }
                });
                this.onRejectedCallbacks.push(() => {
                    try {
                        const rejectedFromLastPromise = onRejected(this.value);
                        if (rejectedFromLastPromise instanceof Promise) {
                            rejectedFromLastPromise.then(resolve, reject);
                        } else {
                            reject(rejectedFromLastPromise);
                        }
                    } catch (err) {
                        reject(err);
                    }
                });
            }

            if (this.status === "fulfilled") {
                try {
                    const fulfilledFromLastPromise = onFulfilled(this.value);
                    if (fulfilledFromLastPromise instanceof Promise) {
                        fulfilledFromLastPromise.then(resolve, reject);
                    } else {
                        resolve(fulfilledFromLastPromise);
                    }
                } catch (err) {
                    reject(err);
                }

            }

            if (this.status === "rejected") {
                try {
                    const rejectedFromLastPromise = onRejected(this.value);
                    if (rejectedFromLastPromise instanceof Promise) {
                        rejectedFromLastPromise.then(resolve, reject);
                    } else {
                        reject(rejectedFromLastPromise);
                    }
                } catch (err) {
                    reject(err);
                }
            }
        });

    }
}

// testing code
let p1 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('resolved first one'), 1000);
});
p1.then((res) => {
    console.log(res);
    return new Promise(resolve => {
        setTimeout(() => resolve('resolved second one'), 1000);
    });
}).then(res => {
    console.log(res);
});

// 1 sec later, 'resolved first one'
// 1 sec later, 'resolved second one'

耶!我们终于完成了!

祝贺你!你做到了!我们从Promise的基础实现开始,一步步到最后把该有的功能都完善,希望这个过程对你有帮助。我们谈到了如何使用Promise,如何消费Promise,并且实现了异步和链式调用。

译者注语

我在网上搜索如何实现Promise的时候发现了这篇文章,对我理解Promise有很大的帮助,文章非常清晰易懂,而且配有例子,所有空暇时间翻译了这篇文章,希望能帮助大家更好地理解Promise。原文链接po在下方。

原文链接:medium.com/swlh/implem…