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

Promise

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

  • pending 状态,表示进行中。这是 Promise 实例创建后的一个初始态;
  • fulfilled 状态,表示成功完成。这是我们在执行器中调用 resolve 后,达成的状态;
  • rejected 状态,表示操作失败、被拒绝。这是我们在执行器中调用 reject后,达成的状态;
    【状态切换机制】Promise实例的状态是可以改变的,但它只允许被改变一次。 当我们的实例状态从 pending 切换为 rejected 后,就无法再扭转为 fulfilled,反之同理。当 Promise 的状态为 resolved 时,会触发其对应的 then 方法入参里的 onfulfilled 函数;当 Promise 的状态为 rejected 时,会触发其对应的 then 方法入参里的 onrejected 函数。

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()发出的一个完成事件的侦听。

  • 使用回调,通知就是任务(foo(...))调用的回调。
  • 使用Promise,这个关系就反转过来了,侦听来自foo(..)的事件,然后在得到通知的时候做相关处理。
  • 显式地创建并返回一个事件订阅对象:
function foo(x){
  // ...do something
  // 构造一个listener处理
  return listener;
}
var evt = foo(42);
evt.on("completion", function(){
  // 可以进行下一步
})
evt.on("failure", function(){
  // foo(..)中出错了
})

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

  • 调用过早:即使这个Promise已经决议,提供给then(..)的回调也总会被异步调用。不需要再插入setTimeout(.., 0) hack,Promise会自动防止Zalgo出现。
  • 调用过晚:Promise创建对象调用resolve()或reject()时,这个Promise的then(..)注册的观察回调就会被自动调度。也就是说,一个Promise决议后,这个Promise上所有通过then(..)注册的回调都会在下一个异步时机点上依次被立即调用。
  • 回调未调用 & 调用次数过少(0次):如果你对一个Promise注册了一个完成回调和一个拒绝回调,那么Promise在决议时总是会调用其中的一个。即使回调本身包含JavaScript错误,也不会被吞掉。
    如果Promise本身永远不被决议,Promise提供超时模式来解决。
  • 调用次数过多(>1次):Promise的定义方式使得其只能被决议一次,所有通过then(..)注册的回调都只会被调一次。Promise只接受第一次决议,并默默地忽略任何后续调用。
  • 未能传递参数/环境值:如果没有用任何值显式决议,那这个值就是undefined,会被传给所有注册的回调。
  • 吞掉错误或异常:在Promise创建过程或查看其决议结果过程中的任何时间点上出现JavaScript异常,这个异常都会被捕获,并且会使这个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决定,情形分以下三种:
  • 如果传入的是一个带有 then 方法的对象(我们称为 thenable 对象),返回的Promise对象会跟随这个thenable对象,采用它的最终状态( resolved/rejected/pending/settled);
  • 如果传入的value本身就是Promise对象,则该对象作为Promise.resolve方法的返回值返回;
  • 其他情况以该值为成功状态返回一个Promise对象;
// 如果传入的 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结构。遗憾的是,它只能是同步的,无法用于异步代码模块。

  • error-firset回调设计风格错误处理:多级error-first回调交织在一起,再加上if检查语句,很容易引发回调地狱的风险
  • 分离回调风格错误处理:接收两个参数,一个回调用于完成情况,一个回调用于拒绝情况(非必填)。
    分离回调风格的错误错误易于出错,如果没有传入第二个拒绝回调,非常容易造成错误被吞掉。
    为了避免丢失被忽略和抛弃的Promise错误,一些开发者表示Promise链的一个最佳实践就是最后总以一个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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271