美文网首页
[Do it Yourself]Promise

[Do it Yourself]Promise

作者: 小丸子啦啦啦呀 | 来源:发表于2022-03-01 15:16 被阅读0次

    目前为止,我在项目中大量使用到了ES6的Promise,最常见的场景有是发送一个网络请求,在成功得到响应之后渲染对应页面或者元素,失败之后提示错误。

    http.get(url).then(res => { render(res) }, err => { showError(err) })
    

    以上代码中http.get(url)返回的就是一个Promise,在promise上调用then方法,它接收两个参数,第一个参数代表请求成功之后的回调函数,第二个参数代表请求失败时的回调函数。

    Before Promise

    在ES6 Promise没有诞生之前,我曾在古老的JSP项目中使用过原生的XHR,也使用过jQuery的$.ajax用来实现和上边同样的事儿。当场景足够简单时,看不出Promise的优越性在哪儿。但是每当遇到多个请求互相依赖,需要嵌套,或者次序调用时,都会发现代码非常难看,也很难捕获和处理异常。更致命的问题是,我们将回调函数的执行控制权交给了发起异步的函数,而发起异步的函数可能完全不受我们自己控制,所以没有办法保证对应的回调函数会被正确执行,进而导致bug。

    Promise Introduction

    而ES6 Promise的出现,解决了以上问题。Promise作为一个第三方担保者的角色出现,它承诺了失败或者成功时一定会执行对应的函数。它具有三种状态: Pending,Fullfilled,Rejected,同一时刻只可能处在一种状态之下;状态转化方向是Pending->Fullfilled和Pending->Reject两种,当Pending->Fullfilled发生时,保证onFullfilled回调函数(then方法的第一个参数)被执行,当Pending->Rejected发生时,保证onRejected回调函数(then方法的第二个参数)被执行。更厉害的是,它还支持链式调用和值穿透,then函数执行后继续返回一个Promise, 包装了上一个Promise的执行结果,这就可以解决上述多个异步动作互相依赖,嵌套,次序调用的问题。

    以上我描述的特性并不完整,Promise有多种规范,其中ES6 Promise遵循的是Promise A+规范。接下来,回到本文主题,分两步来介绍如何DIY Promise.

    实现简单版Promise

    第一步,我们先抛开A+规范,实现一个最简单的Promise。
    首先来设计test case:

      it("resolve", () => {
          new MyPromise((resolve) => {
            resolve(1);
          }).then((data) => {
            expect(data).toEqual(1);
          });
        });
    
        it("reject", () => {
          const onFullFilled = jest.fn();
    
          new MyPromise((resolve, reject) => {
            reject(0);
            resolve(1);
          }).then(onFullFilled, (data) => {
            expect(data).toEqual(0);
          });
          expect(onFullFilled).not.toHaveBeenCalled();
        });
    

    我设计了两个case, 分别针对resolve和reject的情况。其中case reject还需要测试在reject之后,onRejected毁掉是否还会被执行。

    直接先来看第一个版本的代码吧:

    export const PENDING = "PENDING";
    export const FULFILLED = "FULFILLED";
    export const REJECTED = "REJECTED";
    class MyPromise{
      private status= PENDING;
      private value;
      private reason;
    
      constructor(excutor){
        try{
          excutor(this.resolve, this.reject)
        }catch(error){
          this.reject(error)
        }
      }
      
      private resolve(value){
         if(status !== PENDING)return;
         this.value = value;
         this.status = FULFILLED;
      }
    
      private reject(reason){
        if(status !== PENDING)return;
        this.reason = reason;
        this.status = REJECTED;
      }
    
      public then(onFullfilled, onRejected){
          if(this.status === FULFILLED){
            onFullfilled(this.value)
          }
          if(this.status === REJECTED){
            onRejected(this.reason)
          }
      }
    }
    

    以上最简版已经初步刻画了Promise的核心功能。但是,有一种case没有覆盖到:

        it("simple resolve", () => {
          const promise = new MyPromise((resolve) => {
            setTimeout(() => {
              resolve(1);
            }, 0);
          });
    
          expect(promise).resolves.toBe(1);
        });
    

    如果Promise体中含有异步代码,而resove, reject的执行正好在异步代码中会出现什么情况呢? 很显然then会先执行,然而执行then的时候异步代码块中的resolve/reject还未被执行,此时Promise的状态还是PENDING,进而导致onFullfilled/onRejected也不会被执行。

    为了解决这个问题,我们需要使用两个数组分别存储onFullfilleds和onRejecteds, 当遇执行then时如果还处在PENDING的状态,那么先把他们存在数组中,当resolve或reject触发的时候,依次执行他们。

    export const PENDING = "PENDING";
    export const FULFILLED = "FULFILLED";
    export const REJECTED = "REJECTED";
    class MyPromise{
      private status= PENDING;
      private value;
      private reason;
      private onResolvedCallbacks = [];
      private onRejectedCallbacks = [];
    
      constructor(excutor){
        try{
          excutor(this.resolve, this.reject)
        }catch(error){
          this.reject(error)
        }
      }
      
      private resolve(value){
         if(status !== PENDING)return;
         this.value = value;
         this.status = FULFILLED;
         this.onResolvedCallbacks.foreach(cb => cb(this.value))
      }
    
      private reject(reason){
        if(status !== PENDING)return;
        this.reason = reason;
        this.status = REJECTED;
        this.onRejectedCallbacks.foreach(cb => cb(this.reason))
      }
    
      public then(onFullfilled, onRejected){
          if(this.status === FULFILLED){
            onFullfilled(this.value)
          }
          if(this.status === REJECTED){
            onRejected(this.reason)
          }
          if(this.status === PENDING){
            this.onResolvedCallbacks.push(onFullfilled);
            this.onRejectedCallbacks.push(onRejected)
          }
      }
    }
    

    一开始,我针对这段代码有两个疑问:

    1. 为什么要是用数组存回调?以下case可以解答:
       it("multiple then", () => {
          let output = [];
          const promise = new MyPromise((resolve) => {
            setTimeout(() => {
              resolve(1);
            }, 0);
          });
          // 可能会在同一个promise上多次调用then
          promise.then((data) => output.push(data + 1));
          promise.then((data) => output.push(data + 2));
          promise.then((data) => output.push(data + 3));
    
          new MyPromise((resolve) => {
            resolve("a");
          }).then(() => {
            expect(output).toEqual([2, 3, 4]);
        });
    
    1. 这样写之后会不会造成非异步的情况下onFullfilled/onReject被提前执行?
      答案是不会,因为只有在异步的情况下,才会出现执行then的时候状态还是PENDING。

    至此为止我们已经完成了一个简版的Promise。

    实现符合A+规范的Promise

    第二步,我们在上面的基础上,实现符合A+规范的Promise。
    同样,先来设计test cases:

    describe("chain", () => {
        it("should throw cycle error", () => {
          const promise = new MyPromise((resolve) => {
            resolve(1);
          }).then(
            (data) => {
              return promise;
            },
            (error) => {
              expect(error).toEqual(
                new TypeError("Chaining cycle detected for promise #<Promise>")
              );
            }
          );
        });
    
        it("if x is simple value", () => {
          new MyPromise((resolve) => {
            resolve(1);
          })
            .then(() => {
              return 2;
            })
            .then()
            .then((data) => {
              expect(data).toEqual(2);
            });
        });
    
        it("if x is a thenable but then is a simple value", () => {
          const p = new MyPromise((resolve) => {
            resolve(1);
          }).then(() => {
            return { then: 2 };
          });
    
          expect(p).resolves.toEqual({ then: 2 });
        });
    
        it("if x is a real promise and it resolved", () => {
          new MyPromise((resolve) => {
            resolve(1);
          })
            .then(() => {
              return new MyPromise((resolve) => {
                resolve(2);
              });
            })
            .then((data) => {
              expect(data).toBe(2);
            });
        });
    
        it("if x is a real promise and it rejected", () => {
          new MyPromise((resolve) => {
            resolve(1);
          })
            .then(() => {
              return new MyPromise((resolve, reject) => {
                reject(2);
              });
            })
            .then(null, (data) => {
              expect(data).toBe(2);
            });
        });
    
        it("if x is a nested promise", () => {
          new MyPromise((resolve) => {
            resolve(1);
          })
            .then(() => {
              return new MyPromise((resolve) => {
                resolve(2);
              }).then(() => {
                return new MyPromise((resolve) => resolve(3));
              });
            })
            .then((data) => {
              expect(data).toBe(3);
            });
        });
    
        it("if x is promise, resolve and reject should be mutex", () => {
          const onFullFilled = jest.fn();
          new MyPromise((resolve, reject) => {
            resolve(1);
          })
            .then((data) => {
              return {
                then: (resolve, reject) => {
                  reject(3);
                  resolve(2);
                }
              };
            })
            .then(onFullFilled, (data) => {
              expect(data).tobe(3);
            });
          expect(onFullFilled).not.toHaveBeenCalled();
        });
    
        it("if reject in nested promise x", () => {
          new MyPromise((resolve, reject) => {
            resolve(1);
          })
            .then(() => {
              return new MyPromise((resolve, reject) => {
                reject(2);
              }).then(null, () => {
                return new MyPromise((resolve, reject) => {
                  reject(3);
                });
              });
            })
            .then(
              () => {},
              (data) => {
                expect(data).toEqual(3);
              }
            );
        });
      });
    

    然后再来看代码实现:

    export const PENDING = "PENDING";
    export const FULFILLED = "FULFILLED";
    export const REJECTED = "REJECTED";
    
    export class MyPromise {
      constructor(executor) {
        try{
          excutor(this.resolve, this.reject)
        }catch(error){
          this.reject(error)
        }
      }
    
      private resolve(value){
         if(status !== PENDING)return;
         this.value = value;
         this.status = FULFILLED;
         this.onResolvedCallbacks.foreach(cb => cb(this.value))
      }
    
      private reject(reason){
        if(status !== PENDING)return;
        this.reason = reason;
        this.status = REJECTED;
        this.onRejectedCallbacks.foreach(cb => cb(this.reason))
      }
    
     private resolvePromise(promise2, x, resolve, reject) {
        // 不能自己等待自己执行完
        if (promise2 === x) {
          return reject(
            new TypeError("Chaining cycle detected for promise #<Promise>")
          );
        }
    
        // 如果x是一个promise, 用来标记它是否执行完resolve或者reject
       // 其实不做标记也不会发生Bug, 只是会产生没必要的调用
        let called;
    
        if ((typeof x === "object" && x !== null) || typeof x === "function") {
          const then = x.then;
    
          if (typeof then === "function") {
            try {
              then.call(
                x,
                (resolvedData) => {
                  if (called) return;
                  this.resolvePromise(promise2, resolvedData, resolve, reject);
                  called = true;
                },
                (rejectedData) => {
                  if (called) return;
                  reject(rejectedData);
                  called = true;
                }
              );
            } catch (error) {
              if (called) return;
              reject(error);
              called = true;
            }
          } else {
            resolve(x);
          }
        } else {
          resolve(x);
        }
      }
    
      // 包含一个 then 方法,并接收两个参数 onFulfilled、onRejected
      public then(onFulfilled, onRejected) {
        if (typeof onFulfilled !== "function") {
          onFulfilled = (v) => v;
        }
        if (typeof onRejected !== "function") {
          onRejected = (err) => {
            throw Error(err);
          };
        }
    
        const promise2 = new MyPromise((resolve, reject) => {
          if (this.status === FULFILLED) {
            try {
              const x = onFulfilled(this.value);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }
    
          if (this.status === REJECTED) {
            try {
              const x = onRejected(this.reason);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }
    
          if (this.status === PENDING) {
            this.onResolvedCallbacks.push(onFulfilled);
            this.onRejectedCallbacks.push(onRejected);
          }
        });
    
        return promise2;
      }
    }
    

    总结

    我在自己尝试实现Promise的时候,没有搞清楚ES6 Promise和Promise A+是有区别的,ES6 Promise在其之上扩展了一些功能,比如Promise.resolve, Promise.reject, Promise.all, Promise.race。还有一个我纠结了很久的区别,那就是ES6 Promise只要不调用then,那么promise的状态始终是pending, 但是Promise A+不管then有没有被调用,只要resolve/reject被执行了,状态都会变化。

    在我自己尝试实现的时候,我尝试着按照ES6 Promise去实现,但是发现不知如何做,于是我才发现有个Promise A+规范,然后按照文章 中的解析,再尝试自己构建测试用例然后改写出来的。

    这启发我在学习一个东西的原理的时候,应该先有一个大局观,而不是一开始就拘泥于细节。

    同时,这次我尝试应用“测试驱动开发”的思想,整体做下来感觉到思路清晰,效率更高。

    参考链接

    1. 面试官:“你能手写一个 Promise 吗”
    2. promise A+

    相关文章

      网友评论

          本文标题:[Do it Yourself]Promise

          本文链接:https://www.haomeiwen.com/subject/wuddrrtx.html