手写一个Promise ?——(微任务版本)

712 阅读11分钟

Promise基本介绍

Promise的出现, 为解决异步回调时, 避免回调地狱提供了更直观的处理方案, 是比回调函数更直观和强大的原生api。 相关规范本身在 ES6之前就已经存在了, 只是在ES6中得以标准化, 成为了语言标准中的一部分。

它主要有以下两个特点:

(1)对象的状态不受外界影响 。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。

基本用法

下面简单介绍一下 基本用法。


// basic 

const promise = new Promise(function(resolve, reject) {
  // ...code  此处应该是一个异步操作, 并通过异步操作得到一个结果 ,用于判断成功 或者失败

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
}).then(res => {
    console.log(res); // success callback
}, err=>{
    throw err;        // failed callback
});

Promise除了可以通过 then 的第二个参数进行异步错误回调以外, 也可以通过 catch方法进行报错捕获。 而 finally方法,则不管回调是成功还是失败都会在 最后执行。

除此简单应用以外, 还有 静态方法 allrace , allSettled , any等等。 本篇文章只实现最基本的all方法, 其他静态方法 大同小异,不作赘述。

作为一个初入前端的精神小伙, 不禁对其实现产生了兴趣。 于是收集了一下 Promise 规范中的部分要求, 并尝试手动实现相关的功能。

Promise A+ 规范

以下列出规范中提出的若干点, 更详细的内容 可以查阅 Promise A+ 规范

1. 基本特征

  1. Promise是一个通过构造函数生成的对象,对象应该接收一个函数作为参数, 且函数的两个参数应该分别是resolvereject函数(用以提醒内部异步是否成功或失败, 同时修改内部的状态)。

  2. 此对象原型上应该有一个then方法, 且应该有两个参数,分别是两个回调函数, 分别作为成功 和失败的回调。

  3. 内部应该有一个 value 属性表示成功回调中的变量 , 有一个 reason 属性表示失败回调中的变量。

2. 必须要求

  1. 一个Promise中的 state状态属性必须是 pending , fulfilled, rejected 中的一个。且默认妆台为 pending 。 除了pending状态可以变成另外两个状态以外, 另外两个状态不能相互改变。

  2. 状态改变只能在 resolve 或者 reject函数中进行, 且状态本身外界无法修改。

  3. then方法支持两个参数,且都是函数, 分别是成功和失败的回调。如果对应的回调函数(参数)没有传, 必须被省略。

     promise.then([onFulfilled, onRejected])
    

一点一点来写吧!

通过以上描述可以 比较简单的搭一个 构造函数的框架( 用的class因为代码少 , ES5中实现一样的,只要分清 原型和内部变量即可 )


;(function(global){

    const STATUS_PENDING = Symbol("pending");
    const STATUS_FULFILLED = Symbol("fulfilled");
    const STATUS_REJECTED = Symbol("rejected");

    class Promise2 {

        constructor(executor){
            
            this.state = STATUS_PENDING; // 2.1

            this.value = undefined;  // 1.3
            this.reason = undefined; // 1.3

            let resolve = (res)=>{
                if(this.state === STATUS_PENDING){ // 2.2
                    this.state = STATUS_FULFILLED;

                    this.value = res;
                }
            }
            
            let reject = (err)=>{
                if(this.state === STATUS_PENDING){ // 2.2
                    this.state = STATUS_REJECTED;

                    this.reason = err;
                }
            }

            executor(resolve, reject);  // 1.1 

        }

        then( onFulfilled, onRejected ){ // 1.2  2.3

        }

    }

    global.Promise2 = Promise2; // 用于跟 Promise作比较, 防止替换

})(window)

Promise.prototype.then() 实现

由于 then方法第一个参数(onFulfilled)是 成功的回调, 因此它应该在 resolve执行完毕后执行, 而resolve执行完毕的判断依据就是 state 变成了 fulfilled。同时应该将 resolve中修改后的 value当作自己的 参数传给onFulfilled。第二个参数(onRejected)同理。


class Promise2 {
    ...

    then( onFulfilled, onRejected ){

        if(this.state === STATUS_FULFILLED) {
            onFulfilled(this.value);
        }

        if(this.state === STATUS_REJECTED) {
            onRejected(this.reason);
        }
        
    }
}

假设执行以下代码:


new Promise2((resolve, reject)=> {
    
    console.log('start')
    
    resolve('ss')
}).then(res => {

    console.log(res);
})

控制台打印如下:

start
ss

在继续写后面内容之前, 我需要捋一下 上述调用到底做了哪些事。

明确一点,以上测试代码 都是同步的, 当按照 调用栈的内容来看待以上的执行过程时, 大概是这样的

  1. 首先new 了一个 Promise2对象, 并按要求传入了一个 函数作为参数, 此函数在执行到executor(resolve, reject);时, executor 函数进入栈底。

  2. 进程进入executor 函数内部开始执行内部代码, 并打印了 start。此时同步代码中接着执行 resolve('ss')。 于是 resolve 进入函数调用栈,此时位于倒数第二层,同时也算是顶层。

  3. 进程进入 resolve函数内部开始执行内部代码, 修改了状态后, 将传入的字符串ss 赋值给了 this.value。 至此resolve执行完毕, 并出栈。

  4. execurtor执行完毕, 并出栈, 栈空。 此时 new出的Promise2对象 创建完毕。

  5. 接着后面紧跟着then方法, 此方法入栈。 内部通过判断state得知,当前的状态为 fulfilled。 因此 执行第一个参数,同时将value传给此方法当作回调函数的参数。

  6. 此回调函数定义then方法中的第一个参数, 即第一个函数。 此时入栈, 并执行 console.log(res)。 于是打印出了 ss

  7. 回调函数出栈, then函数出栈,调用栈空, 以上代码执行完毕。

至此,大概知道同步代码整个执行过程是什么样子了。 但是还远远不够,假设将 excurtor改成异步的函数时, 代码如下执行:


new Promise2((resolve, reject)=> {
    
    console.log('start')
    setTimeout(()=>{
        resolve('ss')
    },0)
}).then(res => {
    console.log(res);
})

结果只打印了 startthen里的回调并没有执行, 原因很简单, 由于setTimeoutresolve放在当前同步代码后执行, 执行到then时, 此时的 resolve还没执行, 于是状态还保留在pending状态,而这个状态我们并没有考虑应该如何处理。

于是我们需要实现一个功能: 如果执行到then时, resolve或者reject还么有被执行说明,当前还在等待状态, 需要将then的各自回调存储起来, 等到resolvereject执行时, 再遍历对应的回调集合 并执行。按照这个思路 我们需要定义两个新的变量来存储 各自的回调。改一下代码如下

... 
class Promise2 {
    constructor(excurtor) {
        this.state = STATUS_PENDING; 

        this.value = undefined; 
        this.reason = undefined;

        this.onFulfilledArr = []; // then 的 成功回调 集合
        this.onRejectedArr = []; // then 的 失败回调 集合(或catch)

        let resolve = (res)=>{
            if(this.state === STATUS_PENDING){ // 2.2
                this.state = STATUS_FULFILLED;

                this.value = res;

                this.onFulfilledArr.forEach(fn => fn()); // resolve执行, 状态 改变为成功, 遍历成功的数组 ,并执行刚才存起来的所有 onFulfilledFn集合 
            }
        }
        
        let reject = (err)=>{
            if(this.state === STATUS_PENDING){ // 2.2
                this.state = STATUS_REJECTED;

                this.reason = err;

                this.onRejectedArr.forEach(fn => fn()); // reject 状态 改变为失败, 遍历失败的数组 ,并执行刚才存起来的所有 onRejectedFn集合
            }
        }

        executor(resolve, reject);  // 1.1 

    }

    then( onFulfilled, onRejected ){ // 1.2  2.3
        if(this.state === STATUS_FULFILLED) {
            onFulfilled(this.value);
        }

        if(this.state === STATUS_REJECTED) {
            onRejected(this.reason);
        }

        if(this.state === STATUS_PENDING) {
            this.onFulfilledArr.push(()=>{ onFulfilled(this.value) }); // 状态为pending, 将成功回调存起来

            this.onRejectedArr.push(()=>{ onRejected(this.reason) }); // 状态为pending, 将失败回调存起来
        }
    }
}

此时再去测试 以上 setTimeout例子。 我们先在控制台 打印了start , 在 1秒后, 控制台打印了 ss。 延迟执行回调功能实现, 此时一个最基本的 异步行为完成。

如果对设计模式比较熟悉的人, 可能已经看出来了,其实这里是用到了一个 订阅发布者模式

但是!! 这还远远不够!!

按照 Promise A+ 的规范 , 关于 excurtorthen方法还有以下几点需要实现:

  1. excurtor 内部如果报错, 需要被 reject 捕获。

  2. then方法需要返回一个Promise。同时支持链式调用。

  3. 如果then方法没有接收到函数作为参数, 则需要将结果当作新返回的Promise中 resolve的回调结果。 即 值的穿透。

  4. 需要区别 宏任务和微任务, 在有宏任务和Promise和同步代码同时执行时, Promise的表现应该与微任务想通过。

开始新的分析。

报错需要被 reject捕获,不难联想到捕获错误的 try catch, 其实就是在catch块中, 将错误 给到reject。我们稍微改一下 constructor结尾代码


constructor(excurtor){
    ...

    try{
        excurtor(resolve, reject);
    }catch(err){
        reject(err) // 如果报错直接 通知给 reject, 进入 错误回调
    }
}

then需要返回一个 Promise, 同时需要支持链式调用,实质就是 返回一个已经执行了 resolvePromise, 因为只有这样才能把前者的结果当作结果传给新的Promisethen。 同时值得穿透实际上就是当发现没有给回调参数时, 给个默认值回调, 并把结果传给新的 Promiseresolve。再由于, 此处的返回的 Promise中的resovle执行的是 使用者传入的一个函数,此函数不在内部可控范围内, 那么也就不能保证一定不报错, 为了能捕获到对应的错误, 此处我们也需要通过try catch来进行修改。

我们的then方法修改如下。

...
then( onFulfilled, onRejected){

    onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : res => res;

    onRejected = typeof onRejected == 'function' ? onRejected : err => { throw err}; // 由于 throw err 是一个语句, 不能被return, 所以需要用大括号包裹起来

    return new Promise2((resolve, reject)=>{
        if(this.state === STATUS_FULFILLED) {
            try {
                resolve(onFulfilled(this.value));

            }catch(err){
                reject(err);
            }
        }

        if(this.state === STATUS_REJECTED) {
            try {
                reject(onRejected(this.reason));
            }catch(err){
                reject(err);
            }
        }

        if(this.state === STATUS_PENDING) {
            this.onFulfilledArr.push(()=>{ 
                try{
                    resolve(onFulfilled(this.value))

                }catch(err){
                    reject(err)
                } 
            }); 

            this.onRejectedArr.push(()=>{ 
                try {
                    reject(onRejected(this.reason))

                }catch(err){
                    reject(err)
                }
            }); 
        }
    })
}

至此then的链式调用,和值的穿透就已经实现了, 那么我们接着来测试一下它的异步问题。

new Promise2((resolve)=>{
    console.log('start');

    resolve('three')
}).then(res => {
    console.log(res)
})

console.log('second')

打印如下:

start
three
second

明显处理有问题,因为即使传入的函数体是一个同步代码, then函数内部的回调也应该是一个异步回调。 那么我们还需要将 then中的回调改成异步的 , 先用 setTimeout实现。


function observeCallback(callback){
    setTimeout(()=>{
        callback();
    },0)
}


...
then( onFulfilled, onRejected){

    onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : res => res;

    onRejected = typeof onRejected == 'function' ? onRejected : err => { throw err}; // 由于 throw err 是一个语句, 不能被return, 所以需要用大括号包裹起来

    return new Promise2((resolve, reject)=>{
        if(this.state === STATUS_FULFILLED) {

            observeCallback(()=>{
                try {
                    resolve(onFulfilled(this.value));

                }catch(err){
                    reject(err);
                }
            })
            
        }

        if(this.state === STATUS_REJECTED) {
            observeCallback(()=>{
                try {
                    reject(onRejected(this.reason));
                }catch(err){
                    reject(err);
                }
            })
        }

        if(this.state === STATUS_PENDING) {
            this.onFulfilledArr.push(()=>{ 
                observeCallback(()=>{
                    try{
                        resolve(onFulfilled(this.value))

                    }catch(err){
                        reject(err)
                    }
                })
                 
            }); 

            this.onRejectedArr.push(()=>{ 
                observeCallback(()=>{
                    try {
                        reject(onRejected(this.reason))

                    }catch(err){
                        reject(err)
                    }
                })
                
            }); 
        }
    })
}

以上代码比较简单, 就是编写了一个observeCallback方法, 此方法内部 立即执行一个 setTimeout方法,变为异步执行。 此时再测试上述的代码 ,表现正常

start
second
three

但是setTimeout在 异步任务中, 属于宏任务,并不是微任务。我们常见的宏任务 有 setTimeout, 微任务 有 process.nextTicknodejs环境)、 MutationOberseve(DOM监听)。由于我们是在浏览器端使用,所以通过MutationObserve来实现微任务下的异步行为。

于是让我们来重写一个 observeCallback方法:

function observeCallback(callback){
    let randomStr = Math.floor(Math.random()*1000)+''; // 隐式类型转换 =》 String

    let textNode = document.createTextNode(randomStr);  // 创建一个DOM => TextNode =》 文本节点 =》 开销小于 元素节点

    let observe = new MutationObserver(()=>{
        callback()
    })

    observe.observe( textNode, {
        characterData: true // 如果节点中的文本发生变化就会触发 回调    监听
    })

    textNode.textContent = randomStr + 'promise';  // 这里只是为了 修改已有TextNode中的 文本内容, 用来手动触发 mutationObserve 中的回调       手动触发监听事件
}

我们通过代码测试一下:

setTimeout(()=>{
    console.log('three')
},0)

new Promise2((resolve, reject)=>{
    console.log('start');
    resolve('second')
}).then(res=>{
    console.log(res)
})

console.log('four')

此时以上控制台打印如下:

start 
four
second
three

打印顺序正常, then方法实现完毕。

Promise.resolve() 方法实现

Promise.resolve()这个调用的样子就不难看出, resolve方法是Promise内部的一个静态方法, 关于此方法的描述大概是这样的:

  1. resolve方法接收一个任意值, 并对此值解析成一个Promise对象。
  2. 如果值是Promise则返回这个Promise
  3. 如果这个值是thenable(对象中带有then方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态

让我们接着写


class Pormise2 {
    constructor(excurtor){
        ... 
        let resolve = (res)=>{
            if(res instanceof Promise2) {
                return res;
            }


            if(this.state === STATUS_PENDING){ 
                this.state = STATUS_FULFILLED;

                this.value = res;

                this.onFulfilledArr.forEach(fn => fn()); 
            }
        }
    }

    static resolve(value){
        return new Promise((res, rej)=>{
            try {
                res(value);
            }catch(err){
                rej(err);
            }
        })
    }
}

关于如何理解 thenable这一点,我没太看懂 ,所以只列了出来。并未实现,知乎用户齐小神的实现版本中, 有对then的处理,感兴趣的可以去看看。

Promise.reject() 方法实现

...
static reject(data){
    return new Promise((res, rej)=>{
        rej(data)
    })
}

Promise.prototype.catch() 实现

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

Promise.prototype.finally() 实现

finally(callback){
    // 当前的 then 和 catch 走完了 才走,所有要返回当前的 then ,此时的 then 实质 是 内部 在调用链上加的一个 新的then,属于在 执行finally之前执行的一个then, 所以
    // 只要没报错, 按照 then的 值得穿透, 这里得 then必能拿到, 在这个then中同时把值返回,则finally必能获取到 。
    return this.then(res => {
        return new Promise2.resolve(callback()).then(()=> res)
    }, err => {
        return Promise2.resolve(callback()).then(()=>{throw err})
    })
}

Pormise.all() 实现

all方法接受一个可迭代的集合, 可以是常规值, 也可以是Promise的集合。

  1. 如果是Promise集合时,只有当所有的 Promise中的resolve都执行后, 才会触发 all的完成行为。
  2. 如果Promise集合中,任意一个执行了reject, 则执行all的失败回调。
static all(iterator){
    if(typeof iterator[Symbol.iterator] != 'function'){
            // 没有 iterator 接口 , 直接 抛错
        throw new TypeError("the data have not iterator . ")   
    }
    
    // 所有 执行完毕 才 完毕, 一个执行错误 就错误
    let resultArr = [];

    let index = 0;

    return new Promise2((resolve, reject)=>{
        try{
            iterator = Array.of(...iterator);

            let processPromiseResolve = (value)=>{
                resultArr[index] = value;
                
                // 判断情况
                if(++index >= iterator.length){
                    resolve(resultArr);
                }
            }

            for(let i=0, len = iterator.length ; i<len ; i++){
                let value = iterator[i];
                index = i; 
                if(value && typeof value.then == 'function'){
                    // 说明有then方法 , 假设是 Promise2  这里不直接 判断是否是 Promise2是由于, 自己写的应该能跟 原生实现的来回调用而不出差错,如果这里写死, Promise将会进入错的判断分支

                        // 手动监听 Promise的回调
                    value.then(res=> {
                        // Promise

                        processPromiseResolve(res, i)
                    }, reject)
                    
                }else{
                    // 非 Promise

                    processPromiseResolve(value,i);
                }
            }
        }catch(er){
            reject(er);
        }
    })
}

如有没考虑周到的地方, 欢迎批评指正。