Skip to content

从零开始,一步一步实现一个完整的Promise #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
jappp opened this issue Mar 5, 2021 · 0 comments
Open

从零开始,一步一步实现一个完整的Promise #10

jappp opened this issue Mar 5, 2021 · 0 comments

Comments

@jappp
Copy link
Owner

jappp commented Mar 5, 2021

什么是Promise?

Promise 是异步编程的一种解决方案,ES6将其写进了语言标准,原生提供了Promise对象。
Promise主要解决了什么问题?

  • 消灭嵌套调用:通过 Promise 的链式调用可以解决回调地狱;
  • 合并多个任务的请求结果:使用 Promise.all 获取合并多个任务的错误处理

从零开始手写一个Promise

手写一个Promise需要遵循 Promise/A+规范

首先,我们从Promise最基础的概念开始:

  1. Promise有三种状态,pendingfulfilledrejected,状态只能从pendingfulfilledrejected,一旦确定无法改变;
  2. Promise有一个value保存成功状态的值,一个reason保存失败状态的值;
  3. new Promise的时候传入一个 executor立即执行函数,函数接收resolvereject两个函数参数. 若是executor函数报错 直接执行reject()
  4. resolve()将Promise状态变为fulfilledreject()将Promise状态变为rejected,并执行then()传入的回调函数;
  5. Promise有一个then函数, 依次传入onFulfilledonRejected回调函数,
    当status为fulfilled时执行onFulfilled回调函数,传入this.value。当status为rejected时执行onRejected函数,传入this.reason;

按照上面的概念,我们可以写出一个最基本的Promise

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class MyPromise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;

    // 用箭头函数确保this指向当前实例对象
    let resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
      }
    }
    let reject = (value) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = value;
      }
    }

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value);
    }
    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
  }
}

写完代码测试一下

const promise = new MyPromise((resolve, reject) => {
  resolve('成功');
}).then(
  (data) => {
    console.log('success', data)
  },
  (err) => {
    console.log('failed', err)
  }
)

浏览器打印出'success 成功'

但这时候只是同步操作的Promise,若是我们在executor()中传入一个异步操作,则会什么都没有打印出来。

const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('成功');
  })
}).then(
  (data) => {
    console.log('success', data)
  },
  (err) => {
    console.log('failed', err)
  }
)

这是因为执行then方法时,此时Promise的状态是pending,不会触发回调。那么在pending状态的时候应该怎么做呢?

不考虑异常的情况下,Promise的状态改变只依赖于构造函数中的resolve()函数和reject函数,那么可以考虑将onFulfilled和onRejected回调函数先保存到Promise中我们定义的属性onFulfilledFn和onRejectedFn中,等到状态改变时再调用。

这其实就是一个发布订阅模式,这种收集依赖 -> 触发通知 -> 取出依赖执行的方式,被广泛运用于发布订阅模式的实现
在Promise里,then收集依赖-> 异步触发resolve -> resolve执行依赖。

结合以上思路,优化一下代码:

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class MyPromise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledFn = [];
    this.onRejectedFn = [];

    let resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        this.onFulfilledFn.forEach(fn => fn());
      }
    }
    let reject = (value) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = value;
        this.onRejectedFn.forEach(fn => fn());
      }
    }

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value);
    }
    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
    if (this.status === PENDING) {
      this.onFulfilledFn.push(() => onFulfilled(this.value));
      this.onRejectedFn.push(() => onRejected(this.reason));
    }
  }
}

此时测试一下,会发现浏览器延迟1s打印出‘success 成功’

const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('成功');
  },1000);
}).then(
  (data) => {
    console.log('success', data)
  },
  (err) => {
    console.log('failed', err)
  }
)

then的链式调用

这是Promise实现的重点和难点,我们先来看一下JavaScript中原生Promise对象的then是如何链式调用的

const p1 = new Promise((resolve, reject) => {
  resolve(1)
})

p1.then(res => {
    console.log(res)
    // then回调中可以return一个Promise
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(2)
      }, 1000);
    })
  })
  .then(res => {
    console.log(res)
    // then回调中也可以return一个值
    return 3
  })
  .then(res => {
    console.log(res)
  })

浏览器依次输出
1
2
3

这种链式调用该如何实现?

  1. 为了支持多次调用,then方法需要返回一个新的Promise,规范里称为Promise2。
  2. then的回调需要顺序执行,虽然上面的代码return了一个Promise,但执行顺序仍保证是1->2->3。因此,如果then返回值是一个Promise实例的话,我们要等待Promise状态变更后,才执行下一个then方法收集的回调,这就要求我们对then返回值x进行分类讨论。
  then(onFulfilled, onRejected) {
    let promise2 =  new Promise((resolve, reject) => {
      ...
      if (this.status === PENDING) {
        this.onFulfilledFn.push(() => {
          try {
            // 执行第一个(当前)Promise的成功回调,并获取返回值
            let x = onFulfilled(this.value);
            // 分类讨论返回值,如果是Promise实例,那么等待Promise的状态变更后resolve,否则直接resolve
            x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
          } catch (error) {
            reject(error);
          }
        });
        this.onRejectedFn.push(() => {
          try {
            // 执行第一个(当前)Promise的失败回调,并获取返回值
            let x = onRejected(this.reason);
            // 分类讨论返回值,如果是Promise实例,那么等待Promise的状态变更,否则直接resolve
            x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
          } catch (error) {
            reject(error);
          }
        });
      }
    })

测试一下

const p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 500);
})

p1.then(res => {
    console.log(res)
    // then回调中可以return一个Promise
    return new MyPromise((resolve, reject) => {
      setTimeout(() => {
        resolve(2)
      }, 1000);
    })
  })
  .then(res => {
    console.log(res)
    // then回调中也可以return一个值
    return 3
  })
  .then(res => {
    console.log(res)
  })

//输出 1 2 3

这只是最简单的x值判断,只是单纯判断x是否Promise实例,实际上还需要判断更多复杂情况。所以我们可以优化一下,将判断x的函数命名为resolvePromise提取出来。
resolvePromise的参数有promise2(默认返回的promise)、x(我们自己return的对象)、resolve、reject。
代码如下:

function resolvePromise(promise2, x, resolve, reject) {
  // 循环引用,自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise
  if (promise2 === x) {
    return reject(new TypeError("Channing cycle detected for promise"));
  }

  // 防止多次调用
  let called;
  // x为对象或者函数
  if ((typeof x === "object" && x !== null) || typeof x === "function") {
    try {
      // 为了判断 resolve 过的就不用再 reject 了(比如 reject 和 resolve 同时调用的时候)
      let then = x.then;
      if (typeof then === "function") {
        // 让then执行,第一个参数是this,后面是成功的回调 和 失败的回调
        then.call(
          x,
          (res) => {
            // 成功和失败只能调用一个
            if (called) return;
            called = true;
            // 递归解析的过程(因为可能 promise 中还有 promise)
            resolvePromise(promise2, res, resolve, reject);
          },
          (err) => {
            // 成功和失败只能调用一个
            if (called) return;
            called = true;
            reject(err); // 失败了就失败了
          }
        );
      } else {
        resolve(x);
      }
    } catch (error) {
      if (called) return;
      called = true;
      reject(error);
    }
  } else {
    // 普通值直接resolve
    resolve(x);
  }
}

then(onFulfilled, onRejected) {
  let promise2 =  new MyPromise((resolve, reject) => {
    if (this.status === FULFILLED) {
      try {
        // 执行第一个(当前)Promise的成功回调,并获取返回值
        let x = onFulfilled(this.value);
        // resolvePromise函数,处理自己return的promise和默认的promise2的关系
        resolvePromise(promise2, x, resolve, reject);
      } catch (error) {
        reject(error);
      }
    }
    if (this.status === REJECTED) {
      try {
        // 执行第一个(当前)Promise的失败回调,并获取返回值
        let x = onRejected(this.reason);
        // resolvePromise函数,处理自己return的promise和默认的promise2的关系
        resolvePromise(promise2, x, resolve, reject);
      } catch (error) {
        reject(error);
      }
    }
    if (this.status === PENDING) {
      this.onFulfilledFn.push(() => {
        try {
          // 执行第一个(当前)Promise的成功回调,并获取返回值
          let x = onFulfilled(this.value);
          // resolvePromise函数,处理自己return的promise和默认的promise2的关系
          resolvePromise(promise2, x, resolve, reject);
        } catch (error) {
          reject(error);
        }
      });
      this.onRejectedFn.push(() => {
        try {
          // 执行第一个(当前)Promise的失败回调,并获取返回值
          let x = onRejected(this.reason);
          // resolvePromise函数,处理自己return的promise和默认的promise2的关系
          resolvePromise(promise2, x, resolve, reject);
        } catch (error) {
          reject(error);
        }
      });
    }
  })

  return promise2;
}

then的其它细节处理

  1. 「值穿透」:根据规范,如果 then() 接收的参数不是function,将其忽略,且依旧可以在下面的 then 中获取到之前返回的值。
  2. 「异步模拟」:规定onFulfilled或onRejected不能同步被调用,必须异步调用。我们就用setTimeout解决异步问题。

这样,Promise的主要部分就完成了,完整代码如下:

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function resolvePromise(promise2, x, resolve, reject) {
  // 循环引用,自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise
  if (promise2 === x) {
    return reject(new TypeError("Channing cycle detected for promise"));
  }

  // 防止多次调用
  let called;
  // x为对象或者函数
  if ((typeof x === "object" && x !== null) || typeof x === "function") {
    try {
      // 为了判断 resolve 过的就不用再 reject 了(比如 reject 和 resolve 同时调用的时候)
      let then = x.then;
      if (typeof then === "function") {
        // 让then执行,第一个参数是this,后面是成功的回调 和 失败的回调
        then.call(
          x,
          (res) => {
            // 成功和失败只能调用一个
            if (called) return;
            called = true;
            // 递归解析的过程(因为可能 promise 中还有 promise)
            resolvePromise(promise2, res, resolve, reject);
          },
          (err) => {
            // 成功和失败只能调用一个
            if (called) return;
            called = true;
            reject(err); // 失败了就失败了
          }
        );
      } else {
        resolve(x);
      }
    } catch (error) {
      if (called) return;
      called = true;
      reject(error);
    }
  } else {
    // 普通值直接resolve
    resolve(x);
  }
}

class MyPromise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledFn = [];
    this.onRejectedFn = [];

    let resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        this.onFulfilledFn.forEach((fn) => fn());
      }
    };
    let reject = (value) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = value;
        this.onRejectedFn.forEach((fn) => fn());
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    // 解决值穿透
    // onFulfilled如果不是函数,就忽略onFulfilled,直接返回value
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (val) => val;
    // onRejected如果不是函数,就忽略onRejected,直接扔出错误
    onRejected = typeof onRejected === "function" ? onRejected : (err) => { throw err; };

    let promise2 = new MyPromise((resolve, reject) => {
      if (this.status === FULFILLED) {
       // 模拟异步
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      }
      if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }
      if (this.status === PENDING) {
        this.onFulfilledFn.push(() => {
          setTimeout(() => {
            try {
              // 执行第一个(当前)Promise的成功回调,并获取返回值
              let x = onFulfilled(this.value);
              // resolvePromise函数,处理自己return的promise和默认的promise2的关系
              resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0)
        });
        this.onRejectedFn.push(() => {
          setTimeout(() => {
            try {
              // 执行第一个(当前)Promise的失败回调,并获取返回值
              let x = onRejected(this.reason);
              // resolvePromise函数,处理自己return的promise和默认的promise2的关系
              resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0)
        });
      }
    });

    return promise2;
  }
}

写完代码测试一下

首先在Promise实现的代码中加入

MyPromise.defer = MyPromise.deferred = function() {
  var result = {};
  result.promise = new MyPromise(function(resolve, reject){
    result.resolve = resolve;
    result.reject = reject;
  });

  return result;
}

安装测试脚本

yarn add promises-aplus-tests --dev

在package.json中配置指令

"test": "promises-aplus-tests js/promise/promise"
npm run test

promises-aplus-tests 中共有 872 条测试用例,代码完美通过
image

Promise 的 API

原生的Promise还提供了一些方法,我们顺便一起写出来

  • Promise.resolve()
  • Promise.reject()
  • Promise.prototype.catch()
  • Promise.prototype.finally()
  • Promise.all()
  • Promise.race()
  • Promise.allSettled()
  • Promise.any()

Promise.resolve()

  static resolve(value) {
    // 根据规范, 如果参数是Promise实例, 直接return这个实例
    if(value instanceof MyPromise) return value;
    return new MyPromise((resolve, reject) => {
      resolve(value);
    })
  }

Promise.reject()

  static reject(reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason);
    })
  }

Promise.prototype.catch()

MyPromise.prototype.catch = function(onRejected){
  return this.then(null, onRejected);
}

Promise.prototype.finally()

  1. .finally()方法不管Promise对象最后的状态如何都会执行传入的回调函数
  2. .finally()方法的回调函数不接受任何的参数
  3. 它最终返回的默认会是一个上一次的Promise对象值,如果返回的是成功的 Promise,会采用上一次的结果;如果返回的是失败的 Promise,会用这个失败的结果,传到 catch 中
MyPromise.prototype.finally = function(callback){
  return this.then((value) => {
    return MyPromise.resolve(callback()).then(() => value);
  }, (reason) => {
    return MyPromise.resolve(callback()).then(() => { throw reason })
  })
}

Promise.all()

Promise.all 是解决并发问题的,多个异步并发获取最终的结果(如果有一个失败则失败)

  1. 当所有结果成功返回时,按照请求顺序返回成功。当其中有一个失败方法时,则进入失败方法。
  2. catch()函数能够捕获到.all()里最先的那个异常,注意只会捕获最先异常的那次,其余不会捕获。
  static all(promiseArr) {
    if (!Array.isArray(promiseArr)) {
      const type = typeof promiseArr;
      return new TypeError(`TypeError: ${type} ${values} is not iterable`)
    }

    return new MyPromise((resolve, reject) => {
      let counts = 0;
      let result = [];
      let len = promiseArr.length;

      for (let i = 0; i < len; i++) {
        let instance = promiseArr[i];

        // Promise.resolve(p)用于处理传入值不为Promise的情况
        MyPromise.resolve(instance).then(val => {
          counts++;
          result[i] = val;
          if (counts === len) {
            resolve(result);
          }
        }, err => {
          reject(err);
        })
      }
    })
  }

Promise.race()

只需要一个Promise对象状态变为resolved或rejected时就调用then方法,它只会获取最先执行完成的那个结果。

  static race(promiseArr) {
    return new MyPromise((resolve, reject) => {
      for (let i = 0; i < promiseArr.length; i++) {
        let instance = promiseArr[i];
        Promise.resolve(instance).then(resolve, reject)
      }
    })
  }

Promise.allsettled()

allsettled()接受一组 Promise 实例作为参数,但和和.all()方法不同,只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。

  static allSettled(promiseArr) {
    if (!Array.isArray(promiseArr)) {
      const type = typeof promiseArr;
      return new TypeError(`TypeError: ${type} ${values} is not iterable`)
    }

    return new MyPromise((resolve, reject) => {
      let counts = 0;
      let result = [];
      let len = promiseArr.length;

      for (let i = 0; i < len; i++) {
        let instance = promiseArr[i];

        // Promise.resolve(p)用于处理传入值不为Promise的情况
        MyPromise.resolve(instance).then(res => {
          counts++;
          result.push({ status: "fulfilled", value: res });
          if (counts === len) {
            resolve(result);
          }
        }, err => {
          counts++;
          result.push({ status: "rejected", value: err });
          if (counts === len) {
            resolve(result);
          }
        })
      }
    })
  }

Promise.any()

ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

  static any(promiseArr) {
    if (!Array.isArray(promiseArr)) {
      const type = typeof promiseArr;
      return new TypeError(`TypeError: ${type} ${values} is not iterable`)
    }

    return new MyPromise((resolve, reject) => {
      let hasOneResolved = false;
      let counts = 0;
      let len = promiseArr.length;

      for (let i = 0; i < len; i++) {
        let instance = promiseArr[i];

        // Promise.resolve(p)用于处理传入值不为Promise的情况
        MyPromise.resolve(instance).then(res => {
          if (hasOneResolved) return;
          hasOneResolved = true;
          resolve(res);
        }, err => {
          counts++;
          if (counts === len) {
            reject('AggregateError: All promises were rejected');
          }
        })
      }
    })
  }

拓展

Promise是没有中断方法的,fetch是基于Promise的,所以它的请求无法中断,我们来实现一下Promise的中断。

// 包装想要控制中断的Promise, 提供一个abort方法外部调用中断Promise
function promiseAbort(promise) {
  let abort = null;
  let abortPromise = new Promise((resolve, reject) => {
    abort = reject;
  })
  let p = Promise.race([promise, abortPromise]);
  p.abort = abort;
  return p;
}

abort写出来了,日常使用中,有些异步请求需要超时取消,我们可以根据abort,同理自定义超时。

function promiseTimeout(promise, time = 5000) {
  let abortPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('timeout');
    }, time);
  })
  return Promise.race([promise, abortPromise]);
}

好了,以上就是Promise的基本原理与实现了,希望你能从我的这篇博客中有所收获,谢谢。

代码仓库点击查看

@jappp jappp changed the title 从零开始手写Promise 从零开始一步一步实现一个完整的Promise Mar 6, 2021
@jappp jappp changed the title 从零开始一步一步实现一个完整的Promise 从零开始,一步一步实现一个完整的Promise Mar 6, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant