前端进阶 - 手撕Promise

494 阅读7分钟

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大

Promise在我们的开发中运用很广,在面试中也是衡量前端开发水平的重要指标,这篇文章将带领各位同学手把手写一个满足Promise/A+规范的Promise.

Let's go!

实现promise的基本框架

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

  • Promise构造函数接受一个函数作为参数。
  • 该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
  • 传入执行函数立即执行。
function MyPromise(executor) {
    function resolve(result) {
        console.log('resolved', result);
    }

    function reject(reason) {
        console.log('rejected', reason);
    }

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

增加状态机

Promise共有3种状态:pending(等待处理), fulfilled(成功), rejected(失败)

  • Promise的状态是不可逆的:默认状态为pending, 状态只能由pending变成fulfilled, 或者pending变成rejected。
  • resolve函数: 将Promise对象的状态从 pending变为fulfilled,并将异步操作的结果,作为参数传递出去。
  • reject函数: 将Promise对象的状态从 pending变为rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function MyPromise(executor) {
    let self = this;
    // 给promise构造函数增加状态,默认状态为pending
    self.state = PENDING;

    function resolve(result) {
        // resolve函数将Promise状态从pending变为fulfilled
        if (self.state === PENDING) {
            self.state = FULFILLED;
            console.log('resolved', result);
        }
        
    }

    function reject(reason) {
        // reject函数将Promise状态从pending变为rejected
        if (self.state === PENDING) {
            self.state = REJECTED;
            console.log('rejected', reason);
        }
    }

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

实现then方法

Promise实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。

  • then方法可以接受两个回调函数作为参数。
  • 第一个回调函数是Promise对象的状态变为fulfilled时调用
  • 第二个回调函数是Promise对象的状态变为rejected时调用。
  • 这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。
  • then传入的回调函数必须等待promise状态变成fulfilled或者rejected之后才能调用。
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    // 这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => { return this.value };
    onRejected = typeof onRejected === 'function' ? onRejected : () => { return this.reason };

    // then传入的回调函数必须等待promise状态变成fulfilled或者rejected之后才能调用
    if (this.state === PENDING) {
        this.fulfilledCallback = onFulfilled;
        this.rejectedCallback = onRejected;
    }

    if (this.state === FULFILLED) {
        onFulfilled();
    }

    if (this.state === REJECTED) {
        onRejected();
    } 
};

实现链式调用

Promise支持链式写法,即then方法后面再调用另一个then方法。那么会等待当前Promise执行完成之后再返回给下一次的then,Promise如果成功,就会走下一次then的成功回调,如果失败就会走下一次then的失败回调。

  • then方法返回一个新的Promise实例。
  • 后一个回调函数,需要等待前一个Promise对象的状态发生变化,才可被调用。
  • then方法中返回的回调函数不能是自己本身,如果真的这样写,那么函数执行到里面时会等待promise的结果,这样一层层的状态等待就会形成回调地狱。
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function MyPromise(executor) {
    const self = this;
    // 给promise构造函数增加状态,默认状态为pending
    self.state = PENDING;
    self.value = undefined;
    self.reason = undefined;
    self.onResolvedCallbacks = [];
    self.onRejectedCallbacks = [];

    function resolve(result) {
        // resolve函数将Promise状态从pending变为fulfilled
        if (self.state === PENDING) {
            self.state = FULFILLED;
            self.value = result;
            // 接受Promise对象传出的值作为参数。
            self.onResolvedCallbacks.forEach(callback => {
                callback();
            });
        }
    }

    function reject(reason) {
        // reject函数将Promise状态从pending变为rejected
        if (self.state === PENDING) {
            self.state = REJECTED;
            self.reason = reason;
            // 接受Promise对象传出的值作为参数
            self.onRejectedCallbacks.forEach(callback => {
                callback();
            });
        }
    }

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

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    let self = this;
    // 这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => { return this.value };
    onRejected = typeof onRejected === 'function' ? onRejected : () => { return this.reason };
    // 返回一个新的Promise实例
    return new MyPromise((resolve, reject) => {
        /**
         * then传入的回调函数必须等待promise状态变成fulfilled或者rejected之后才能调用
         * 如果Promise处理的为一个异步函数,那么当执行then的时候,Promise的状态仍然为pending,
         * 此时我们并不知道要去执行onFulfilled回调还是onRejected回调,
         * 所以先把这两个回调函数保存起来,等到promise状态改变之后,再根据最终promise的状态选择调用。
         */
        if (self.state === PENDING) {
            self.onResolvedCallbacks.push(function() {
                let result = onFulfilled(self.value);

                resolve(result);
            });

            self.onRejectedCallbacks.push(function() {
                let result = onRejected(self.value);

                reject(result);
            });
        }

        if (self.state === FULFILLED) {
            const result = onFulfilled(self.value);
            
            resolve(result);
        }

        if (self.state === REJECTED) {
            const reason = onRejected(self.reason);

            reject(reason);
        } 
    });
};

Promise 解决程序,完善promise.then()

在上述的例子中,我们假设传入的then回调onFulfilled 和 onRejected返回的都是普通值,但其实这2个回调函数的返回值可以是各种各样的,比如onFulfilled返回一个promise, 或者返回一个thenable 对象或者方法所有我们需要完善一下,使我们的代码能够兼容各种返回值.

创建resolvePromise方法,用来处理onFulfilled 和 onRejected的各种返回值

// onFulfilled 和 onRejected的返回值是开发者传入的,可能有各种问题。返回值也有很多种情况,
// 所以我们需要一个函数处理onFulfilled 和 onRejected 的各种返回值
function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        return reject(new TypeError('不能循环调用'));
    }

    // 针对对象或者方法中有 then 方法,也就是所谓的 thenable 对象
    if (isThenable(x)) {
        let called;

        try {
            // 取then的时候可能会出错
            let then = x.then;

            if (typeof then === 'function') {
                then.call(x, result => {
                    if (called) {
                        return;
                    }

                    called = true;
                    // 取得 x.then()的返回值之后,仍然需要对返回值进行判断,所以这里需要再次调用resolvePromise.
                    resolvePromise(x, result, resolve, reject);
                }, error => {
                    if (called) {
                        return;
                    }

                    called = true;
                    reject(error);
                });
            } else {
                resolve(x);
            }
        } catch (e) {
            if (called) {
                return;
            }

            called = true;
            reject(e);
        };
    } else {
        resolve(x);
    }
};

创建isThenable,检测对象或者函数是否拥有then方法

// 判断对象或者函数是否有 then 方法
function isThenable(t) {
    // if (t !== null && typeof t === 'object' || typeof t === 'function' && 'then' in t) {
    if (t !== null && (typeof t === 'object' || typeof t === 'function')) {
        return true;
    }

    return false;
}

Promise.all()

关键点

  1. Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
  2. 直接在构造函数上增加all方法
  3. Promise.all()方法接受一个数组作为参数(可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例)
  4. 需要判断数组中每一个参数是否是Promise,是的话就执行该Promise的then方法, 将返回值放到并放到数组result中,如果是个普通值,直接讲该值放到result中
  5. 等待参数数组中所有的promise都执行完毕后再返回结果
MyPromise.all = function(arr) {
    return new MyPromise((resolve, reject) => {
        if (!Array.isArray(arr)) {
            throw new TypeError('Promise.all 参数必须是数组');
        }

        // 定义存放最终结果的数组result
        let result = [];
        let count = 0;

        addResult = (key, value) => {
            count++;
            result[key] = value;

            // 当count 等于 传入数组的长度时,说明所有的promise执行完毕
            if (count === arr.length) {
                resolve(v);
            }
        }

        for (let i = 0; i < arr.length; i++) {
            let item = arr[i];

            // 判断item是否是thenable对象, 并且item.then是function
            if (isThenable(item)) {
                try {
                    let then = item.then;

                    if (typeof then === 'function') {
                        then(v => {
                            addResult(i, v);
                        }, e => reject(e));
                    } else {
                        addResult(i, item);
                    }
                } catch (e) {
                    reject(e);
                }
            } else {
                // item是普通值,直接把值加到result中
                addResult(i, item);
            }
        }
    });
}

Promise.race()

关键点

  1. Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
  2. 只要有一个实例率先改变状态,整个promise的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给promise的回调函数。
// 只要有一个promise成功整个race就resolve
Promise.race = function(arr){
    return new Promise((resolve, reject) => {
        for (let i = 0; i < arr.length; i++){
            let item = arr[i];

            if (isThenable(item)){
                try {
                    let then = item.then;

                    if (typeof then === 'function') {
                        then(resolve, reject);
                    } else {
                        resolve(item);
                    }
                } catch (e) {
                    reject(e);
                }
            } else {
                resolve(item);
            }
        }
    });
}

思维导图

promise.then

image.png

resolvePromise

image.png

测试

如何测试我们自己的Promise是否符合Promises/A+规范呢? 开源社区提供了一个包用于测试我们的代码:promises-aplus-tests

1首先 安装 promises-aplus-tests:npm install promises-aplus-tests

2然后 添加一下代码:

// 实现一个promise的延迟对象 defer, 在我们测试的时候需要用到
MyPromise.defer = MyPromise.deferred = function() {
    let deferred = {};

    deferred.promise = new MyPromise((resolve, reject) => {
        deferred.resolve = resolve;
        deferred.reject = reject;
    });

    return deferred;
}

module.exports = MyPromise;

3 更改package.json文件

// promises-aplus-tests 加上你的js文件
"scripts": {
    "test": "promises-aplus-tests test-promise.js",
  },

4 执行命令

npm run test

5 检查测试结果,如果全是绿色,说明你的代码符合Promises/A+规范(没有完成的promise.all() 和 promise.rase()不会影响我们已有代码的测试结果) 屏幕快照 2021-04-27 08.54.12 PM

image.png

最终代码(含测试代码):

github.com/Aaron-Gym/K…

希望这篇文章能够帮助同学们更好的理解Promise的原理,有疑惑或者不懂的地方请随时留言讨论,如果这篇文章有帮助到同学们,请留一个赞吧。