美文网首页
[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

    目前为止,我在项目中大量使用到了ES6的Promise,最常见的场景有是发送一个网络请求,在成功得到响应之后渲染对...

  • 因为我别无选择

    Do best yourself,trust yourself,just do it ! 不是因为我坚强而坚持,而...

  • 亲子打卡日记Dec.11th,rainy

    Whatever you do, remember that you do it for yourself...

  • 英文摘抄(五十八)

    Promise yourself to be strong that nothing can disturb yo...

  • DAY023

    Do not impose on others what you yourself do not desire.—...

  • DAY 022

    Do not impose on others what you yourself do not desire.—...

  • For yourself

    Whatever you do ,remember that you do it for yourself ,th...

  • April 14: 去知道自己

    Whatever you do, know yourself and do it.Knowyour action ...

  • 日更文

    To their own love, love yourself. You just do yourself, U...

  • Do yourself

    与其去羡慕着他人的成功,还不如走着自己的路,跟随着自己的心去拼搏、去奋斗 做自己……就好

网友评论

      本文标题:[Do it Yourself]Promise

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