听风是风

学或不学,知识都在那里,只增不减。

导航

【JS】因两道Promise执行题让我产生自我怀疑,从零手写Promise加深原理理解

壹 ❀ 引

其实在去年七月份,博客所认识的一个朋友问了我一个关于Promise执行先后的问题,具体代码如下:

const fn = (s) => (
  new Promise((resolve, reject) => {
    if (typeof s === 'number') {
      resolve();
    } else {
      reject();
    }
  })
  .then(
    res => console.log('参数是一个number'),
  )
  .catch(err => console.log('参数是一个字符串'))
)
fn('1');
fn(1);
// 先输出   参数是一个number
// 后输出   参数是一个字符串

他的疑惑是,以上代码中关于Promise状态的修改都是同步的,那为什么fn(1)的输出还要早于fn('1')

说来惭愧,我当时对于这个输出也疑惑了半天,最后基于自己掌握的现有知识,给了对方一个自认为说的过去但现在回想起来非常错误的解释...想起来真是羞愧= =,这个问题也让我当时有了了解Promise底层原理的想法。

没过多久,另一位博客认识的朋友又问了我一道Promise执行顺序的题,代码如下:

Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
}).then((res) => {
    console.log(res)
})

Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() =>{
    console.log(6);
})

// 输出为0 1 2 3 4 5 6

我看了一眼题,结果难道不应该是0 1 4 2 3 5 6?对方抱着疑问而来,结果这次我自己都蒙圈了,这也让我意识到自己对于Promise的理解确实有点薄弱。

我承认,上面两道题真的有点为考而考的意思了,毕竟实际开发我们也不可能写出像例子2这样的代码,但站在面试的角度,对方总是需要一些评判标准来筛掉部分人,人人都不想卷,却又不得不卷,多懂一点总是没有坏处。

既然意识到自己的不足,那就花点功夫去了解Promise原理,如何了解?当然是模拟实现一个Promise,所以本篇文章的初衷是通过手写Promise的过程理解底层到底发生了什么,从而反向解释上面两道题为什么会这样。放心吧,当我写完我已经恍然大悟,所以你也一定可以,那么本文开始。

贰 ❀ 从零手写Promise

贰 ❀ 壹 搭建框架

对于手写新手而言,从零开始写一个Promise真正的难点在于你可能不清楚到底要实现Promise哪些特性,没事,我们从一个最简单的例子开始分析:

const p = new Promise((resolve, reject) => {
  // 同步执行
  resolve(1);
});
p.then(
  res => console.log(res),
  err => console.log(err)
);

从上述代码我们可以提炼出如下信息:

  1. new过程是同步的,我们传递了一个函数(resolve, reject)=>{resolve(1)}Promise,它会帮我们同步执行这个函数。
  2. 我们传递的函数接受resolve reject两个参数,这两个参数由Promise提供,所以Promise一定得有这两个方法。
  3. new Promise返回了一个实例,这个实例能调用then方法,因此Promise内部一定得实现then方法。

我们也别想那么多,先搭建一个基本的Promise框架,代码如下:

class MyPromise {

  constructor(fn) {
    // 这里的fn其实就是new Promise传递的函数
    fn(this.resolve, this.reject);
  }

  resolve = () => {}

  reject = () => {}

  then = () => {}
}

constructor中接受的参数fn其实就是new Promise传递的函数,我们在constructor中同步调用它,同时传递了this.resolvethis.reject,这也就解释了为何传递的函数会同步执行,以及如何使用到Promsise提供的resolve方法。

贰 ❀ 贰 增加状态管理与值记录

我们知道Promisepending、fuldilled、rejected三种状态,且状态一旦改变就无法更改,无论成功失败或者失败,Promise总是会返回一个succesValue或者failReason回去,所以我们来初始化状态、value以及初步的成功/失败逻辑:

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

class MyPromise {

  constructor(fn) {
    // 这里的fn其实就是new Promise传递的函数
    fn(this.resolve, this.reject);
  }
  // 初始化状态以及value
  status = PENDING;
	value = null;

  resolve = (value) => {
    // 当调用resolve时修改状态成fulfilled,同时记录成功的值
    if (this.status === PENDING) {
      this.value = value;
      this.status = FULFILLED;
    }
  }

  reject = (reason) => {
    // 当调用reject时修改状态成rejected,同时记录失败的理由
    if (this.status === PENDING) {
      this.value = reason;
      this.status = REJECTED;
    }
  }

  then = () => {}
}

叁 ❀ 叁 初步实现then

在实现Promise状态管理以及值记录后,我们接着来看看then,很明显then接受两个参数,其实就是成功的与失败的回调,而这两个函数我们也得根据之前的this.status来决定要不要执行,直接上代码:

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

class MyPromise {

  constructor(fn) {
    // 这里的fn其实就是new Promise传递的函数
    fn(this.resolve, this.reject);
  }
  // 初始化状态以及成功,失败的值
  status = PENDING;
  value = null;

  resolve = (value) => {
    // 当调用resolve时修改状态成fulfilled,同时记录成功的值
    if (this.status === PENDING) {
      this.value = value;
      this.status = FULFILLED;
    }
  }

  reject = (reason) => {
    // 当调用reject时修改状态成rejected,同时记录失败的理由
    if (this.status === PENDING) {
      this.value = reason;
      this.status = REJECTED;
    }
  }

  then = (fulfilledFn, rejectedFn) => {
    const callbackMap = {
      [FULFILLED]: fulfilledFn,
      [REJECTED]: rejectedFn
    };
    callbackMap[this.status](this.value);
  }
}

那么到这里我们已经实现了一个简陋的MyPromise,让我们检验下状态改变以及回调执行:

const p = new MyPromise((resolve, reject) => {
  // 同步执行
  resolve(1);
  reject(2);
});
p.then(
  res => console.log(res),
  err => console.log(err)
);
// 只输出了1

上述代码只输出了1,说明状态控制以及回调处理都非常成功!!!我们继续。

贰 ❀ 肆 异步修改状态

上述代码虽然运行正常,但其实只考虑了同步resolve的情况,假设我们修改状态在异步上下文中,就会引发意想不到的错误,比如:

const p = new MyPromise((resolve, reject) => {
  // 同步执行
  setTimeout(() => resolve(1), 2000);
});
p.then(
  (res) => console.log(res),
  (err) => console.log(err)
);

Uncaught TypeError: callbackMap[this.status] is not a function

简单分析下,因为目前我们对于Promise状态的修改依赖了resolve,但因为定时器的缘故,导致执行p.then执行时状态其实还是pending,从而造成callbackMap[this.status]无法匹配,因此我们需要添加一个pending状态的处理。

还有个问题,即使解决了callbackMap匹配报错,定时器等待结束后执行resolve,我们怎么再次触发对应回调的执行呢?要不我们在pending状态中把两个回调记录下来,然后在resolve或者reject时再调用记录的回调?说干就干:

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

class MyPromise {
  constructor(fn) {
    // 这里的fn其实就是new Promise传递的函数
    fn(this.resolve, this.reject);
  }
  // 初始化状态以及成功,失败的值
  status = PENDING;
  value = null;
  // 新增记录成功与失败回调的参数
  fulfilledCallback = null;
  rejectedCallback = null;

  resolve = (value) => {
    // 当调用resolve时修改状态成fulfilled,同时记录成功的值
    if (this.status === PENDING) {
      this.value = value;
      this.status = FULFILLED;
      // 新增成功回调的调用
      this.fulfilledCallback?.(value);
    }
  };

  reject = (reason) => {
    // 当调用reject时修改状态成rejected,同时记录失败的理由
    if (this.status === PENDING) {
      this.value = reason;
      this.status = REJECTED;
      // 新增失败回调的调用
      this.rejectedCallback?.(reason);
    }
  };

  then = (fulfilledFn, rejectedFn) => {
    const callbackMap = {
      [FULFILLED]: fulfilledFn,
      [REJECTED]: rejectedFn,
      // 针对异步问题,新增pending状态时记录并保存回调的操作
      [PENDING]: () => {
        this.fulfilledCallback = fulfilledFn;
        this.rejectedCallback = rejectedFn;
      },
    };
    callbackMap[this.status](this.value);
  };
}

再次执行上面定时器的例子,现在不管有没有异步修改状态,都能正常执行了!!!

贰 ❀ 伍 实现then多次调用

当我们new一个Promise后会得到一个实例,而这个实例其实是支持多次then调用的,比如:

const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 0);
});
p.then((res) => console.log(res));// 1
p.then((res) => console.log(res));// 1
p.then((res) => console.log(res));// 1

但如果我们我们使用自己实现的MyPromise去做相同的调用,你会发现只会输出1个1,原因也很简单,我们在pending情况下记录回调的逻辑只能记录一个,所以还得再改造一下,将fulfilledCallback定义成一个数组,如下:

class MyPromise {
	// ....
  
  // 修改为数组
  fulfilledCallback = [];
  rejectedCallback = [];

  resolve = (value) => {
    // 当调用resolve时修改状态成fulfilled,同时记录成功的值
    if (this.status === PENDING) {
      this.value = value;
      this.status = FULFILLED;
      // 新增成功回调的调用
      while (this.fulfilledCallback.length) {
        this.fulfilledCallback.shift()?.(value);
      }
    }
  };

  reject = (reason) => {
    // 当调用reject时修改状态成rejected,同时记录失败的理由
    if (this.status === PENDING) {
      this.value = reason;
      this.status = REJECTED;
      // 新增失败回调的调用
      while (this.rejectedCallback.length) {
        this.rejectedCallback.shift()?.(reason);
      }
    }
  };

  then = (fulfilledFn, rejectedFn) => {
    const callbackMap = {
      [FULFILLED]: fulfilledFn,
      [REJECTED]: rejectedFn,
      // 针对异步问题,新增pending状态时记录并保存回调的操作
      [PENDING]: () => {
        this.fulfilledCallback.push(fulfilledFn);
        this.rejectedCallback.push(rejectedFn);
      },
    };
    callbackMap[this.status](this.value);
  };
}

这也修改完成后再次执行上述例子,我们发现多次调用then已满足。

贰 ❀ 陆 实现then链式调用

OK,终于来到Promise链式调用这个环节了,对于整个Promise手写,我个人觉得这部分是稍微有点绕,不过我会尽力解释清楚,我们先看个最简单的例子:

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

p1.then((res) => {
  console.log(res);
  return new Promise((resolve) => resolve(2));
}).then((res) => {
  console.log(res);
});

// 1
// 2

假设我们将上述代码中的new Promise都改为new MyPromise,运行你会发现代码直接报错:

Uncaught TypeError: Cannot read properties of undefined (reading 'then')

不能从undefined上读取属性then?我不是在then里面return了一个new Promise吗?这咋回事?假设你是这样想的,那么恭喜你,你已经成功进入了思维误区。

我们将上面的例子代码进行拆分:

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

const p2 = p1.then((res) => {
  console.log(res);
  return new Promise((resolve) => resolve(2));
});

p2.then((res) => {
  console.log(res);
});

Promise若要实现链式调用,那么p1.then()一定得返回一个新的Promise,不然下一次链式调用的then从哪读取呢?

所以这个新的Promisethen方法创建并提供的,而(res)=>{console.log(1);return new Promise((resolve) => resolve(2))}这一段只是then方法调用时的callback,它的返回值(假设有值)将成为下次新的Promisevalue,所以上述代码中的return new Promise((resolve) => resolve(2))只是在为then创建的Promise准备value而已。看个例子:

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

p1.then((res) => {
  console.log(res);
  return new Promise((resolve) => {
    // 我们不改状态
    console.log("不做状态改变的操作");
  });
}).then((res) => {
  console.log(res); // 这里不会输出
});

在这个例子中,第二个then并不会执行,这是因为p1.then()虽然创建了一个新的Promise,但是它依赖的value由内部的new Promise提供,很明显我们并未做任何状态改变的操作,导致第二个Promise不会执行。

那么到这里我们能提炼出两个非常重要的结论:

  1. Promise若要实现链式调用,then一定得返回一个新的Promise
  2. 新的Promise的状态以及value由上一个thencallback决定。

再次回到我们自己实现的then方法,很明显它并没有创建一个新Promise,函数没返回值默认返回undefined,这就解释了为啥报这个错了。

好了,解释完了我们得再次改造我们的MyPromise,为then提供返回Promise的操作,以及对于thencallback结果的处理:

const resolvePromise = (result, resolve, reject) => {
  // 判断result是不是promise
  if (result instanceof MyPromise) {
    result.then(resolve, reject);
  } else {
    resolve(result);
  }
};

class MyPromise {
	// ....
  then = (fulfilledFn, rejectedFn) => {
    // 我们得在每次调用then时返回一个Promise
    return new MyPromise((resolve, reject) => {
      const callbackMap = {
        [FULFILLED]: fulfilledFn,
        [REJECTED]: rejectedFn,
        // 针对异步问题,新增pending状态时记录并保存回调的操作
        [PENDING]: () => {
          this.fulfilledCallback.push(fulfilledFn);
          this.rejectedCallback.push(rejectedFn);
        },
      };
      // 上一个then的callback的结果将作为新Promise的值
      const result = callbackMap[this.status](this.value);
      resolvePromise(result, resolve, reject);
    });
  };
}

经过这样修改,再次运行代码,我们发现then链式调用已经成功了!!!!

我知道上面这段代码有同学又懵了,我建议先看看上面对于then链式调用我们得出的两个结论,然后我再用两个例子来解释这段代码为什么要这样写,别着急,我会解释的非常清楚。

对于then返回一个Promise的修改这一点大家肯定没问题,疑惑的点应该都在新增的resolvePromise方法中。其实在前面我们解释过了,第一个then回调返回结果(函数没返回默认就是undefined),会作为下一个新Promise的参数,而这个返回的结果它可能是一个数字,一个字符串,也可能是一个Promise(上面的例子就是返回了一个promise作为参数),先看一个简单的例子:

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

p1.then((res) => {
  return 520;
}).then((res) => {
  console.log(res);// 520
});

这个例子的第一个thencallback直接返回了一个数字,但奇怪的是下一个then居然能拿到这个结果,这是因为上述代码等同于:

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

p1.then((res) => {
  return Promise.resolve(520);
}).then((res) => {
  console.log(res);// 520
});

没错,这也是Promise的特性之一,如果我们的then的回调返回的是一个非Promise的结果,它等同于执行Promise.resolve(),这也是为啥我们在自定义的resolvePromise中一旦判断result不是Promise就直接执行resolve的缘故。

强化理解,来给大家看个更离谱的例子:

Promise.resolve()
  .then(() => {
    return new Error("error!!!");
  })
  .then((res) => {
    console.log("成功啦");
  })
  .catch((err) => {
    console.log("失败啦");
  });

猜猜这段代码最终输出什么?输出成功啦,因为它等同于:

Promise.resolve()
  .then(() => {
    return Promise.resolve(new Error("error!!!"));
  })
  .then((res) => {
    console.log("成功啦");
  })
  .catch((err) => {
    console.log("失败啦");
  });

对于Promise而言,它只是一个type类型是错误的value而已,当然执行成功回调。有同学可能就要问了,那这个例子假设我就是想执行catch咋办?两种写法:

Promise.resolve()
  .then(() => {
  	// 第一种办法,直接reject
    return Promise.reject(new Error("error!!!"));
  	// 第二种办法,直接抛出错误
  	// throw new Error('error!!!')
  })
  .then((res) => {
    console.log("成功啦");
  })
  .catch((err) => {
    console.log("失败啦");
  });

解释了resolvePromise中的resolve(result),再来解释下为什么resultPromise时执行result.then(resolv,reject)就可以了。

我们已知回调的结果会作为下一个Promise的参数,那假设这个参数自身就是个Promise,对于then返回的新Promise而言,它就得等着作为参数的Promise状态改变,在上面我们已经演过参数是Promise但不会改变状态的例子,结果就是下一个then不会调用。

所以对于下一个新Promise而言,我就等着参数自己送到嘴里来,你状态变不变,以及成功或者失败那是你自己的事,因此我们通过result.then()来等待这个参数Promise的状态变化,只要你状态变了,比如resolve了,那是不是得执行this.resolve方法,从而将值赋予给this.value,那么等到下一次执行then时自然就能读取对应this.value了,是不是很巧妙?

另外,result.then(resolve, reject);这一句代码其实是如下代码的简写,不信大家可以写个小例子验证下:

result.then((res)=> resolve(res), (err)=> reject(err));

算了,我猜测你们可能还是懒得写例子验证,运行下如下代码就懂了,其实是一个意思:

// 定时器是支持传递参数的
setTimeout(console.log, 1000, '听风是风')

// 等同于
setTimeout((param)=> console.log(param), 1000, '听风是风')

那么上面的简写,其实也是这个意思,然后我们画张图总结下上面的结论:

恭喜你,模拟Promise最为绕的一部分你弄清楚了,剩下的模拟都是小鱼小虾,我们继续。

贰 ❀ 柒 增加then不能返回Promise自己的判断

直接看个例子,这个代码执行报错:

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

const p2 = p1.then((res) => {
  console.log(res);
  return p2;
});

Uncaught (in promise) TypeError: Chaining cycle detected for promise #

结合上面我们自己实现then的理解,p1.then()返回了一个Promise p2,结果p2又成p2自己需要等待的参数,说直白点就是p2等待p2的变化,自己等自己直接陷入死循环了。对于这个问题感兴趣的同学可以看看segmentfault中对于这个问题的解答 关于promise then的问题

我们也来模拟这个错误的捕获,直接上代码:

const resolvePromise = (p, result, resolve, reject) => {
  // 判断是不是自己,如果是调用reject
  if (p === result) {
    reject(new Error("Chaining cycle detected for promise #<Promise>"));
  }
  // 判断result是不是promise
  if (result instanceof MyPromise) {
    result.then(resolve, reject);
  } else {
    resolve(result);
  }
};

class MyPromise {
	// ....

  then = (fulfilledFn, rejectedFn) => {
    // 我们得在每次调用then时返回一个Promise
    const p = new MyPromise((resolve, reject) => {
      const callbackMap = {
        [FULFILLED]: fulfilledFn,
        [REJECTED]: rejectedFn,
        // 针对异步问题,新增pending状态时记录并保存回调的操作
        [PENDING]: () => {
          this.fulfilledCallback.push(fulfilledFn);
          this.rejectedCallback.push(rejectedFn);
        },
      };
      // 上一个then的callback的结果将作为新Promise的值
      const result = callbackMap[this.status](this.value);
      // 新增了一个p,用于判断是不是自己
      resolvePromise(p, result, resolve, reject);
    });
    return p;
  };
}

执行上面的代码,结果又报错....

index.html:159 Uncaught ReferenceError: Cannot access 'p2' before initialization

错误说我们不能在p2初始化好之前调用它,其实看上面那个代码本身就很奇怪,哪有在产生自己的函数的callback中使用自己的,但这就是Promise的特性之一,咱也没办法。

现在思路就是让resolvePromise(p, result, resolve, reject)这一句执行晚一点,起码要晚于新Promise的产生,咋办?当然是用异步,比如定时器。但我们知道Promisethen是微任务,为了更好的模拟这个异步行为,这里借用一个API,名为queueMicrotask,想详细了解的同学可以点击链接跳转MDN,这里我们直接上个简单的例子:

queueMicrotask(() => {
  console.log("我是异步的微任务");
});
setTimeout(() => console.log("我是异步的宏任务"));
console.log("我是同步的宏任务");

看来这个API非常符合我们的预期,因为需要考虑pending状态暂存函数的行为,我们还是额外封装两个成功与失败的微任务,继续改造:

then = (fulfilledFn, rejectedFn) => {
  // 我们得在每次调用then时返回一个Promise
  const p = new MyPromise((resolve, reject) => {
    // 封装成功的微任务
    const fulfilledMicrotask = () => {
      // 创建一个微任务等待 promise2 完成初始化
      queueMicrotask(() => {
        // 获取成功回调函数的执行结果
        const result = fulfilledFn(this.value);
        // 传入 resolvePromise 集中处理
        resolvePromise(p, result, resolve, reject);
      });
    };
    // 封装失败的微任务
    const rejectedMicrotask = () => {
      // 创建一个微任务等待 promise2 完成初始化
      queueMicrotask(() => {
        // 调用失败回调,并且把原因返回
        const result = rejectedFn(this.value);
        // 传入 resolvePromise 集中处理
        resolvePromise(p, result, resolve, reject);
      });
    };
    const callbackMap = {
      [FULFILLED]: fulfilledMicrotask,
      [REJECTED]: rejectedMicrotask,
      // 针对异步问题,新增pending状态时记录并保存回调的操作
      [PENDING]: () => {
        this.fulfilledCallback.push(fulfilledMicrotask);
        this.rejectedCallback.push(rejectedMicrotask);
      },
    };

    callbackMap[this.status]();
  });
  return p;
};

好了,现在执行下面这段代码来检验下效果:

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

const p2 = p1.then((res) => {
  console.log(res);
  return p2;
});

p2.then(
  () => {},
  (err) => console.log(err)
);

有同学就要说了,你这不对啊,原生Promise是直接就报错,你这还要p2.then()才能感知报错。咱前面就说了,这是在模拟仿写Promise,大致达到这个效果,而且这个小节的核心目的其实是为了引出thencallback执行为什么是异步的原因

贰 ❀ 捌 添加new Promise以及then执行错误的捕获

我们知道new Promise或者then回调执行报错是,then的错误回调是能成功捕获的,我们也来模拟这个过程,这个好理解一点我们就直接上代码:

class MyPromise {
  constructor(fn) {
    try {
      // 这里的fn其实就是new Promise传递的函数
      fn(this.resolve, this.reject);
    } catch (e) {
      this.reject(e);
    }
  }

	// ....

  then = (fulfilledFn, rejectedFn) => {
    // 我们得在每次调用then时返回一个Promise
    const p = new MyPromise((resolve, reject) => {
      // 封装成功的微任务
      const fulfilledMicrotask = () => {
        // 创建一个微任务等待 promise2 完成初始化
        queueMicrotask(() => {
          // 添加错误捕获
          try {
            // 获取成功回调函数的执行结果
            const result = fulfilledFn(this.value);
            // 传入 resolvePromise 集中处理
            resolvePromise(p, result, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      };
      // 封装失败的微任务
      const rejectedMicrotask = () => {
        // 创建一个微任务等待 promise2 完成初始化
        queueMicrotask(() => {
          // 添加错误捕获
          try {
            // 调用失败回调,并且把原因返回
            const result = rejectedFn(this.value);
            // 传入 resolvePromise 集中处理
            resolvePromise(p, result, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      };

      // ....
    });
    return p;
  };
}

执行如下例子,效果很理想:

const p1 = new MyPromise((resolve, reject) => {
  throw new Error("new报错啦");
});

const p2 = p1.then(
  (res) => {
    console.log(res);
  },
  (err) => {
    console.log("我是错误回调", err);
    throw new Error("then报错啦");
  }
);
p2.then(
  () => {},
  (err) => console.log("我是错误回调", err)
);

贰 ❀ 玖 实现then无callback,或者callback不是函数时的值穿透

看标题可能不明白什么意思,看个例子就懂了:

const p1 = new Promise((resolve, reject) => {
  resolve("听风");
});
const fn = () => {};
p1.then(fn()) // 函数调用,并不是一个函数
  .then(1) // 数字
  .then('2') // 字符串
  .then() // 不传递
  .then((res) => console.log(res)); // 听风

说通俗一点就是,假设then没有回调,或者回调根本不是一个函数,那么你就当这个then不存在,但我们的MyPromise很明显没考虑无回调的情况,现在实现这一点:

then = (fulfilledFn, rejectedFn) => {
  // 新增回调判断,如果没传递,那我们就定义一个单纯起value接力作用的函数
  fulfilledFn =
    typeof fulfilledFn === "function" ? fulfilledFn : (value) => value;
  rejectedFn =
    typeof rejectedFn === "function"
      ? rejectedFn
      : (value) => {
          throw value;
        };
  // 我们得在每次调用then时返回一个Promise
  const p = new MyPromise((resolve, reject) => {
		// ...
  });
  return p;
};

上述代码做的事情非常简单,检查两个回调是不是函数,不是函数我们就帮它定义一个只做值接力的函数,你传递什么我们就原封不动返回什么的函数。为啥rejectedFn要定义成(value)=>{throw value}呢?这是因为我们希望当此函数执行时能走reject路线,所以一定得抛错,那为什么不写成(value)=>{throw new Error(value)}这样?因为.then().then()这种会导致new Error执行多次,结果就不对了。我们在贰 ❀ 陆小节,提到有两种办法可以在报错时让catch捕获,一种是直接reject(),另一种就是throw一个错误,后面的throw影响更小一点,所以就用这种。

经过上面的修改,此时我们再执行我们无回调的例子,此时不管是成功还是失败,都能成功执行了。

贰 ❀ 拾 实现静态resolve与reject

创建Promise除了new Promise之外,其实还能通过Promise.resolve()静态方法直接获取,但目前MyPromise只提供了实例方法,所以我们需要补全静态方法:

class MyPromise {
	// ....

  // 静态resolve
  static resolve(value) {
    // 加入蚕食是一个promise,原封不动的返回
    if (value instanceof MyPromise) {
      return value;
    }
    return new MyPromise((resolve, reject) => {
      resolve(value);
    });
  }

  // 静态reject
  static reject(value) {
    if (value instanceof MyPromise) {
      return value;
    }
    return new MyPromise((resolve, reject) => {
      reject(value);
    });
  }

	// ....
}

逻辑也很简单,如果参数是一个Promise,那就原封不动返回,如果不是,我们就手动帮他创建一个Promise即可,这个特性可以通过下面这个例子验证:

const p1 = new Promise((resolve, reject) => {
  resolve("听风");
});
const p2 = new Promise((resolve, reject) => {
  resolve("我是一个promise");
});

p1.then(
  (res) => {
    return Promise.resolve(p2);
  },
  (err) => console.log(err)
).then(
  (res) => console.log(res), // 我是一个promise
  (err) => console.log(err)
);

可以看到假设Promise.resolve参数本身就是一个Promise时,这个方法本质上就想啥也没做一样,但如果参数是一个数字,它会帮你包装成一个Promise,我们将上述代码的new Promise改成new MyPromise,效果完全一致,说明模拟的很理想!!

OK,那么到这里,一个满足基本功能的MyPromise就实现完毕了,但事先说明,它并未符合Promise A+规范,如果要做到一样,我们仍需要对then方法中做一些条件判断,这些逻辑都是规范明确告诉你应该怎么写,没有什么道理可言,但鉴于这段逻辑补全对于我们理解上面的题不会有额外的帮助,因此我就不做额外的改造了,下面是一份实现到现在完整的MyPromise代码:

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

const resolvePromise = (p, result, resolve, reject) => {
  if (p === result) {
    reject(new Error("Chaining cycle detected for promise #<Promise>"));
  }
  // 判断result是不是promise
  if (result instanceof MyPromise) {
    result.then(resolve, reject);
  } else {
    resolve(result);
  }
};

class MyPromise {
  constructor(fn) {
    try {
      // 这里的fn其实就是new Promise传递的函数
      fn(this.resolve, this.reject);
    } catch (e) {
      this.reject(e);
    }
  }

  // 初始化状态以及成功,失败的值
  status = PENDING;
  value = null;
  // 新增记录成功与失败回调的参数
  fulfilledCallback = [];
  rejectedCallback = [];

  // 静态resolve
  static resolve(value) {
    if (value instanceof MyPromise) {
      return value;
    }
    return new MyPromise((resolve, reject) => {
      resolve(value);
    });
  }

  // 静态reject
  static reject(value) {
    if (value instanceof MyPromise) {
      return value;
    }
    return new MyPromise((resolve, reject) => {
      reject(value);
    });
  }

  resolve = (value) => {
    // 当调用resolve时修改状态成fulfilled,同时记录成功的值
    if (this.status === PENDING) {
      this.value = value;
      this.status = FULFILLED;
      // 新增成功回调的调用
      while (this.fulfilledCallback.length) {
        this.fulfilledCallback.shift()?.(value);
      }
    }
  };

  reject = (reason) => {
    // 当调用reject时修改状态成rejected,同时记录失败的理由
    if (this.status === PENDING) {
      this.value = reason;
      this.status = REJECTED;
      // 新增失败回调的调用
      while (this.rejectedCallback.length) {
        this.rejectedCallback.shift()?.(reason);
      }
    }
  };

  then = (fulfilledFn, rejectedFn) => {
    // 新增回调判断,如果没传递,那我们就定义一个单纯起value接力作用的函数
    fulfilledFn =
      typeof fulfilledFn === "function" ? fulfilledFn : (value) => value;
    rejectedFn =
      typeof rejectedFn === "function"
        ? rejectedFn
        : (value) => {
            throw value;
          };
    // 我们得在每次调用then时返回一个Promise
    const p = new MyPromise((resolve, reject) => {
      // 封装成功的微任务
      const fulfilledMicrotask = () => {
        // 创建一个微任务等待 promise2 完成初始化
        queueMicrotask(() => {
          try {
            // 获取成功回调函数的执行结果
            const x = fulfilledFn(this.value);
            // 传入 resolvePromise 集中处理
            resolvePromise(p, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      };
      // 封装失败的微任务
      const rejectedMicrotask = () => {
        // 创建一个微任务等待 promise2 完成初始化
        queueMicrotask(() => {
          try {
            // 调用失败回调,并且把原因返回
            const x = rejectedFn(this.value);
            // 传入 resolvePromise 集中处理
            resolvePromise(p, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      };
      const callbackMap = {
        [FULFILLED]: fulfilledMicrotask,
        [REJECTED]: rejectedMicrotask,
        // 针对异步问题,新增pending状态时记录并保存回调的操作
        [PENDING]: () => {
          this.fulfilledCallback.push(fulfilledMicrotask);
          this.rejectedCallback.push(rejectedMicrotask);
        },
      };
      callbackMap[this.status]();
    });
    return p;
  };
}

代码看着有点多,但事实上顺着思路写下来,其实没有什么很大的难点。

叁 ❀ 重回面试题

MyPromise实现完毕,现在让我们回头再看看第一道题,现在再来分析为什么这么输出,为了方便,我将题目加在下方:

const fn = (s) => (
  new Promise((resolve, reject) => {
    if (typeof s === 'number') {
      resolve();
    } else {
      reject();
    }
  })
  .then(
    res => console.log('参数是一个number'),
    // 注意,这里没定义失败回调
  )
  .catch(err => console.log('参数是一个字符串'))
)
fn('1');
fn(1);

叁 ❀ 壹 第一轮执行

我们先考虑同步执行,首先我们执行fn('1'),此时执行new Promise,因为这个过程是一个同步行为,因此它会立马调用传递给Promise的回调,然后走逻辑判断,因为不是一个数字,导致执行了reject()

紧接着执行.then,前文也说了.then注册微任务的行为是同步,但需要注意的是,.then中并未提供失败函调,因此对于Promise底层而言,它要做的是值和状态的穿透,这些先不管,毕竟我们还有剩余的同步任务没走完。

于是紧接着,我们又执行了fn(1),同样同步执行.then()注册了成功的回调,到这里,同步任务全部执行完成。

叁 ❀ 贰 第二轮执行

由于同步代码全部跑完了,此时肯定得按照我们注入的微任务顺序,依次执行微任务,由于fn('1')这一步的.then()没有失败回调,默认理解为执行了值穿透的步骤,于是返回的新Promise的状态依旧是rejected且值为undefined(因为reject没传值)。

紧接着,我们执行fn(1)的成功回调,于是先输出了参数是一个number,注意,这个成功回调只有一个console,并无返回,我们默认理解为return resolve(undefined),因此返回了一个状态是成功,但是值是undefined的新Promise

叁 ❀ 叁 第三轮执行

两次调用的.then又返回了两个新promise,因为状态一开始都改变了,所以还是先走rejectedPromise,并成功触发.catch,此时输出参数是一个字符串,而第二个Promise是成功状态,不能触发.catch,到此执行结束。

为了更好理解值穿透的解释,我们改改代码:

const fn = (s) => {
  new Promise((resolve, reject) => {
      if (typeof s === "number") {
        resolve(1);
      } else {
        reject(2);
      }
    })
    .then(
      (res) => console.log("参数是一个number")) // 注意,这里虽然提供了函数,但是没返回,所以理解为  return resolve(undefined)
    // 注意,这里没传递失败函数,只要callback不是一个函数,默认值穿透拿上一步的promise
    .then(
      (succ) => console.log(succ) // 这里一定输出undefined,毕竟上一步没返回值,默认理解成resolve(undefined)
    )
    .catch((err) => {
      console.log("参数是一个字符串");
      console.log(err); // 这里输出2,因为上一个then又没失败回调,一直穿透下来
    });
};
fn("1");
fn(1);
// 参数是一个number
// undefined
// 参数是一个字符串
// 2

而假设我们有为then提供失败回调,那么此时返回的顺序就符合一开始我们对于Promise还不太了解时能够理解的预期:

const fn = (s) => {
  new Promise((resolve, reject) => {
    if (typeof s === "number") {
      resolve();
    } else {
      reject();
    }
  })
    .then(
      (res) => console.log("参数是一个number"),
      (err) => console.log("参数是一个字符串11")
    )
    .catch((err) => {
      console.log("参数是一个字符串");
      // 看看上一个then传递的value是啥
      console.log(err);
    });
};
fn("1");
fn(1);

因为有提供失败回调,这就导致.catch不会执行了。那么到这里,第一道面试题算是非常透彻的解释完了,也多亏手写Promise加深了对于底层原理的理解。

我们接着聊第二道题,为了方便理解,我们将这道题的Promise全部改成MyPromise,再看看输出如何:

MyPromise.resolve()
  .then(() => {
    console.log(0);
    return MyPromise.resolve(4);
  })
  .then((res) => {
    console.log(res);
  });

MyPromise.resolve()
  .then(() => {
    console.log(1);
  })
  .then(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(5);
  })
  .then(() => {
    console.log(6);
  });

// 0 1 2 4 3 5 6

使用我们实现的MyPromise,结果发现4跑到了2后面,我们可以先站在自己实现的逻辑上解释这个现象。

我们已知.then()会返回一个Promise,且这个Promise啥时候执行以及参数都是由.then()接收的回调函数的返回结果来决定的。而在题目中MyPromise.resolve(4)这一句,其实本质上就等同于如下代码(参照静态resolve实现):

MyPromise.resolve()
  .then(() => {
    console.log(0);
    return new MyPromise(resolve=>resolve(4));
  })
  .then((res) => {
    console.log(res);
  });

而在then调用中最后都需要走resolvePromise,此方法会判断参数是否是一个Promise,如果是就需要执行result.then()

不知道你脑袋里是否已经有了一种感觉,相比.then(()=>console.log(2)),前者比后者多执行了一次.then,也就是说多创建了一次微任务,这就导致4一定晚于2输出。

但是题目2的输出,4其实是在3之后,会不会有一种可能,官方Promisereturn Promise.resolve(4)这种行为在底层其实创建了两次微任务,导致4延迟了2次后才输出呢?

在查证了V8中关于Promise的源码,直接说结论,确实是创建了两次微任务,因为涉及到篇幅问题,若对这两个微任务有兴趣,可直接阅读知乎问题 promise.then 中 return Promise.resolve 后,发生了什么?,有优秀答主详细分析了源码中两次微任务产生的地方,只是站在我的角度,我个人觉得了解到这个结论就好,再继续深入分析收益不成正比,所以在这我偷个懒。

肆 ❀ 总

那么到这里,一篇长达八千多字的文章也记录完成了,本着了解两道面试题的态度,我们尝试手写了一个自己的Promise,在实现过程中,就我自己而言确实又了解了不少之前从未听过的特性,比如Promise不能返回自己,比如.then返回的Promise的执行其实依赖了.then回调函数的结果等等。另外,我会在参考中附带一篇我觉得很不错的Promise面试题集合,大家也可以在看完这篇文章后尝试做做这里面的执行题,加深对于Promise的理解。

另外,实际面试中基本没有真让你手写Promise A+的题,毕竟规范那么多,手写下来难度过大,但实际面试会有让你手写Promise.all或者Promise.race类似的手写题,后续我也会把这些手写问题给补全,那么到这里本文结束。

推荐阅读

超耐心地毯式分析,来试试这道看似简单但暗藏玄机的Promise顺序执行题

一个思路搞定三道Promise并发编程题,手摸手教你实现一个Promise限制器

强化Promise理解,从零手写属于自己的Promise.all与Promise.race

伍 ❀ 参考

从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节

【V8源码补充篇】从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节

promise.then 中 return Promise.resolve 后,发生了什么?

[要就来45道Promise面试题一次爽到底](

posted on 2022-02-13 19:41  听风是风  阅读(1230)  评论(9编辑  收藏  举报