从事件循环机制深挖到Promise 源码实现

604 阅读9分钟

1. 前言

之前有一篇文章是关于详细聊事件循环机制的(eventLoop), 本文将深入到promise中,详细研究promise的实现,从根本上解决对promise的一些认识误区。关于promise的面试题也基本是必问考点,如果面试官对promise的认识非常深刻,那么他一定不会只问你promse.all是做什么的,他一定会结合setTimeout、 promise的各种写法来迷惑你,看你到底是不是从根本上掌握了。

我在面试别人的时候,也经常会问一些promise的问题。 所以,今天跟随我的问题,一起揭开promise的底层实现。要读懂这篇文章,需要对事件循环有一个相对清晰的认识,建议看一下我的这篇文章: # 从setTimeout说到事件循环机制(event-loop)

2.从一道可以当面试题的小题目开始

setTimeout(() => {console.log(1)}, 0);
new Promise((resolve, reject) => {
    console.log(2);
    resolve('res1');
    console.log(3);
}).then(res => console.log(4))
.then(data => console.log(5))
setTimeout(() => {
    console.log(6);
    Promise.resolve().then(res => console.log(7));
}, 0);

setTimeout(() => {
    console.log(8)
})
console.log(9);

大家可以默念一下结果,然后跑一下看看结果。 如果使都对的,那么恭喜你,对promise的基础还算掌握的不错。

那么我们继续, 进入使用Promise的真正场景。

3.Promise的真实使用场景举例

下面再来一个题:

要实现一个小功能,实现第一个方法完成 -> sleep3秒钟 -> 第二个方法执行完成

要实现一个sleep的功能,就说明逻辑是需要有执行顺序的。大家可以尝试实现一下。

目前我们可以使用 async awaitpromise实现。因为我们这篇文章介绍的使promise,那么就使用promise来实现。

// 创建一个队列
const queue = [];

const fn1 = () => new Promise((resolve, reject) => {
    console.log('fn1 executed');
    resolve();
});

const sleep = () => new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('sleep executed');
        resolve();
    }, 3000)
});

const fn2 = () => new Promise((resolve, reject) => {
    console.log('fn2 executed');
    resolve();
});

queue.push(fn1, sleep, fn2);

const execute = () => {
    // 执行
    let queueExe = Promise.resolve();
    for(let i=0; i< queue.length; i++) {
        queueExe = queueExe.then(queue[i]);
    }
}
// 执行
execute();

promise的功能如此强大,不止只处理一个异步请求这么简单。通过上面两道题,其中比较容易令人困惑的是:

  • 都说使promise是微任务,但是为什么例子1中的 new Promise()中的console.log会马上执行? 所说的微任务是体现在哪里?
  • promise是如何通过resolve等方法做到可以顺序执行的?

所以有必要自己实现一个promise了。

4.代码实现一个Promise

针对上面提的两个问题,要从底层掌握,最好的方法还是实现一遍。 所以我通过写一个promise来帮助大家理解,也帮助自己加深记忆,毕竟时间长了不拿出来熟悉熟悉,也会忘记一些细节。

4.1 核心思想

先把思维框架搭建起来

    const MyPromise = (fn) => {
        let status = 'pending';
        let newValue = null;
        
        this.then = (onFulfilled) => {
            // 因为then是可以链式调用的,并且我们也知道then会返回一个新的promise
            return new MyPromise((resolve, reject) => {
                // ...
            })
        }
        
        const resolve = (value) => {
            
        }
        
        const reject = (value) => {

        }
        
        // 把resolve和reject函数传入
        fn(resolve, reject);
    }
    
  new MyPromise((resolve, reject) => {
        // ...
        resolve('step1 done');
  }).then((res) => {
      // ...
  }).then(res => {
      // ...
  })

目前只是把框架简单的搭了一下,但是这是核心的。 在这里我们会关注到resolve和reject到底是怎么来的,并不局限在只知道调用,也会关注到then()内部是怎么实现的,同样会关注到'pending', 'fulfilled' 的作用是什么等等。

4.2 对框架进行填充

下面开始对立面的逻辑进行填充。为了主体逻辑的尽量明了,会先不去实现reject的逻辑。

function MyPromise(cb) {
    const callbacks = [];
    let status = 'pending';
    let newValue = null;
    const handle = (fnObj) => {
        //  处理执行问题
        if(status === 'pending') {
            callbacks.push(fnObj);
            return;
        }
        // 执行
        if(!fnObj.onFulfilled) {
            resolve(newValue);
            return;
        }
        const res = fnObj.onFulfilled(newValue);
        fnObj.resolve(res);
    }
    this.then = (onFulfilled) => {
        // 这里注意此回调的执行环境
        return new MyPromise((resolve) => {
            handle({
                onFulfilled,
                resolve
            })
        })
    }

    const resolve = (value) => {
        const handleCb = () => {
            while(callbacks.length) {
                let retCb = callbacks.shift();
                handle(retCb);
            }
        }
        // 先放到任务队列中,等待下个事件循环时执行
        const fn = () => {
            if(status != 'pending') return;
            status = 'fulfilled';
            newValue = value;
            // 去callbacks中取callback执行
            handleCb();
        }
        // 这样模拟其实是不合适的,因为设置成了宏任务,其实是微任务
        // 但具体的微任务因为是v8中处理的,所以
        // 目前只能先用setTimeout来模拟
        setTimeout(fn, 0);
    }
    cb(resolve);
}


new MyPromise((resolve) => {
    console.log(1);
    resolve('init');
    resolve('new data') // 这里就不会执行了
}).then((res) => {
    console.log(res);
}).then((data) => {
    console.log(3);
})

这样,就能够做到基本的promise。 其中的两个难点:

  • resolve的作用和实现。也就是说,当微任务(例子中使用setTimeout模拟)执行时机到了以后,就从callbacks中获取cb执行。
  • then的作用。 then中又调用并实例化了MyPromise函数,并且把then中的回调当做 onFulfilled函数存储到callbacks中。

这两步其实有些绕,特别是then方法中又调用MyPromise函数。

4.3 更复杂一些的场景

如果看懂了上面的代码,那么我们看一些更复杂的场景。我们要实现reject和对应的catch逻辑。

这里其实也对应了一个面试中的坑。当把这一章看完后,相信对reject就有了更深刻的认识。

function MyPromise(cb) {
    const callbacks = [];
    let status = 'pending';
    let newValue = null;
    const handle = (fnObj) => {
        //  处理执行问题
        if(status === 'pending') {
            callbacks.push(fnObj);
            return;
        }
        const callback = status === 'fulfilled' ? fnObj.onFulfilled : fnObj.onRejected;
        const execute = status === 'fulfilled' ? fnObj.resolve : fnObj.reject;

        if(!callback) {
            execute(newValue);
            return;
        }

        try{
            const result = callback(newValue);
            // 把页面代码中的回调函数返回值给到对应的resolve或reject函数
            // 如果没有返回值,那就是undefined
            execute(result);
        } catch(e) {
            // 直接进reject
            fnObj.reject(e)
        }
    }

    // 在promise API中,then中也是可以通过第二个回调函数捕获reject的
    this.then = (onFulfilled, onRejected) => {
        // 这里注意此回调的执行环境
        return new MyPromise((resolve, reject) => {
            handle({
                onFulfilled,
                onRejected,
                resolve,
                reject
            })
        })
    }

    // 更多情况用的是catch方法
    this.catch = (onReject) => {
        // 同样需要返回一个新的promise
        // 那么可以直接调用this.then
        this.then(null, onReject);
    }
    

    const handleCb = () => {
        while(callbacks.length) {
            let retCb = callbacks.shift();
            handle(retCb);
        }
    }
    const resolve = (value) => {
        
        // 先放到任务队列中,等待下个事件循环时执行
        const fn = () => {
            if(status != 'pending') return;
            status = 'fulfilled';
            newValue = value;
            // 去callbacks中取callback执行
            handleCb();
        }
        // 这样模拟其实是不合适的,因为设置成了宏任务,其实是微任务
        // 但具体的微任务因为是v8中处理的,所以
        // 目前只能先用setTimeout来模拟
        setTimeout(fn, 0);
    }

    const reject = (value) => {
        // 原理上跟resolve是相同的
        const fn = () => {
            if(status !== 'pending') return;
            status = 'rejected';
            newValue = value;
            handleCb();
        }
        setTimeout(fn, 0);
    }
    cb(resolve, reject);
}


new MyPromise((resolve, reject) => {
    console.log(1);
    reject('init error');
}).then((res) => {
    console.log(res);
}).then((data) => {
    console.log(3);
}).catch((e) => {
    console.log('捕获到了错误', e);
})

// 结果是:
// 1
// 捕获到了错误 init error

4.4 更更复杂的场景

通过上面的操作,我们已经完成了80%的Promise功能。剩下的是 thencatch中,有异步数据获取的场景。 这种场景下,我们需要等待数据获取成功后才继续走下面的then或catch。 继续举个例子:

    new MyPromise((resolve, reject) => {
        console.log(1);
        resolve('https://get/asncyData/api');
    }).then((res) => {
        let resData = null;
        axios.get(res).then(data => {
            // 把数据处理好
            resData = data;
        })
    }).then((data) => {
        // 这时应该怎么处理,我们才能在这里面获取到上面的真实输出??
        console.log(data);
    }).catch((e) => {
        console.log('捕获到了错误', e);
    })

要在第二个then中把axios中的请求结果获取到,那么最好的方法还是需要通过一个Promise来实现,或者通过async await来实现。 这里我们既然研究Promise,那就把Promise研究透,用Promise来实现。

    new MyPromise((resolve, reject) => {
        console.log(1);
        resolve('https://get/asncyData/api');
    }).then((res) => {
        return new MyPromise((resolve, reject) => {
            let resData = null;
            axios.get(res).then(data => {
                // 把数据处理好
                resData = data;
                resolve(resData);
            })
        })
    }).then((data) => {
        // 这时就能获取到上一个then中的数据了
        console.log(data);
    }).catch((e) => {
        console.log('捕获到了错误', e);
    })

一般来说,这就是我们遇到的比较复杂的场景了:Promise中套Promise

主要思路是: 在then中的回调方法执行时,需要判断一下这个回调是不是MyPromise的实例, 如果是的话,就等执行完成后,再继续下面的操作。 这其中涉及的逻辑比较烧脑,涉及到递归处理一些事务,建议大家慢慢看,不要一知半解就翻篇。

下面看一下代码:

let flag = 0;  // 用作辅助查看之间的关系,当然也可以不使用
function MyPromise(cb) {
    debugger;
    this.callbacks = [];
    let status = 'pending';
    let newValue = null;
    this.flag = flag++;
    const handle = (fnObj) => {
        
        //  处理执行问题
        if(status === 'pending') {
            this.callbacks.push(fnObj);
            return;
        }
        const callback = status === 'fulfilled' ? fnObj.onFulfilled : fnObj.onRejected;
        const execute = status === 'fulfilled' ? fnObj.resolve : fnObj.reject;

        if(!callback) {
            execute(newValue);
            return;
        }

        try{
            const result = callback(newValue);
            // 把页面代码中的回调函数返回值给到对应的resolve或reject函数
            // 如果没有返回值,那就是undefined
            execute(result);
        } catch(e) {
            // 直接进reject
            fnObj.reject(e)
        }
    }

    // 在promise API中,then中也是可以通过第二个回调函数捕获reject的
    this.then = (onFulfilled, onRejected) => {
        // 这里注意此回调的执行环境
        return new MyPromise((resolve, reject) => {
            // 
            handle({
                onFulfilled,
                onRejected,
                resolve,
                reject
            })
        })
    }

    // 更多情况用的是catch方法
    this.catch = (onReject) => {
        // 同样需要返回一个新的promise
        // 那么可以直接调用this.then
        this.then(null, onReject);
    }
    

    const handleCb = () => {
        // console.log('callbacks:::', this);
        while(this.callbacks.length) {
            let retCb = this.callbacks.shift();
            handle(retCb);
        }
    }
    const resolve = (value) => {
        // 先放到任务队列中,等待下个事件循环时执行
        const fn = () => {
            if(status != 'pending') return;
            // 当执行完上一个context的handle,执行当前context的resolve
            if (typeof value === 'object' && value instanceof MyPromise) {
                // 需要把promise插入到执行的队列中
                const {then} = value;
                then.call(value, resolve, reject);
                return;
            }
            status = 'fulfilled';
            newValue = value;
            // 去callbacks中取callback执行
            handleCb();
        }
        // 这样模拟其实是不合适的,因为设置成了宏任务,其实是微任务
        // 但具体的微任务因为是v8中处理的,所以
        // 目前只能先用setTimeout来模拟
        setTimeout(fn, 0);
    }

    const reject = (value) => {
        // 原理上跟resolve是相同的
        const fn = () => {
            if(status !== 'pending') return;
            status = 'rejected';
            newValue = value;
            handleCb();
        }
        setTimeout(fn, 0);
    }
    cb(resolve, reject);
}

// 

new MyPromise((resolve, reject) => {
    console.log(1);
    resolve('https://get/asncyData/api');
}).then((res) => {
    return new MyPromise((resolve, reject) => {
        setTimeout(() => {
            resolve({code: 0, msg: '模拟ajax获取数据返回的结果'})
        }, 2000);
    })
}).then((data) => {
    // 这时就能获取到上一个then中的数据了
    console.log('获取到的数据::', data);
}).catch((e) => {
    console.log('捕获到了错误', e);
})

前几步基本上是线性的逻辑,相对比较好理解一些, 最后这种复杂场景,需要细细的理解。 我自己在想最后一部分的时候就绕了挺长时间。

纯ES6版本


/**
 * 用class实现一个MyPromise
 * 在promise-base的基础上,实现更复杂的功能:当then方法中需要处理另一个Promise时,应该怎么做
 */
const _status = new WeakMap();
const _resolveValue = new WeakMap();

class MyPromise {
    callbacks = [];
    constructor(cb) {
        // 可以使用weakmap定义私有变量
        _status.set(this, 'pending');
        _resolveValue.set(this, void 0);
        
        // 定义局部变量
        const handle = (fnField) => {
            const status = _status.get(this);
            if(status === 'pending') {
                this.callbacks.push(fnField);
                return;
            }
            // 否则就要执行
            const callback = status === 'fulfilled' ? fnField.onFulfilled : fnField.onRejected;
            const execute = status === 'fulfilled'? fnField.resolve : fnField.reject;

            if(!callback) {
                execute(_resolveValue.get(this));
                return;
            }
            try {
                const result = callback(_resolveValue.get(this));
                execute(result);
            } catch (error) {
                fnField.reject(error);
            }

        }

        const handleCb = () => {
            while(this.callbacks.length) {
                const targetCb = this.callbacks.shift();
                handle(targetCb);
            }
        }
        const resolve = (value) => {
            const fn = () => {
                // 目的是需要去取出对应的callback并执行
                if(_status.get(this) !== 'pending') {
                    return;
                }
                // 如果返回的value是个新的promise,那么需要把这个新的promise插入到下一个then中的回调
                // 执行前,否则时序上就会出问题
                if(typeof value === 'object' && value instanceof MyPromise) {
                   value.then(resolve, reject);
                   return;
                }
                _status.set(this, 'fulfilled');
                _resolveValue.set(this, value);
                handleCb();

            }
            setTimeout(fn, 0);
        }

        const reject = (errorMsg) => {
            const fn = () => {
                if(_status.get(this) !== 'pending') {
                    return;
                }
                if(typeof value === 'object' && value instanceof MyPromise) {
                    value.then(resolve, reject);
                    return;
                }
                _status.set(this, 'rejected');
                _resolveValue.set(this, value);
                handleCb();
            }
            setTimeout(fn, 0);
        }
        // 定义实例方法和属性
        
        this.then = (onFulfilled, onRejected) => {
            return new MyPromise((resolve, reject) => {
                handle({
                    onFulfilled,
                    onRejected,
                    resolve,
                    reject
                })
            })
        }

        cb(resolve, reject);
    }
}

new MyPromise((resolve, reject) => {
    console.log(1);
    // 模拟异步ajax操作
    setTimeout(() => {
        resolve('init promise executed');
    }, 1000);
}).then((res)=> {
    console.log(2, res);
    return new MyPromise((resolve, reject) => {
        setTimeout(() => {
            // 这时候的resolve是外层then方法中的resolve
            resolve('目标promise也执行完了');
        }, 2000);
    })
}).then((res) => {
    console.log(3, res);
})

后记

这篇文章并不是完整的promise实现,还少了一些方法,也不那么健壮。 但从深入分析原理来说,应该是足够使用了。

由于工作比较忙,从8月30号开始开头,到今天9月7号正式写完,用了一周的时间, 有点慢,但是是在工作外时间一个字一个字的敲出来,代码也是从 0到1,从简到难一点一点的实现出来,所以说,自己认为,这是一篇诚意之作,欢迎大家浏览和交流~~