likes
comments
collection
share

我把 JS Promise 翻了个底朝天,以后再也不怕 Promise 面试题了

作者站长头像
站长
· 阅读数 37

开头

JS Promise 的源码使用了多个设计模式,比如工厂、观察者等。其中,我认为最核心的是 观察者模式,executor 中的代码类似被观察者,通过 then 传入的回调函数类似观察者被观察者通过 resolve/reject 方法通知到观察者

Promise 的实现并不是完全符合观察者模式的定义。在观察者模式中,观察者必须要显式地注册到被观察者中,而在 Promise 中,then 方法中的回调函数并不需要显式地注册到 Promise 实例中,而是通过调用 then 方法来间接地与 Promise 实例关联起来。此外,Promise 还有一些其他的特性,如 Promise.allPromise.race 等,这些特性在观察者模式中并不存在。

MDN 官网对 Promise 的特性给出了不少的规则描述,而要彻底掌握 Promise,最简单的方法就是实现一个 Promise,我们开始吧!

制作自己的 Promise

大概框架

先写出大概的框架

class MyPromise {
  constructor() {}

  private resolve = () => {};

  private reject = () => {};

  then = () => {};

  catch = () => {};

  finally = () => {};
}

初始化状态和结果

Promise 内部维护了一个状态机。有三种状态:

  • pending
  • resolved
  • rejected

状态的扭转很简单,只有两种情况:

  • pending → resolved,拿到成功的数据
  • pending → rejected,拿到失败的原因

初始化状态和结果

enum State {
  Pending,
  Resolved,
  Rejected,
}

class MyPromise {
  // 初始状态为 pending
  private state = State.Pending;

  // 保存 Promise 的结果
  private result?: any;

  //..
}

状态的扭转

下图可以看出,用 resolve/reject 方法来扭转状态,并且状态和结果是强绑定的。 我把 JS Promise 翻了个底朝天,以后再也不怕 Promise 面试题了

添加 resolve/reject 方法,更新状态和结果 从 setResultAndStatus 方法可以看出,如果是 pending 状态的话,resolve/reject 方法不做任何操作,所以 Promise 的状态扭转是不可逆的

class MyPromise {
  //..

  private resolve = (value: any | MyPromise) => {
    this.setResultAndStatus(value, State.Resolved);
  };

  private reject = (reason?: any) => {
    this.setResultAndStatus(reason, State.Rejected);
  };

  private setResultAndStatus = (value: any, state: State) => {
     // 这里就是为什么 Promise 的状态是不可逆的. 如果 status 不是 pending 则是直接 return
    if (this.state !== State.Pending) {
      return;
    }

    this.state = state;
    this.result = value;
  };
}

executor - the Revealing Constructor Pattern

我们通过 new Promise 的时候通过构造函数给到 Promise executor 的具体实现

这种通过构造函数暴露内部方法的设计,用到了 revealing constructor pattern 设计模式。好处是,只有 new Promise 时外界可以操作 Promise 的状态。

补充构造函数的代码

class MyPromise {
  //..
  constructor(
    executor: (
      resolve: (value: any | MyPromise) => void,
      reject: (reason?: any) => void
    ) => void
  ) {
    executor(this.resolve, this.reject);
  }

  //..
}

executor 抛出异常

添加 try catch 逻辑,如果 executor 中抛出异常,我们通过 .then 或者 .catch 传入的处理异常的回调函数会被调用

class MyPromise {
  //..
  constructor(
    //..
  ) {
    try {
      executor(this.resolve, this.reject);
    } catch (e) {
      this.reject(e);  // 这样抛出异常的话下面的 .catch 可以接到
    }
  }
  //..
}

executor 支持异步调用

executor 是同步代码,但它的内部可以通过 setTimeout 等方法进行异步操作

  • 新增 handlersQueue 队列用来存放通过 then 方法传入的回调函数。
  • 新增 executeHandlers 用来执行队列中的回调函数。
  • then 中的回调会先加入队列,等 executor 中的异步代码跑完,通过 resolve 方法调用到 executeHandlers
class MyPromise {

  // 它的作用下面会讲到
  private handlersQueue: Handler[] = [];

  private setResultAndStatus = (value: any, state: State) => {
    //..

    // 扭转完状态后,处理所有回调函数
    this.executeHandlers();
  };

  private executeHandlers = () => {
    // 如果状态还是 pending,说明 executor 中的异步代码还没有结束。直接返回,不执行, 不执行 handlers 是没关系的,因为 handlers 都保存在 handlersQueue 中,等待着状态改变后会再执行的
    if (this.state === State.Pending) {
      return;
    }

	// 处理所有回调函数
    this.handlersQueue.forEach((handler) => {
      if (this.state === State.Resolved) {
        handler.handleOnFullfilled(this.result);  // 此时 result 是异步操作成功后给的数据
      } else {
        handler.handleOnRejected(this.result);  // 此时 result 是异步操作失败后给的失败原因
      }
    });

    // 执行完毕后,清空 handlers
    this.handlersQueue = [];
  };
}

then

值穿透链式调用的原理都在 then 方法中。

then 方法的原理可以总结为:注册回调函数,等待 Promise 对象的状态扭转后,构造一个新的Promise 对象并返回它,然后下一层可以继续调用 then

then 和微任务

then 中的回调函数会在微任务阶段执行,实现起来很简单,把执行回调函数的代码放入微任务队列即可

const runAsync = (cb: () => void) => {
  setTimeout(cb, 0);
};

class MyPromise {
  // ..
  private executeHandlers = () => {
    // ..
    // 模拟放入微任务队列,这样 then 中的回调会在微任务阶段被调用
    runAsync(() => {
      this.handlersQueue.forEach((handler) => {
        if (this.state === State.Resolved) {
          handler.handleOnFullfilled(this.result);
        } else {
          handler.handleOnRejected(this.result);
        }
      });

      this.handlersQueue = [];
    });
  };

  then = (
    onFullfilled?: (value: any) => any | MyPromise,
    onRejected?: (reason: any) => any | MyPromise
  ) => {
    // ..
    // 执行队列中的所有回调函数
    this.executeHandlers();
    // ..
  };
}

then 和值穿透

值穿透使得我们可以在 then 方法中省略不需要的回调函数,下面是值穿透的例子

new Promise((resolve, _) => {
  resolve(1);
})
  .then(2)  // 2 不会往下传
  .then(console.log);  // 1

值穿透的原理见下方代码注释

executeHandlers = () => {
  //..
  runAsync(() => {
    this.handlersQueue.forEach((handler) => {
      // 根据状态执行不同的回调函数
      if (this.state === State.Resolved) {
        // 通过这行代码把本次 promise 的结果传递给下一个 promise
        handler.handleOnFullfilled(this.result);
      }
    });
  });
  //..
};

then = (
  onFullfilled?: (value: any) => any | MyPromise,
  onRejected?: (reason: any) => any | MyPromise
) => {
  return new MyPromise((resolve: any, reject: any) => {
    const handler = {
      handleOnFullfilled: (value: any) => {
        if (!onFullfilled || typeof onFullfilled !== "function") {
          // 这里可以看到值穿透的原理:
          //  如果给 then 的不是个回调函数,那么新创建的 promise 的 result 直接设为 value
          resolve(value);
        } else {
          // ..
        }
      },

      // handleOnRejected 原理同上
  });
};

then 和链式调用

我画了一个图来描述链式调用then 返回新的 Promise,它继续调用 then

我把 JS Promise 翻了个底朝天,以后再也不怕 Promise 面试题了

由下方代码也可以看出 链式调用的原理是,then 返回一个新的 Promise,它可以继续调用下一个 then

then = (
  onFullfilled?: (value: any) => any | MyPromise,
  onRejected?: (reason: any) => any | MyPromise
) => {
  // 返回一个新的 promise,这样可以实现链式调用,即 then 的返回值是个 Promise, 可以继续调用 then
  return new MyPromise((resolve: any, reject: any) => {
	  //..
  }
};

then 和并行执行

MDN 文档中有这么一段描述:

我把 JS Promise 翻了个底朝天,以后再也不怕 Promise 面试题了 大体意思是,当我们在同一个 promise (取名 promise1) 上分别调用两次 then (不是链式调用)的时候,会创建两个新的 promise,分别放入到这个 promise1 中的队列。类似于开了两条道路,这两条道路互不干扰。就像下面这张图:

我把 JS Promise 翻了个底朝天,以后再也不怕 Promise 面试题了

下面代码中,promise1 内部的队列会保存两次调用 then 生成的新 promise,以确保它们可以按顺序调用

const promise1 = new Promise((resolve) => resolve("ok"));

// then 会生成一个新的 promise, 然后放入 promise1 内部的队列中
promise1.then((v) => {
  return new Promise((resolve) => {console.log(1)});
});

// 此时 promise1 的 handlersQueue 的长度为1

// then 会又生成一个新的 promise, 然后又放入 promise1 内部的队列中
promise1.then((v) => {
  return new Promise((resolve) => {console.log(2)});
});

// 此时 promise1 的 handlersQueue 的长度为2

// output:
// 1
// 2

那么怎么确保回调函数是按顺序调用的呢?这时就需要一个队列来存放回调函数们,也就是代码中的 handlersQueue 队列

代码实现:

class MyPromise {

  // 用来保存多个 then (不是链式调用) 传入的回调函数
  private handlersQueue: Handler[] = [];

  //..

  private executeHandlers = () => {
    //..
    runAsync(() => {
      // 按顺序调用队列中的回调函数
      this.handlersQueue.forEach((handler) => {
        if (this.state === State.Resolved) {
	      // 通过 .then(callback, _) 传入的回调函数
          handler.handleOnFullfilled(this.result);
        } else {
	      // 通过 .then(_, callback) 或者 .catch(callback) 传入的回调函数
          handler.handleOnRejected(this.result);
        }
      });
	  // 清空队列,避免重复调用
      this.handlersQueue = [];
    });
  };

  //..
}

适配 thenable

这段代码中,resolve 传入了一个新的 promise,你能看出运行结果吗?

new Promise((resolve) => {
  const inner = new Promise((resolve) => {
    resolve(1); // 把 inner 的 status 设为 resolved, result 设为 1
  });
  resolve(inner);
}).then((v) => {
  console.log("outer", v);
});

console.log("a");

inner promise resolve 的结果可以被外层的 promise 拿到,为什么呢? 原理是这样的:

  1. resolve 方法的内部实现有一行判断:如果 innerthen 方法(说明 inner 是个 thenable 对象),就调用 inner.then(this.resolve, this.reject)
  2. then箭头函数,所以 then 里面的 this 指向的是外层 promise,而不是 resolve(new Promise(...)) 中的 promise。通过箭头函数的特性,inner 可以由 this 拿到外部 promise 的 handlersQueue,然后让外部 promise 拿到 result

then 回调函数返回值

then 的代码实现可以看出,then 内部会创建一个新的 promise 并去 resolve(value) ,这个 value 是我们给到 then 的回调函数的返回值。如果 then 回调没有返回值,那么后面一个 then 回调接收到的参数是 undefined

new Promise((resolve) => {
  resolve(1);  // 把 promise 内部的状态设为 fulfilled, value 设为 1
})
  .then((valueofPrevPromise) => {
    // 根据上一个 promise 的 value,生成新的 promise,并 resolve (value),所以 valueofPrevPromise 为 1
    console.log("b", valueofPrevPromise);
    // then 方法内部会通过这个回调函数生成一个新的 promise, 并且这个回调函数的返回值是新 promise resolve 的值,
    // 这里没有返回值,所以下面的 then 接收到的 value 是 undefined
  })
  .then((valueofPrevPromise) => {
    console.log("c", valueofPrevPromise);
  });

console.log("a");

// output
//a
//b 1
//c undefined

收尾工作 - catch finally

catch 和 finally 只是语法糖,它们内部仅仅是调用了 then

语法糖之 catch

catch = (onRejected?: (reason: any) => any | MyPromise) => {
  return this.then(undefined, onRejected);
};

语法糖之 finally

finally = (onFinally: () => void) => {
  return this.then(
    (value) => {
      onFinally();
      return value;
    },
    (reason) => {
      onFinally();
      throw reason;
    }
  );
};

总结

核心原理总结

  • 状态如何管理? 通过 resolve/reject 方法扭转状态,并且过滤掉非 pending 状态,实现状态不可逆。

  • Executor 为什么这么写? 使用了 Revealing Constructor Pattern,在构造函数阶段暴露 resolve/reject 方法,使用者只能在构造函数阶段扭转状态。

  • 链式调用是怎么实现的? 每个 then 方法都返回一个新的 Promise 对象。

  • 错误处理是怎么实现的? Promise 对象可以通过 .catch 或者 .then( _ , callback) 方法捕获错误并处理它们。原理是通过 Promise 内部帮我们调用 reject 方法。

  • 值穿透是怎么实现的? 原理在 then 的内部实现中,如果我们给 then 传入一个 type 不是 function 的值,then 会构造一个新的 promise,同时把现在的 promise 的 result 设为新的 promise 的 result。

最终完全体

可以在 GitHub 上查看最终源码: MyPromise源码

race、all 等 Promise 静态方法的实现不包括在本篇中,其实理解了本篇的代码,实现它们就很简单了。

References

Promise A+ 规范

Learn JavaScript Promises by Building a Custom Implementation

转载自:https://juejin.cn/post/7242493101088555064
评论
请登录