likes
comments
collection
share

新手也能看懂的Promise原理,超简单!不来看一下吗?(续)想真正明白Promise的原理?那就来看这篇文章吧,新手也

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

5 修复then回调异步执行

 _Promise.prototype.then = function (onResolved, onRejected) {
   const _self = this;
   setTimeout(() => {
     return new _Promise((resolve, reject) => {
       // 省略代码
     });
   });
 };

但其实这样是有问题的,我们就用这个加上了setTimeout的_Promise.prototype.then来测试一下,还是上节的测试代码:

 const p0 = new _Promise((resolve) => {
   console.log(1);
   resolve(3);
 });
 ​
 p0.then((val) => {
   console.log(val);
 });
 ​
 console.log(2);

输出结果如下:

新手也能看懂的Promise原理,超简单!不来看一下吗?(续)想真正明白Promise的原理?那就来看这篇文章吧,新手也

唉!这不是输出1 2 3了吗?没问题呀!!别急,我们换下面这个测试用例:

 setTimeout(() => console.log(4));
 ​
 const p0 = new Promise((resolve) => {
   console.log(1);
   resolve(3);
 });
 ​
 p0.then((val) => {
   console.log(val);
 });
 ​
 console.log(2);

我们在最开始时加入了setTimeout(() => console.log(4)),原生的Promise输出如下:

新手也能看懂的Promise原理,超简单!不来看一下吗?(续)想真正明白Promise的原理?那就来看这篇文章吧,新手也

然后换我们给_Promise.prototype.then加了setTimeout()进行验证,看看输出:

新手也能看懂的Promise原理,超简单!不来看一下吗?(续)想真正明白Promise的原理?那就来看这篇文章吧,新手也

它输出的是1 2 4 3

这里就涉及到浏览器的事件循环了,咱们简单提一下,在浏览器的事件循环中,每次执行完一个宏任务后,会清空微任务队列,之后再遍历延时队列按到期时间执行。

这里涉及到三个队列,宏任务队列、微任务队列、延时任务队列。原生的Promise.then的执行,是放在微任务队列里的,而setTimeout是放在延时任务队列里的。如果我们的_Promise.prototype.then里是用setTimeout包裹的话,那么它就是放在延迟任务队列里去执行了。延时队列是根据延时时间来定的,两个延时时间一样,就按照声明的位置前后来定,setTimeout(() => console.log(4))的声明要更早,所以它会先执行。

明确了问题,那就好改了,将_Promise.prototype.then里包裹的return语句放在微任务队列里就好了,我们可以用queueMicrotask()方法将then返回的内容放在微任务队列中,具体改造如下:

 _Promise.prototype.then = function (onResolved, onRejected) {
   const _self = this;
   queueMicrotask(() => {
     return new _Promise((resolve, reject) => {
       // 其他代码
     });
   });
 };

这样改造之后,输出就是1 2 3 4了,这里就不截图了,大家可以试试。

6.Promise.prototype.catch

实现了Promisethen方法,那么其他方法就比较简单了。我们先看看catch方法:

老规矩,我们先上原生Promise的catch方法,看看catch的用法:

 const p0 = new Promise((resolve, reject) => reject('报错'));
 ​
 p0.then(
   (val) => {
     console.log('onResolved', val);
   },
   (err) => {
     console.log('onRejected', err);
     throw Error('onRejected 抛出异常');
   }
 ).catch((err) => {
   console.log(err);
 });

上述代码输出如下:(下面的输出信息我把报错的文件位置等信息去掉了)

 onRejected Error: new时报错 
 Error: onRejected 抛出异常                        

在上面的示例代码中,我们用new一个p0Promise对象,并在new的时候抛出一个错误,结果我们在p0.then(onResolved, onRejected)onRejected里接受到了这个错误。接着,我们在onRejected里抛出了另一个错误,结果,我们如期在catch里面捕捉到了这个错误。

所以,我们发现,catch方法其实是一个没有第一个入参的then方法,具体来说,就是Promise.prototype.catch()方法是.then(null, onRejected).then(undefined, onRejected)的别名。明白了这点,那我们实现catch方法的时候完全可以复用then方法:

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

我们来测试一下:

  const p = new _Promise((resolve, reject) => {
    reject("err");
  });
  
  p.catch((res) => {
    console.log(res);
  });
  // 

上述代码如期输出err,这样,我们就又实现了catch方法。

7.Promise.race

race方法将多个 Promise 实例,包装成一个新的 Promise 实例。率先改变状态的Promise实例,其返回值将传回给新生成的Promise实例,并且新生成的Promise实例的状态跟率先改变状态的Promise实例的状态一致。

这么说可能有点饶,我们来看个例子:

 const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

不知道大家有没有注意到,这里的race方法我们是直接用Promise调用的,而不是通过实例化后的对象进行调用。所以,这个race方法不是实例的方法,我们不能定义在原型链上。

明白race的作用机制,接下来我们就可以开始实现race了。

首先,我们根据第一点,race方法返回一个新的Promise实例,可以写出如下代码:

 _Promise.race = function (promiseArray) {
   return new _Promise((resolve, reject) => {});
 }

其次,率先改变状态的Promise实例,其返回值将传给新的Promise实例。率先这个词要怎么去理解呢?其实很简单,我们遍历这些传入的Promise,然后在每个Promise的then的回调函数里,就能拿到其返回值了。根据这一点,我们可以写出如下代码:

 _Promise.race = function (promiseArray) {
   return new _Promise((resolve, reject) => {
     promiseArray.forEach(p => {
       p.then((val) => {}, (err) => {});
     })
   });
 }

在第5行中,我们通过p.then((val) => {}, (err) => {})可以拿到传进来的每个Promise实例的返回值,不仅如此,我们还知道每个Promise实例的状态是resolved还是rejected:在then的第一个函数参数,该Promise实例的状态肯定是resolved,第二个函数参数里,状态肯定是rejected

因此,要想做到race方法新生成的Promise实例的状态,跟传进来的Promise数组中、最先改变状态的Promise实例的状态保持一致,就直接在then方法里执行resolve或者reject就好了:

 _Promise.race = function (promiseArray) {
   return new _Promise((resolve, reject) => {
     promiseArray.forEach(p => {
       p.then((val) => resolve(val), (err) => reject(err));
     })
   });
 }

我们在第4行代码p.then((val) => resolve(val), (err) => reject(err))中,如果p的状态变成resolved了,就会进入到第一个函数参数里,然后在这里我们就可以将race方法新生成的Promise实例的状态resolve掉,同理,p的状态变成rejected时,就将新生成的Promise实例rejected掉。

另外,第四行代码可以简写成:p.then(resolve, reject),最终race方法就变成下面这样:

 _Promise.race = function (promiseArray) {
   return new _Promise((resolve, reject) => {
     promiseArray.forEach(p => {
       p.then(resolve, reject);
     })
   });
 }

这个简写就有点考验大家对于then方法里第一个入参onResolved和第二个入参onRejected的理解了,如果大家有疑惑的,就再回头看一下then方法的实现,相信你一定可以明白这个简写的逻辑。

接下来我们测试一下我们实现的race方法:

 const p1 = new _Promise((resolve, reject) => {
   setTimeout(() => {
     resolve('p1');
   }, 300);
 });
 ​
 const p2 = new _Promise((resolve, reject) => {
   setTimeout(() => {
     resolve('p2');
   }, 200);
 });
 ​
 const p3 = new _Promise((resolve, reject) => {
   setTimeout(() => {
     resolve('p3');
   }, 100);
 });
 ​
 _Promise.race([p1, p2, p3]).then((res) => console.log(res));

执行上述代码后,成功输出p3,这就说明了我们的race方法的实现是正确的。

新手也能看懂的Promise原理,超简单!不来看一下吗?(续)想真正明白Promise的原理?那就来看这篇文章吧,新手也

8.all

最后一个是all方法, all方法同样将多个 Promise 实例,包装成一个新的 Promise 实例。它有以下两个机制:

  1. 所有的Promise实例的状态都resolved后,其返回值组成的数组才传回给回调函数,并且返回的Promise实例的状态也会变成resolved
  2. 或者,传入的多个 Promise 实例中,只要有一个的状态是rejected,新生成的Promise实例的状态也会变成rejected,并且,最先rejectedPromise实例,其返回值传给回调函数。

有了上节race的基础,相信你对于all的实现应该有自己的想法了,我们可以很轻松的写出下面的代码:

 _Promise.all = function (promiseArray) {
   const result = [];
   return new _Promise((resolve, reject) => {
     promiseArray.forEach((p) => {
       p.then(
         (res) => {},
         (err) => {}
       );
     });
   });
 };

因为有返回值组成的数组,所以我们在第2行定义了一个保存每个promise的执行结果的数组result,然后在all方法里返回了一个Promise实例(暂且称为p0),并且在新建这个Promise实例的时候遍历传入的Promise数组。

那么现在,只要在遍历传入的Promise数组的过程中,遇到resolved状态的实例,就将其结果存到reuslt数组中,然后判断数组的长度是否跟传入的Promise数组的长度相等,相等的话就说明传入的Promise数组都已经遍历完了,而且也有结果了,那自然就可以执行resolve函数将p0的状态改成resolved了!

而且,如果在遍历的过程中,只要有一个是rejected的状态,我们就执行reject,将p0的状态改成rejected, 这样就满足all方法的第2点机制了。 修改后的代码如下:

 _Promise.all = function (promiseArray) {
   const result = [];
   return new _Promise((resolve, reject) => {
     promiseArray.forEach((p) => {
       p.then(
         (res) => {
           result.push(res);
           if (result.length === promiseArray.length) {
             resolve(result);
           }
         },
         (err) => reject(err) // 这个箭头函数也可以直接简写成reject
       );
     });
   });
 };

这样写其实有个问题,那就是第7行代码:result.push(res),这一行代码,会导致谁先resolved谁的结果就会先被push进数组,而all方法返回的数组,其结果是跟Promise实例在promiseArray中的位置一一对应的。所以我们不能用push,而应该直接对下标赋值,修改代码如下:

 _Promise.all = function (promiseArray) {
   const result = [];
   let resolvedCount = 0;
   return new _Promise((resolve, reject) => {
     promiseArray.forEach((p, i) => {
       p.then(
         (res) => {
           result[i] = res;
           resolvedCount++;
           if (resolvedCount === promiseArray.length) {
             resolve(result);
           }
         },
         (err) => reject(err)
       );
     });
   });
 };

而又因为我们将result.push(res)改成了result[i] = res,所以我们不能根据result的长度来判断当前resolved状态的promise实例数量了,因为有可能最后一个promise实例已经resolved了,而前面的promise实例还在pending中,这时候reuslt的前面都是undefined,只有最后一个有值。

因此,我们需要增加一个计数器resolvedCount,最后也是用计数器跟promiseArray.length去做比较,来判断当前是不是所以的promise实例的状态都变成resolved了。

接下来,我们测试一下all方法,测试用例如下:

 const p1 = new _Promise((resolve, reject) => {
   setTimeout(() => {
     resolve('p1');
   }, 300);
 });
 ​
 const p2 = new _Promise((resolve, reject) => {
   setTimeout(() => {
     resolve('p2');
   }, 200);
 });
 ​
 const p3 = new _Promise((resolve, reject) => {
   setTimeout(() => {
     resolve('p3');
   }, 100);
 });
 ​
 _Promise.all([p1, p2, p3]).then((res) => console.log(res));

诶!没错,还是这三个Promise实例,只不过我们最后调用的是all方法。其输出结果如下:

新手也能看懂的Promise原理,超简单!不来看一下吗?(续)想真正明白Promise的原理?那就来看这篇文章吧,新手也

非常好,很符合我们的预期。大家也可以用result.push(res)而不是result[i] = res来进行测试,以此来加深印象。放个完整代码如下:

 function _Promise(executor) {
   const _self = this;
   _self.status = 'pending';
   _self.value = undefined; // 保存resolve函数的入参
   _self.reason = undefined; // 保存reject函数的入参
   _self.onResolvedCallbacks = []; // 保存pending状态下的onResolved函数
   _self.onRejectedCallbacks = []; // 保存pending状态下的onRejected函数
 ​
   function resolve(res) {
     if (_self.status === 'pending') {
       _self.status = 'resolved';
       _self.value = res;
       // 一旦 resolve 执行,遍历执行已收集到的 onResolved 回调函数,并重置 onResolvedCallbacks
       _self.onResolvedCallbacks.forEach((fn) => fn());
       _self.onResolvedCallbacks = [];
     }
   }
 ​
   function reject(err) {
     if (_self.status === 'pending') {
       _self.status = 'rejected';
       _self.reason = err;
       // 一旦 reject 执行,遍历执行已收集到的 onRejectedCallbacks 回调函数,并重置 onRejectedCallbacks
       _self.onRejectedCallbacks.forEach((fn) => fn());
       _self.onRejectedCallbacks = [];
     }
   }
 ​
   try {
     executor(resolve, reject);
   } catch (e) {
     reject(e);
   }
 }
 ​
 _Promise.prototype.then = function (onResolved, onRejected) {
   const _self = this;
   queueMicrotask(() => {
     return new _Promise((resolve, reject) => {
       const callback = (fn) => {
         try {
           const result = fn(_self.value || _self.reason);
           if (result instanceof _Promise) {
             // prettier-ignore
             result.then((res) => resolve(res), (err) => reject(err));
             return;
           }
           resolve(result);
         } catch (e) {
           reject(e);
         }
       };
 ​
       switch (_self.status) {
         case 'resolved':
           callback(onResolved);
           break;
         case 'rejected':
           callback(onRejected);
         case 'pending':
           _self.onResolvedCallbacks.push(() => callback(onResolved));
           _self.onRejectedCallbacks.push(() => callback(onRejected));
           break;
         default:
           break;
       }
     });
   });
 };
 ​
 _Promise.prototype.catch = function (onRejected) {
   return this.then(null, onRejected);
 };
 ​
 _Promise.race = function (promiseArray) {
   return new _Promise((resolve, reject) => {
     promiseArray.forEach((p) => {
       p.then(resolve, reject);
     });
   });
 };
 ​
 _Promise.all = function (promiseArray) {
   const result = [];
   let count = 0;
   return new _Promise((resolve, reject) => {
     promiseArray.forEach((p, i) => {
       p.then(
         (res) => {
           result[i] = res;
           count++;
           if (count === promiseArray.length) {
             resolve(result);
           }
         },
         (err) => reject(err)
       );
     });
   });
 };

刚好100行~我们实现了Promise构造函数、实现了Promise.prototype.then、实现了Promise.prototype.catch,还实现了Promise.racePromise.all方法,平均每个行数也就25行左右,非常简单!!!

以上就是关于Promise原理的全部内容了,如果对你有帮助的话,欢迎点赞收藏哦 !!如果有问题的话,也可以在评论区留言,我看到会及时回复的~

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