手写Promise:从易到难,一步一步补完Promise,讲解每一行代码的做用

前言

手写Promise一直是前端童鞋很是头疼的问题,也是面试的高频题。网上有不少手写Promise的博客,但大部分都存在或多或少的问题。
下面咱们根据A+规范,手写一个Promise前端

基础结构

在此部分,先把Promise的基础结构写出来。
直接上代码node

// 规范2.1:A promise must be in one of three states: pending, fulfilled, or rejected.
// 三个状态:PENDING、FULFILLED、REJECTED
const PENDING = Symbol();
const FULFILLED = Symbol();
const REJECTED = Symbol();

// 根据规范2.2.1到2.2.3
class _Promise {
    constructor(executor) {
        // 默认状态为 PENDING
        this.status = PENDING;
        // 存放成功状态的值,默认为 undefined
        this.value = undefined;
        // 存放失败状态的值,默认为 undefined
        this.reason = undefined;

        // 成功时,调用此方法
        let resolve = (value) => {
            // 状态为 PENDING 时才能够更新状态,防止 executor 中调用了两次 resovle/reject 方法
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
            }
        };

        // 失败时,调用此方法
        let reject = (reason) => {
            // 状态为 PENDING 时才能够更新状态,防止 executor 中调用了两次 resovle/reject 方法
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
            }
        };

        try {
            // 当即执行,将 resolve 和 reject 函数传给使用者
            executor(resolve, reject);
        } catch (error) {
            // 发生异常时执行失败逻辑
            reject(error);
        }
    }

    // 包含一个 then 方法,并接收两个参数 onFulfilled、onRejected
    then(onFulfilled, onRejected) {
        if (this.status === FULFILLED) {
            onFulfilled(this.value);
        }

        if (this.status === REJECTED) {
            onRejected(this.reason);
        }
    }
}

export default _Promise;

接下来咱们用测试代码测一下面试

const promise = new _Promise((resolve, reject) => {
        resolve('成功');
        setTimeout(() => {
            console.log('settimeout1');
        }, 0);
    })
        .then(
            (data) => {
                console.log('success', data);
                setTimeout(() => {
                    console.log('settimeout2');
                }, 0);
            },
            (err) => {
                console.log('faild', err);
            }
        )
        .then((data) => {
            console.log('success2', data);
        });

控制台打印
image.png
能够看到,在executor方法中的异步行为在最后才执行
并且若是把resolve方法放到setTimeout中,会没法执行
这固然是不妥的。
接下来咱们优化一下异步数组

executor方法中的异步

在上一小节中,咱们将resolve的结果值存放到了this.value里。
优化后的代码以下:promise

// 规范2.1:A promise must be in one of three states: pending, fulfilled, or rejected.
// 三个状态:PENDING、FULFILLED、REJECTED
const PENDING = Symbol();
const FULFILLED = Symbol();
const REJECTED = Symbol();

class _Promise {
    constructor(executor) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined;
        // 存放成功的回调
        this.onResolvedCallbacks = [];
        // 存放失败的回调
        this.onRejectedCallbacks = [];
        // 这里使用数组,是由于若是屡次调用then,会把方法都放到数组中。
        // 可是目前这个版本还不支持then的链式调用

        let resolve = (value) => {
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
                // 依次将对应的函数执行
                // 在此版本中,这个数组实际上长度最多只为1
                this.onResolvedCallbacks.forEach(fn => fn());
            }
        }

        let reject = (reason) => {
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
                // 依次将对应的函数执行
                // 在此版本中,这个数组实际上长度最多只为1
                this.onRejectedCallbacks.forEach(fn => fn());
            }
        }

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

    then(onFulfilled, onRejected) {
        if (this.status === FULFILLED) {
            onFulfilled(this.value)
        }

        if (this.status === REJECTED) {
            onRejected(this.reason)
        }
        // 上面两个分支是:支持resolve函数执行的时候,若是不在异步行为里执行resolve的话,会当即执行onFulfilled方法

        if (this.status === PENDING) {
            // 若是promise的状态是 pending,须要将 onFulfilled 和 onRejected 函数存放起来,等待状态肯定后,再依次将对应的函数执行
            this.onResolvedCallbacks.push(() => {
                onFulfilled(this.value)
            });

            // 若是promise的状态是 pending,须要将 onFulfilled 和 onRejected 函数存放起来,等待状态肯定后,再依次将对应的函数执行
            this.onRejectedCallbacks.push(() => {
                onRejected(this.reason);
            })
        }
    }
}

咱们用测试方法测一下:浏览器

const promise = new _Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('settimeout1');
                resolve('成功');
            }, 0);
        })
        .then(
            (data) => {
                console.log('success', data);
                setTimeout(() => {
                    console.log('settimeout2');
                }, 0);
                return data;
            },
            (err) => {
                console.log('faild', err);
            }
        )
        .then((data) => {
            console.log('success2', data);
        });

控制台结果:
image.png
能够看到,异步顺序是正确的,先执行settimeout1,再执行success
可是不支持链式的then调用,也不支持在then中返回一个新的Promisedom

支持链式调用的Promise

接下来咱们将完整实现一个支持链式调用的Promis异步

// 规范2.1:A promise must be in one of three states: pending, fulfilled, or rejected.
// 三个状态:PENDING、FULFILLED、REJECTED
const PENDING = Symbol();
const FULFILLED = Symbol();
const REJECTED = Symbol();

class _Promise {
    constructor(executor) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined;
        // 存放成功的回调
        this.onResolvedCallbacks = [];
        // 存放失败的回调
        this.onRejectedCallbacks = [];
        // 这里使用数组,是由于若是屡次调用then,会把方法都放到数组中。
        // 可是目前这个版本还不支持then的链式调用

        let resolve = (value) => {
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
                // 依次将对应的函数执行
                // 在此版本中,这个数组实际上长度最多只为1
                this.onResolvedCallbacks.forEach(fn => fn());
            }
        }

        let reject = (reason) => {
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
                // 依次将对应的函数执行
                // 在此版本中,这个数组实际上长度最多只为1
                this.onRejectedCallbacks.forEach(fn => fn());
            }
        }

        try {
            // 当即执行executor方法
            executor(resolve, reject)
        } catch (error) {
            reject(error)
        }
    }
    
    // 这里就是最关键的then方法
    then(onFulfilled, onRejected) {
        // 克隆this,由于以后的this就不是原promise的this了
        const self = this;
        
        // 判断两个传入的方法是否是funcion,若是不是,那么给一个function的初始值
        onnFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function'?onRejected:reason => { throw new Error(reason instanceof Error ? reason.message:reason) }
        
        // 返回一个新的promise,剩下的逻辑都在这个新的promise里进行
        return new _Promise((resolve, reject) => {
            if (this.status === PENDING) {
                // 若是promise的状态是 pending,须要将 onFulfilled 和 onRejected 函数存放起来,等待状态肯定后,再依次将对应的函数执行
                self.onResolvedCallbacks.push(() => {
                    // 使用settimeout模拟微任务
                    setTimeout((0 => {
                        // self.value是以前存在value里的值
                        const result = onFulfilled(self.value);
                        // 这里要考虑两种状况,若是onFulfilled返回的是Promise,则执行then
                        // 若是返回的是一个值,那么直接把值交给resolve就行
                        result instanceof _Promise ? result.then(resolve, reject) : resolve(result);
                    }, 0)
                    onFulfilled(self.value)
                });

                // 若是promise的状态是 pending,须要将 onFulfilled 和 onRejected 函数存放起来,等待状态肯定后,再依次将对应的函数执行
                
                
                
                // reject也要进行同样的事
                self.onRejectedCallbacks.push(() => {
                    setTimeout(() => {
                        const result = onRejected(self.reason);
                        // 不一样点:此时是reject
                        result instanceof _Promise ? result.then(resolve, reject) : reject(result);
                    }, 0)
                })
            }
            
            // 若是不是PENDING状态,也须要判断是否是promise的返回值
            if (self.status === FULFILLED) {
                setTimeout(() => {
                    const result = onFulfilled(self.value);
                    result instanceof _Promise ? result.then(resolve, reject) : resolve(result);
                });
            }

            if (self.status === REJECTED) {
                setTimeout(() => {
                    const result = onRejected(self.reason);
                    result instanceof _Promise ? result.then(resolve, reject) : reject(result);
                })
            }
        
        })
        // 到这里,最难的then方法已经写完了
    }
}

额外补充

catch、静态resolve、静态reject方法

catch方法的通常用法是
new _Promise(() => {...}).then(() => {...}).catch(e => {...})
因此它是一个和then同级的方法,它实现起来很是简单:函数

class _Promise{
    ...

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

静态resolve、静态reject的用法:
_Promise.resolve(() => {})
这样能够直接返回一个_Promise
这块的实现,参考then中返回_Promise的那一段,就能实现
reject相似测试

class _Promise{
    ...
    
    static resolve(value) {
        if (value instanceof Promise) {
            // 若是是Promise实例,直接返回
            return value;
        } else {
            // 若是不是Promise实例,返回一个新的Promise对象,状态为FULFILLED
            return new Promise((resolve, reject) => resolve(value));
        }
    }
    static reject(reason) {
        return new Promise((resolve, reject) => {
            reject(reason);
        })
    }
}

优化setTimeout变成微任务

最后再说一个关于微任务的
setTimeout毕竟是个宏任务,咱们能够用MutationObserver来模拟一个微任务,只要将下面的nextTick方法替换setTimeout方法便可

function nextTick(fn) {
  if (process !== undefined && typeof process.nextTick === "function") {
    return process.nextTick(fn);
  } else {
    // 实现浏览器上的nextTick
    var counter = 1;
    var observer = new MutationObserver(fn);
    var textNode = document.createTextNode(String(counter));

    observer.observe(textNode, {
      characterData: true,
    });
    counter += 1;
    textNode.data = String(counter);
  }
}

这个方法的原理不难看懂,就是在dom里建立了一个textNode,用MutationObserver监控这个node的变化。在执行nextTick方法的时候手动修改这个textNode,触发MutationObserver的callback,这个callback就会在微任务队列中执行。
注意MutationObserver的兼容性

总结

我我的感受完整理解Promise的源码仍是比较考验代码功底的,一开始建议把源码放在编译器里一点一点调试着看,若是实在不知道怎么下手,也能够把代码背下来,慢慢咀嚼。实际上,背下来以后,人脑对这个东西会有一个缓慢的理解过程,到了某一天会感受恍然大悟。

参考文章

https://zhuanlan.zhihu.com/p/183801144

Promise/A+规范

相关文章
相关标签/搜索