关于promise的一些分享

190 阅读11分钟

1. promise之前

回调函数是我们经常使用的,我们可以在回调函数中去处理我们得到某些数据之后的一些操作,但是如果回调函数嵌套的太多,那可能就头痛了,例如下面的示例:

function show () {
  fn(1);
  setTimeout(() => {
    fn(2)
      setTimeout(() => {
      fn(3)
      setTimeout(() => {
        fn(4)
            setTimeout(() => {
            fn(5)
          },100)
        },100)
      },100)
  },100)

在这个实例中我们想要模拟的场景类似于请求嵌套,这样是没有问题的,但是我们需要去排查问题的时候就会发现,这里面的耦合度太高了,嵌套的太深,以至于我们需要一层一层的去抽丝剥茧才能找到我们需要的作用域。这就是我们所谓的回调地狱。回调本来没问题,但问题是嵌套的太深,就会增加阅读的难度。

那有没有一种方式能更轻松的查看代码,查看逻辑之间的关系喃?这时候Promise的出现就成功的解决了这个问题。

2. 什么是promise?

Promise是异步编程的一种解决方案,它是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。Promise遵循Promise/A+规范,这个规范里面规定了promise需要遵循的规则。

2-1 promise的用法

let p1 = new Promise((resolve,reject)=> {
    resolve(1)
})
p1.then((result) => {
    console.log(result)
},err => {
    console.log(error)
})

这是一个promise的简单使用,示例可以看出Promise是一个类,通过new调用,并且接收一个回调函数,回调函数接收两个函数作为参数,根据A+规范可以知道,resolve是返回成功的值,而reject则是将错误拦截。Promise会返回一个实例对象,这个对象有then方法。

2-2 基本规则

知道了promise的使用,我们再来看看promise的基本规则。 刚刚我们说了Promise/A+规范,那么我们具体来看看这个规范有哪些规则。

  • promise拥有三个状态,分别是pending、fulfiled、reject,这三个状态的转换过程是pending->fulfiled,pending->reject。从这个规范可以看出状态的改变只能从pending开始,一旦变化就不可再逆;
  • fulfiled状态下拥有一个不可变的值value;
  • reject状态下拥有一个不可变的错误信息error;
  • Promise在new的时候,接收一个函数,这个函数有两个参数,resolve为成功,接收一个参数value,reject为失败,接收一个错误信息;
  • Promise在new时接收的函数要立即执行;

3. 代码编写

通过以上的规则我们,我们可以得出一个Promise的基础形态:

3.1 Promise雏形

let PENDING = "pending", FULFILED = "fulfiled", REJECT = "reject";
class Promise {
    constructor(executor) {
        this.status = PENDING;
        this.value = null;
        this.error = null;
        
        let resolve = (value) => {}

        let reject = (reason) => {}
         try {
            if (!executor) {
                reject(new Error('必须传递一个函数'));
            } else {
                executor(resolve, reject);
            }
        } catch (e) {
            reject(e);
        }
    }
}

目前我们看见的就是Promise类最基本的形态,它有状态值,成功后不可变的值,以及一个失败后不可变的拒绝原因,然后立即去执行我们传入的回调函数。

那么我们现在根据规则去给它添加一些简单的逻辑:

     class MyPromise {
            constructor(executor) {
                this.status = PENDING;
                this.value = null;
                this.error = null;

                let resolve = (value) => {
                // 根据规则,状态只能从pending开始
                // 如果是resolve,则表示成功
                    if(this.status === PENDING) {
                        // 将状态修改为成功
                        this.status =FULFILED;
                        // 把值赋给成功时的变量
                        this.value = value;
                    }
                }

                let reject = (reason) => {
                // 根据规则,状态只能从pending开始
                // 如果是reject,则表示失败
                    if(this.status === PENDING) {
                        // 把状态修改为失败
                        this.status =REJECT;
                        // 把拒绝原因存起来
                        this.error = reason;
                    }
                }

                try{
                    if(!executor) {
                        reject(new Error('必须传递一个函数'));
                    }else{
                    executor(resolve,reject);
                    }
                }catch(e){
                    reject(e);
                }
            }
            then = () => {

            }
            catch = (fn) => {
                fn(this.error)
            }
        }

这个时候其实我们就可以去创建一个promise实例了

const p = new MyPromise((resolve,reject) => {
    resolve(1)
})
console.log(p)

在浏览器中打开,发现p实例上面的一些属性:

 MyPromise {status: 'fulfiled', value: 1, error: null, then: ƒ, catch: ƒ}
     catch: (fn) => { fn(this.error) }
     error: null
     status: "fulfiled"
     then: () => { }
     value: 1
     [[Prototype]]: Object

这时候我们发现确实是按照写的发展,状态变成了“fulfiled”,并且值也记录到了,我们再尝试把resolve变成reject,在打印看看。

const p = new MyPromise((resolve,reject) => {
    reject("错误信息")
})
console.log(p)
    MyPromise {status: 'reject', value: null, error: '错误信息', then: ƒ, catch: ƒ}
     catch: (fn) => { fn(this.error) }
     error: "错误信息"
     status: "reject"
     then: () => { }
     value: null
     [Prototype]]: Object

状态也变成了“reject”,拒绝原因也存储起来了,看来我们前面的代码是没有问题的。

3.2 创建then的执行逻辑

这时候我们来完善then方法,因为promise的作用是采用链式调用的方法解决回调地狱给我们带来的一些麻烦。

class MyPromise {
   constructor(executor) {
        // 省略其他代码
   }
    // 省略其他代码
   then = (onfulfiled,onrejected) => {
        if(this.status === FULFILED) {
            onfulfiled(this.value);
        }
        if(this.status === REJECT) {
            onrejected(this.error);
        }
    }

}

这时候我们将刚刚的实例进行then操作,在浏览器执行看看会得到什么:

p.then(() => {
           console.log(1)
    },err => {
        console.log(err)
    }
}
// 把resolve(1)改成reject(“错误信息”)
p.then(() => {
           console.log(1)
    },err => {
        console.log(err)
    }
}

// 代码执行的结果
1  
// 改成reject后
错误信息

这时候能在浏览器看见1,就说明then函数在resolve(1)时执行了onfulfiled函数,在reject("错误信息")时执行了onreject函数。

好了,代码编写到这步,我们已经基本上完成了promise的雏形,我们可以通过new得到一个promise实例,同时可以通过then链式调用一次。但是这好像并不能满足我们的要求,这时候我们只需要一步操作就可以发现我们现在的逻辑有缺陷。

const p = new MyPromise((resolve, reject) => {
            // 我们把resolve函数放在一个setTimeout中执行
            setTimeout(() => {
                resolve(1)
            })
            
        })
        p.then (res => {
            console.log(1)
        },err => {
            console.log(err)
        })

这时候可以在浏览器中发现其实它什么都没有打印,这就是我们所说的第一个缺陷。 出现这个问题的原因是resolve的执行顺序晚于then方法的执行,因为resolve是放在宏任务中执行的,它是在当前所有任务执行完后再执行,也就是说then方法执行时,状态依然为pending,这时候就不会触发onfulfiled方法,也不会出发onreject方法。 要解决这个问题也很容易,在then方法中添加一个判断,如果status=== “pending”,我们就把onfulfiled和onreject分别收集起来,只需要在resolve或者reject方法执行的时候再去执行我们传递的对应回调。

// 其他代码省略...
constructor(executor) {
    // 其他代码省略...
    // 新增
    this.fulfilesCallbacks = [];
    this.rejectCallback = [];
    // 修改
    let resolve = (value) => {
                        if(this.status === PENDING) {
                            this.status =FULFILED;
                            this.value = value;
                            this.fulfilesCallbacks.forEach(callback => callback());
                        }
                    }

    let reject = (reason) => {
        if(this.status === PENDING) {
            this.status =REJECT;
            this.error = reason;
            this.rejectCallback.forEach(callback => callback());
        }
  }
}
then = (onfulfiled,onrejected) => {
    if(this.status === FULFILED) {
        onfulfiled(this.value);
    }
    if(this.status === REJECT) {
        onrejected(this.error);
    }
    if(this.status === PENDING) {
        this.fulfilesCallbacks.push(() => {
            onfulfiled(this.value);
        });
        this.rejectCallback.push(() => {
            onfulfiled(this.error);
        });
    }
}


// 修改后再执行
const p = new MyPromise((resolve, reject) => {
    // 我们把resolve函数放在一个setTimeout中执行
    setTimeout(() => {
        resolve(1)
    },1000)

})
p.then (res => {
    console.log(1)
},err => {
    console.log(err)
})

这时就可以发现,1秒后我们打印出了1,bingo!

如果就刚刚的代码中,我们直接使用链式调用,会发生什么?我们来试试

p.then (res => {
    console.log(1)
},err => {
    console.log(err)
}).then (res => {
    console.log(1)
},err => {
    console.log(err)
})

这时候我们有两个then进行链式调用,这就是我们期望把回调地狱改成的模样。打开浏览器的控制台:

test.html:385 Uncaught TypeError: Cannot read properties of undefined (reading 'then')at test.html:385:3(anonymous) @ test.html:385
test.html:382 1

报错了。这并不奇怪,因为我们第一个then执行完后什么都没返回,所以再用then方法就会报错。如果想要promise无限支持then的链式调用,那我们需要分析一下它应该具备什么属性。

  • 它得有一个then方法
  • 它得和第一个then拥有一样的功能

等等,这不就是说前一个then方法要返回一个promise吗?😏😏

为了达成链式,我们默认在第一个then里返回一个promise。规定了一种方法,就是在then里面返回一个新的promise,称为promise2:promise2 = new Promise((resolve, reject)=>{})

  • 将这个promise2返回的值传递到下一个then中;
  • 如果返回一个普通的值,则将普通的值传递给下一个then中;

当我们在第一个then中return了一个参数(参数未知,需判断)。这个return出来的新的promise就是onFulfilled()或onRejected()的值;

有了这个方法我们就继续完成我们的代码:

// 省略其他代码...
    then (onfulfiled, onrejected)  {
        // 想要实现then的链式调用,并且可以多次then调用
        // 思路: then函数就要继续返回Promise
        /**
         * 首先,要看x是不是promise。
         *如果是promise,则取它的结果,作为新的promise2成功的结果
         *如果是普通值,直接作为promise2成功的结果
         *所以要比较x和promise2
        */
        let promise2 = new MyPromise((resolve, reject) => {
            if (this.status === FULFILED) {
                setTimeout(() => {
                    let x = onfulfiled(this.value);
                    MyPromise.resolvePromise(promise2, x, resolve, reject);
                })
            }
            if (this.status === REJECT) {
                setTimeout(() => {
                    let x = onrejected(this.error);
                    MyPromise.resolvePromise(promise2, x, resolve, reject);
                })
            }
            if (this.status === PENDING) {
                this.fulfilesCallbacks.push(() => {
                    setTimeout(() => {
                        let x = onfulfiled(this.value);
                        MyPromise.resolvePromise(promise2, x, resolve, reject);
                    })
                });
                this.rejectCallback.push(() => {
                    setTimeout(() => {
                        let x = onrejected(this.error);
                        MyPromise.resolvePromise(promise2, x, resolve, reject);
                    })
                });
            }
        })
        return promise2;
   }
 static resolvePromise  (promise2, x, resolve, reject)  {
    if (promise2 === x) return new Error("不要返回promise本身,这样会造成重复调用的死循环");
    let called = false;
    if (x !== null && (typeof x === "object" || typeof x === "function")) {
        try {
            let then = x.then;
            if (typeof then === "function") {
                then.call(x, y => {
                    if (called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                },
                    error => {
                        if (called) return;
                        called = true;
                        reject(error); // 如果失败了就直接抛出错误
                    }
                )
            } else {
                // 如果返回值是一个值,就直接传递给下一个promise
                resolve(x)
            }
        } catch (error) {
            if (called) return;
            called = true;
            reject(error); // 如果失败了就直接抛出错误
        }

    } else {
        resolve(x)
    }
}

这段代码中,我们封装了一个resolvePromise静态方法,这个方法接收四个参数,分别是要返回的promise2,一个return的值x,以及一个resolve和reject,这个方法主要是为了做以下几件事:

  • 如果promise2和x相同,直接return,并抛出错误,因为这样会造成死循环;
  • x 不能是null
  • x 是普通值 直接resolve(x)
  • x 是对象或者函数(默认promise),let then = x.then 声明了then
  • 如果取then报错,则走reject()
  • 如果then是个函数,则用call执行then,第一个参数是this,后面是成功的回调和失败的回调
  • 如果成功的回调还是pormise,就递归继续解析
  • 成功和失败只能调用一个 所以设定一个called来防止多次调用

最后把promise2作为then的返回值返回回去,这样,then执行后我们得到的依然是一个promise,我们去控制台打印看看:

const p1 = p.then (res => {
    console.log(1)
},err => {
    console.log(err)
})
console.log(p1)

// 控制台输出
MyPromise {status: 'pending', value: null, error: null, fulfilesCallbacks: Array(0), rejectCallback: Array(0)}
    error: null
    fulfilesCallbacks: []
    rejectCallback: []
    status: "fulfiled"
    value: undefined
    [[Prototype]]: Object

bingo!到目前为止我们的promise基本上就算开发完成了,现在我们再来测试一下:

p.then (res => {
    console.log(1)
},err => {
    console.log(err)
}).then (res => {
    console.log(2)
},err => {
    console.log(err)
})

// 控制台输出
1
2

果然不出所料,浏览器打印出了1和2,现在我们的链式调用也完成了。

4. 完整代码

接下来我们把resolve,reject,all,race方法也实现一下,这个MyPromise类就算开发完成了(我直接把完整代码贴出来,这几个方法我作为MyPromise的静态方法)

class MyPromise {
    constructor(executor) {
        this.status = PENDING;
        this.value = null;
        this.error = null;
        this.fulfilesCallbacks = [];
        this.rejectCallback = [];
        let resolve = (value) => {
            if (this.status === PENDING) {
                this.status = FULFILED;
                this.value = value;
                this.fulfilesCallbacks.forEach(callback => callback(this.value));
            }
        }

        let reject = (reason) => {
            if (this.status === PENDING) {
                this.status = REJECT;
                this.error = reason;
                this.rejectCallback.forEach(callback => callback(this.error));
            }
        }

        try {
            if (!executor) {
                reject(new Error('必须传递一个函数'));
            } else {
                executor(resolve, reject);
            }
        } catch (e) {
            reject(e);
        }
    }
    then (onfulfiled, onrejected)  {
        // 想要实现then的链式调用,并且可以多次then调用
        // 思路: then函数就要继续返回Promise
        /**
         * 首先,要看x是不是promise。
         *如果是promise,则取它的结果,作为新的promise2成功的结果
         *如果是普通值,直接作为promise2成功的结果
         *所以要比较x和promise2
        */
        let promise2 = new MyPromise((resolve, reject) => {
            if (this.status === FULFILED) {
                setTimeout(() => {
                    let x = onfulfiled(this.value);
                    MyPromise.resolvePromise(promise2, x, resolve, reject);
                })
            }
            if (this.status === REJECT) {
                setTimeout(() => {
                    let x = onrejected(this.error);
                    MyPromise.resolvePromise(promise2, x, resolve, reject);
                })
            }
            if (this.status === PENDING) {
                this.fulfilesCallbacks.push(() => {
                    setTimeout(() => {
                        let x = onfulfiled(this.value);
                        MyPromise.resolvePromise(promise2, x, resolve, reject);
                    })
                });
                this.rejectCallback.push(() => {
                    setTimeout(() => {
                        let x = onrejected(this.error);
                        MyPromise.resolvePromise(promise2, x, resolve, reject);
                    })
                });
            }
        })
        return promise2;
    }
    catch (fn)  {
        return this.then(null, fn)
    }
    static resolve  (val)  {
        return new MyPromise((resolve, reject) => {
            resolve(val);
        })
    }
    static reject  (error)  {
        return new MyPromise((resolve, reject) => {
            reject(error); // 如果失败了就直接抛出错误
        })
    }
    static race  (promiseArr)  {
        return new MyPromise((resolve, reject) => {
            for (let i = 0; i < promiseArr.length; i++) {
                promiseArr[i].then(resolve, reject);
            }
        })
    }
    static all  (promiseArr)  {
        let index = 0;
        let resultList = [];
        return new MyPromise((resolve, reject) => {
            for (let i = 0; i < promiseArr.length; i++) {
                promiseArr[i].then(data => {
                    resultList[index](data);
                    index++;
                    if (index === promiseArr.length) {
                        resolve(resultList)
                    }
                }, reject).catch(err => {
                    reject(err);
                });

            }
        })
    }
    static resolvePromise  (promise2, x, resolve, reject)  {
        if (promise2 === x) return new Error("不要返回promise本身,这样会造成重复调用的死循环");
        let called = false;
        if (x !== null && (typeof x === "object" || typeof x === "function")) {
            try {
                let then = x.then;
                if (typeof then === "function") {
                    then.call(x, y => {
                        if (called) return;
                        called = true;
                        resolvePromise(promise2, y, resolve, reject);
                    },
                        error => {
                            if (called) return;
                            called = true;
                            reject(error); // 如果失败了就直接抛出错误
                        }
                    )
                } else {
                    // 如果返回值是一个值,就直接传递给下一个promise
                    resolve(x)
                }
            } catch (error) {
                if (called) return;
                called = true;
                reject(error); // 如果失败了就直接抛出错误
            }

        } else {
            resolve(x)
        }
    }
}

其实在开发Promise的时候,最难的点我认为是then的链式调用,这里需要搞清楚then的返回类型,以及如何做到链式调用。