[深入理解ES6]Promise与异步编程

异步编程的背景

JS引擎建立在单线程事件循环的概念上,即同一时刻只能执行一段代码,无须留意那些“可能”运行的代码。代码会被放置在作业队列( job queue )中,每当一段代码准备被执行,它就会被添加到作业队列。当 JS 引擎结束当前代码的执行后,事件循环就会执行队列中的下一个作业。事件循环(event loop )是 JS 引擎的一个内部处理线程,能监视代码的执行并管理作业队列。要记住既然是一个队列,作业就会从队列中的第一个开始,依次运行到最后一个。

事件模型

let button = document.getElementById("my-btn");
button.onclick = function(event) {
  console.log("Clicked");
};

当用户点击一个按钮或按下键盘上的一个键时,一个事件( event )——例如 onclick ——就被触发了。该事件可能会对此交互进行响应,从而将一个新的作业添加到作业队列的尾部。这就是 JS 关于异步编程的最基本形式。事件处理程序代码直到事件发生后才会被执行,此时它会拥有合适的上下文。
事件可以很好地工作于简单的交互,但将多个分离的异步调用串联在一起却会很麻烦,因为必须追踪每个事件的事件对象(例如上例中的 button )。

回调模式

当 Node.js 被创建时,它通过普及回调函数编程模式提升了异步编程模型。回调函数模式类似于事件模型,因为异步代码也会在后面的一个时间点才执行。不同之处在于需要调用的函数(即回调函数)是作为参数传入的,如下所示:

readFile("example.txt", function(err, contents) {
  if (err) {
    throw err;
  }
  console.log(contents);
});
console.log("Hi!");

此例使用了 Node.js 惯例,即错误优先( error-first )的回调函数风格。 readFile() 函数用于读取磁盘中的文件(由第一个参数指定),并在读取完毕后执行回调函数(即第二个参数)。如果存在错误,回调函数的 err 参数会是一个错误对象;否则 contents 参数就会以字符串形式包含文件内容。
使用回调函数模式, readFile() 会立即开始执行,并在开始读取磁盘时暂停。这意味着console.log("Hi!") 会在 readFile() 被调用后立即进行输出,要早于console.log(contents) 的打印操作。当 readFile() 结束操作后,它会将回调函数以及相关参数作为一个新的作业添加到作业队列的尾部。在之前的作业全部结束后该作业才会执行。
回调函数模式要比事件模型灵活得多,因为使用回调函数串联多个调用会相对容易。例如:

readFile("example.txt", function(err, contents) {
  if (err) {
    throw err;
  }
  writeFile("example.txt", function(err) {
    if (err) {
      throw err;
    }
  console.log("File was written!");
  });
});

代码就不解释了,但如果一直这样下去,就会出现回调地狱(callback hell)。如:

method1(function(err, result) {
  if (err) {
    throw err;
  }
  method2(function(err, result) {
     if (err) {
      throw err;
     }
     method3(function(err, result) {
      if (err) {
        throw err;
      }
        method4(function(err, result) {
          if (err) {
            throw err;
          }
            method5(result);
        });
    });
  });
});

要是你想让两个异步操作并行运行,并且在它们都结束后提醒你,那该怎么做?要是你想同时启动两个异步操作,但只采用首个结束的结果,那又该怎么做?此时Promise出现了。

Promise基础

Promise 是为异步操作的结果所准备的占位符。函数可以返回一个 Promise,而不必订阅一个事件或向函数传递一个回调参数,就像这样:

// readFile 承诺会在将来某个时间点完成
let promise = readFile("example.txt");

在此代码中, readFile() 实际上并未立即开始读取文件,这将会在稍后发生。此函数反而会返回一Promise 对象以表示异步读取操作,因此你可以在将来再操作它。你能对结果进行操作的确切时刻,完全取决Promise 的生命周期是如何进行的。

Promise的生命周期

每个 Promise 都会经历一个短暂的生命周期,初始为挂起态( pending state),这表示异步操作尚未结束。一个挂起的 Promise 也被认为是未决的( unsettled )。上个例子中的Promise 在 readFile() 函数返回它的时候就是处在挂起态。一旦异步操作结束, Promise就会被认为是已决的( settled ),并进入两种可能状态之一:

  1. 已完成( fulfilled ): Promise 的异步操作已成功结束;
  2. 已拒绝( rejected ): Promise 的异步操作未成功结束,可能是一个错误,或由其他原因导致。

内部的 [[PromiseState]] 属性会被设置为 "pending" 、 "fulfilled" 或 "rejected" ,以反映 Promise 的状态。该属性并未在 Promise 对象上被暴露出来,因此你无法以编程方式判断 Promise 到底处于哪种状态。不过你可以使用 then() 方法在 Promise 的状态改变时执行一些特定操作。

译注:相关词汇翻译汇总

  1. pending :挂起,表示未结束的 Promise 状态。相关词汇“挂起态”。
  2. fulfilled :已完成,表示已成功结束的 Promise 状态,可以理解为“成功完成”。相关词汇“完成”、“被完成”、“完成态”。
  3. rejected :已拒绝,表示已结束但失败的 Promise 状态。相关词汇“拒绝”、“被拒绝”、“拒绝态”。
  4. resolve :决议,表示将 Promise 推向成功态,可以理解为“决议通过”,在 Promise概念中与“完成”是近义词。相关词汇“决议态”、“已决议”、“被决议”。
  5. unsettled :未决,或者称为“未解决”,表示 Promise 尚未被完成或拒绝,与“挂起”是近义词。
  6. settled :已决,或者称为“已解决”,表示 Promise 已被完成或拒绝。注意这与“已完成”或“已决议”不同,“已决”的状态也可能是“拒绝态”(已失败)。
  7. fulfillment handler :完成处理函数,表示 Promise 为完成态时会被调用的函数。
  8. rejection handler :拒绝处理函数,表示 Promise 为拒绝态时会被调用的函数。

then() 方法在所有的 Promise 上都存在,并且接受两个参数。第一个参数是 Promise 被完成时要调用的函数,与异步操作关联的任何附加数据都会被传入这个完成函数。第二个参数则是 Promise 被拒绝时要调用的函数,与完成函数相似,拒绝函数会被传入与拒绝相关联的任何附加数据。

let promise = readFile("example.txt");
promise.then(function(contents) {
  // 完成
  console.log(contents);
}, function(err) {
  // 拒绝
  console.error(err.message);
});
promise.then(function(contents) {
  // 完成
  console.log(contents);
});
promise.then(null, function(err) {
  // 拒绝
  console.error(err.message);
});

每次调用 then() 或 catch() 都会创建一个新的作业,它会在 Promise 已决议时被执行。但这些作业最终会进入一个完全为 Promise 保留的作业队列。这个独立队列的确切细节对于理解如何使用 Promise 是不重要的,你只需理解作业队列通常来说是如何工作的。

创建未决的Promise

新的 Promise 使用 Promise 构造器来创建。此构造器接受单个参数:一个被称为执行器(executor )的函数,包含初始化 Promise 的代码。该执行器会被传递两个名为 resolve()与 reject() 的函数作为参数。 resolve() 函数在执行器成功结束时被调用,用于示意该Promise 已经准备好被决议( resolved ),而 reject() 函数则表明执行器的操作已失败。
对比以下两个例子:

let promise = new Promise(function(resolve, reject) {
  console.log("Promise");
  resolve();
});
console.log("Hi!");

Promise
Hi!
let promise = new Promise(function(resolve, reject) {
  console.log("Promise");
  resolve();
});
promise.then(function() {
  console.log("Resolved.");
});
console.log("Hi!");

Promise
Hi!
Resolved

注意:尽管对 then() 的调用出现在 console.log("Hi!") 代码行之前,它实际上稍后才会执行(与执行器中那行 "Promise" 不同)。这是因为完成处理函数与拒绝处理函数总是会在执行器的操作结束后被添加到作业队列的尾部。

创建已决的Promise

使用Promise.resolve()

Promise.resolve()方法接受单个参数并返回一个完成状态的Promise。

let promise = Promise.resolve('test');
promise.then(function(value){
  console.log(value)
})

使用Promise.reject()

此方法和Promise.resolve()一样,区别是被创建的Promise处于拒绝状态。

let promise = Promise.reject(42)

promise.catch(function(value){
  console.log(value)
})

任何附加到这个Promise的拒绝处理函数都将会被调用,而完成处理函数则不会执行。

对挂起态或完成态的Promise使用Promise.resolve()没问题,会返回原Promise;对拒绝态的promise使用Promise.resolve()也没问题。而除此之外的情况都会在原Promise上包装出一个新的Promise。
这句活需要反复琢磨!!!在实战中还未遇到类似的问题。

非Promise的Thenable

在 Promise 被引入 ES6 之前,许多库都使用了 thenable ,因此将 thenable 转换为正规 Promise 的能力就非常重要了,能对之前已存在的库提供向下兼容。当你不能确定一个对象是否是 Promise 时,将该对象传递给 Promise.resolve() 或 Promise.reject() (取决于你的预期结果)是能找出的最好方式,因为传入真正的 Promise 只会被直接传递出来,并不会被修改。

全局的Promise拒绝处理

Promise 最有争议的方面之一就是:当一个 Promise 被拒绝时若缺少拒绝处理函数,就会静默失败。有人认为这是规范中最大的缺陷,因为这是 JS 语言所有组成部分中唯一不让错误清晰可见的。
由于 Promise 的本质,判断一个 Promise 的拒绝是否已被处理并不直观。例如,研究以下示例:

let rejected = Promise.reject(42)

// 此时rejected不会被处理

// 一段时间后
rejected.catch(function(value){
  // 现在rejected已经被处理了
  console.log(value)
})

无论 Promise 是否已被解决,你都可以在任何时候调用 then() 或 catch() 并使它们正确工作,这导致很难准确知道一个 Promise 何时会被处理。此例中的 Promise 被立刻拒绝,但它后来才被处理。
虽然下个版本的 ES 可能会处理此问题,不过浏览器与 Node.js 已经实施了变更来解决开发者的这个痛点。这些变更不是 ES6 规范的一部分,但却是使用 Promise 时的宝贵工具。

Node.js的拒绝处理

偏理论,待写...

浏览器的拒绝处理

偏理论,待写...
以上部分偏理论多一些,下面的注重实战...

串联Promise

捕获错误

在Promise链中返回值

在Promise链中返回Promise

响应多个Promise

Promise.all()方法

Promise.race()方法

继承Promise

异步任务运行

总结

你的赞是我前进的动力

求赞,求评论,求转发...

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

推荐阅读更多精彩内容