📒【异步】4. 异步方案之Promise

2020-07-29  本文已影响0人  BubbleM

Promise

Promise对象是一个代理对象。它接受你传入的 executor (执行器)作为入参,允许你把异步任务的成功和失败分别绑定到对应的处理方法上。一个 Promise 实例有三种状态

Promise解决的痛点

对于回调地狱的引发的问题,我们需要一种更加友好的代码组织方式,解决异步嵌套的问题。
于是 Promise 规范诞生了,并且在业界有了很多实现来解决回调地狱的痛点。比如业界著名的 Qbluebirdbluebird 甚至号称运行最快的类库。
Promise对象现已在ECMAScript 2015中作为JavaScript的标准内置对象提供,这个对象根据 Promise A+ 规范实现。(Promise规范有很多,如Promise/A,Promise/B,Promise/D 以及 Promise/A的升级版 Promise/A+,最终ES6采用了Promise/A+规范)

  1. 回调嵌套 -> 理解问题,缺乏顺序性
new Promise(请求1)
    .then(请求2(请求结果1))
    .then(请求3(请求结果2))
    .then(请求4(请求结果3))
    .then(请求5(请求结果4))
    .catch(处理异常(异常信息))

对比Promise写法和嵌套回调写法,Promise链以顺序的方式表达异步流,有助于我们的大脑更好的计划和维护异步JavaScript代码,并且能够在外层捕获异步函数的异常信息。

  1. 控制反转 -> 信任问题

如果我们能够把控制反转再反转回来,会怎样呢?如果我们不把自己程序的continuation传给第三方,而是希望第三方给我们提供了解其任务何时结束的能力,然后由我们自己的代码来决定下一步做什么,那将会怎样呢?这种范式就称为Promise。

Promise封装了依赖于时间的状态——等待底层值的完成或拒绝,所以Promise本身是与时间无关的。因此,Promise可以按照可预测的方式组合,而不用关心时序或者底层的结果。
Promise是一种封装和组合未来值的易于复用的机制。

Promise的决议也可以看做是一种在异步任务中作为两个或更多步骤的流程控制机制。

⛲️ 场景:要调用一个函数foo()执行某个任务,期望通过某种方式在foo()执行完成时得到通知
🤔️ 思考:在典型的JavaScript场景中,如果需要侦听某个通知,就会使用事件。需实现对foo()发出的一个完成事件的侦听。

function foo(x){
  // ...do something
  // 构造一个listener处理
  return listener;
}
var evt = foo(42);
evt.on("completion", function(){
  // 可以进行下一步
})
evt.on("failure", function(){
  // foo(..)中出错了
})

Promise模式构建的最重要的特性,就是解决了部分信任问题:

var p = new Promise(function(resolve, reject){
  resolve(42);
});
p.then(function fulfilled(msg){
  foo.bar();
  console.log(msg); // 永远不会到达这里
}, function rejected(err){
  console.log(err); // 永远不会到达这里
}).then(function fulfilled(msg){
  console.log('....'+msg); // 永远不会到达这里
}, function rejected(err){
  console.log('....')
  console.log(err); // 到达这里
})

Promise并没有完成摆脱回调,只是改变了传递回调的位置。并没有把回调传给foo(..),而是从foo(..)获得某个东西(Promise),然后把回调传给他。

🤔️ Q:为什么这就比单纯的使用回调更值得信任呢?如何确定返回的这个东西实际上就是一个可信任的Promise?
😯 A:Promise对这个问题已经有一个解决方案:原生ES6 Promise实现中的解决方案就是 Promise.resolve() 。 可接受任何thenable,得到一个真正的Promise。如果传入的已经是真正的Promise,将得到其本身。

Promise常见方法及其作用

类方法

JavaScript中的类(对象)方法可以认为是静态方法(即:不需要实例化就可以使用的方法)

  1. Promise.all(iterable):这个方法返回一个新的 promise 对象,该 promise 对象在 iterable 参数对象里所有的 promise 对象都成功的时候才会触发成功,一旦有任何一个 iterable 里面的 promise 对象失败则立即触发该 promise 对象的失败。
  2. Promise.race(iterable):当 iterable 参数里的任意一个子 promise 被成功或失败后,父 promise 马上也会用子 promise 的成功返回值或失败详情作为参数调用父 promise 绑定的相应处理函数,并返回该 promise 对象。
  3. Promise.reject(reason): 返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法。
  4. Promise.resolve(value):它返回一个 Promise 对象,但是这个对象的状态由你传入的value决定,情形分以下三种:
// 如果传入的 value 本身就是 Promise 对象,则该对象作为 Promise.resolve 方法的返回值返回。  
function fn(resolve){
    setTimeout(function(){
        resolve(123);
    },3000);
}
let p0 = new Promise(fn);
let p1 = Promise.resolve(p0);

console.log(p0 === p1); // 返回为true,返回的 Promise 即是 入参的 Promise 对象。

实例方法

实例方法,是指创建Promise实例后才能使用的方法,即:被添加到原型链 Promise.prototype 上的方法。

  1. Promise.prototype.then 实例方法,为Promise注册回调,fn(value){}其中value是上一个任务的返回结果。如果我们的后续任务是异步任务的话,必须return一个新的promise对象;如果后续任务是同步任务,只需return一个结果即可。
    then 中的函数一定要 return 一个结果或者一个新的 Promise 对象,才可以让之后的then 回调接收。
  2. Promise.prototype.catch 捕获异常,可以捕获到前面回调中可能抛出的异常。

Promise A+规范规定:每个Promise实例中返回的都应该是一个Promise实例或thenable对象。基于这个特性,能够实现类似于同步的链式调用。

new Promise((resolve, reject) => {
  // a()
  resolve(2) 
}).then(value => {
  a();
  console.log(value);
}).catch(err => {
  console.log('..................'); // 可以捕获到
  console.log('err:', err);
})

异常捕获

对于多数开发者来说,错误处理最自然的形式就是同步的try..catch结构。遗憾的是,它只能是同步的,无法用于异步代码模块。

var p = Promise.resolve(42);
p.then(function fulfilled(msg){
    console.log(msg.toLowerCase); // 数字没有string函数,会抛错
})
.catch( handleErrors )

因为我们没有为then(..)传入拒绝处理函数,所以默认的处理函数被替换掉了,而这仅仅是把错误传递给了链中的下一个Promise。因此,进入p的错误以及p之后进入其决议的错误都会传递到最后的handleErrors(...)

🤔️ Q:如果handleErrors本身内部也有错误怎么办呢?
😯 A:浏览器有一个特定的功能是我们的代码所没有的,它们可以跟踪并了解所有对象被丢弃以及垃圾回收的机制。所以,浏览器可以追踪Promise对象。如果在它被垃圾回收的时候其中有拒绝,浏览器就能够确保这是一个真正的未捕获错误,进而可以确定应该将其报告到开发者终端。

Promise的使用

  1. 例1:
const promise = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
    console.log(2);
});
promise.then(() => {
    console.log(3);
});
console.log(4);
// 1 2 4 3
  1. 例2
const promise = new Promise((resolve, reject) => {
  resolve('第 1 次 resolve')
  console.log('resolve后的普通逻辑')
  reject('error')
  resolve('第 2 次 resolve')
})
promise
.then((res) => {
  console.log('then: ', res)
})
.catch((err) => {
  console.log('catch: ', err)
})
// resolve后的普通逻辑
// then:  第 1 次 resolve

Promise 对象的状态只能被改变一次。 我们忽略的是第一次 resolve 后的 reject、resolve,而不是忽略它身后的所有代码。因此 console.log(‘resolve后的普通逻辑’) 这句,仍然可以正常被执行。

  1. 例3 值穿透问题
Promise.resolve(1)
  .then(Promise.resolve(2))
  .then(3)
  .then()
  .then(console.log)
// 1

then 方法里允许我们传入两个参数:onFulfilled(成功态的处理函数)和 onRejected(失败态的处理函数)。
可以两者都传,也可以只传前者或者后者。但是无论如何,then 方法的入参只能是函数,其他都会被忽略。
在这个过程中,我们最初 resolve 出来那个值,穿越了一个又一个无效的 then 调用,就好像是这些 then 调用都是透明的、不存在的一样,因此这种情形我们也形象地称它是 Promise 的“值穿透”

手写一个Promise的 polyfill

精简版

function CutePromise(executor){
  this.value = null; //记录异步任务成功的执行结果
  this.reason = null; //记录异步任务失败的原因
  this.status = 'pending'; //记录当前的状态 初始化为pending

  //缓存两个队列,维护resolved和rejected各自对应的处理函数
  this.onResolvedQueue = [];
  this.onRejectedQueue = [];

  var self = this;

  function resolve(value){
    if(self.status !== 'pending'){
      return;
    }
    self.value = value;
    self.status = 'resolved';
    //用setTimeout延迟队列任务的执行
    setTimeout(function(){
      self.onResolvedQueue.forEach(resolved => resolved(self.value));
    })
  }
  function reject(reason){
    if(self.status !== 'pending'){
      return;
    }
    self.reason = reason;
    self.status = 'rejected';
    setTimeout(function(){
      self.onRejectedQueue.forEach(rejected => rejected(self.reason));
    })
  }
  
  //把 resolve 和 reject 能力赋予执行器
  executor(resolve, reject);
}

CutePromise.prototype.then = function(onResolved, onRejected){
  if(typeof onResolved !== 'function'){
    onResolved = function(x){ return x };
  }
  if(typeof onRejected !== 'function'){
    onRejected = function(e){ throw e };
  }

  var self = this;
  if(self.status === 'resolved'){
    onResolved(self.value);
  }else if(self.status === 'rejected'){
    onRejected(self.reason);
  }else if(self.status === 'pending'){
    //如果是pending状态,则只对任务做入队列处理
    self.onResolvedQueue.push(onResolved);
    self.onRejectedQueue.push(onRejected);
  }
  return this; //链式调用  ⚠️ 真实的场景是返回一个新的Promise实例
}

new CutePromise(function(resolve, reject){
  resolve('成了!');
}).then((value) => {
  console.log(value)
  console.log('我是第 1 个任务')
  return '第一个任务的结果'
}).then(value => {
  console.log(value);
  console.log('我是第 2 个任务')
});
// 依次输出“成了!” “我是第 1 个任务” “我是第 2 个任务

其他

阮一峰ES6-Promise
Promise A+规范
Promise代码题
ES6系列之聊聊Promise

上一篇 下一篇

猜你喜欢

热点阅读