深入理解ES6:11.Promise 与异步编程

异步编程的背景知识

JavaScript 引擎是基于单线程(Single-threaded)事件循环的概念构建的。

JavaScript 引擎同一时刻只能执行一个代码块,每当一段代码准备执行时,都会被添加到一个任务队列(job queue)中,以跟踪即将运行的代码。每当 JavaScript 引擎中的一段代码结束执行,事件循环(event loop)会执行队列中的下一个任务,它是 JavaScript 引擎中的一段程序,负责监控代码执行并管理任务队列。请记住,队列中的任务会从第一个一直执行到最后一个。

事件模型

事件模型会向任务队列添加一个新任务来响应用户的操作,当指定的事件被触发时,执行事件处理程序。

// 将 onclick() 函数添加到任务队列中,单击 button 后,就会执行相应的代码!
let button = document.getElementById('my-btn');
button.onclick = function (event) {
  console.log('Clicked');
};

必须要保证事件在添加事件处理程序之后才被触发。(即,必须先将事件添加到任务队列中,才能执行该事件)。

回调模式

回调模式与事件模型类似,异步代码会在未来的某个时间点被执行,二者的区别是:回调模式中被调用的函数是作为参数传入的

相比之下,回调模式比事件模型更灵活,因为它可以嵌套多个回调函数,但也同时会产生回调地狱问题。

Node.js 中的错误优先(error-first)风格示例代码:

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

console.log('Hi!');

Promise 的基础知识

Promise 相当于异步操作结果的占位符,它会让函数返回一个 Promise

Promise 的生命周期

每个 Promise 都会经历一个短暂的生命周期:先是处于进行中(pending)的状态,此时操作尚未完成,所以它也是未处理(unsettled)的;一旦异步操作执行结束, Promise 则变为已处理( settled)的状态。操作结束后, Promise 可能会进入到以下两个状态中的其中一个:

  • FulfilledPromise 异步操作成功完成。
  • Rejected:由于程序错误或一些其他原因, Promise 异步操作未能成功完成。

内部属性 [[Promisestate]] 被用来表示 Promise 的 3 种状态:"pending"、"fulfilled"及"rejected"。这个属性不暴露在 Promise 对象上,所以不能以编程的方式检测 Promise 的状态,只有当 Promise 的状态改变时,通过 then() 方法来采取特定的行动。

所有 Promise 都有 then() 方法,它接受两个可选参数

  • 参数一:当 Promise 的状态变为 fulfilled 时要调用的函数,与异步操作相关的附加数据都会传递给这个完成函数(fulfilled function);
  • 参数二:当 Promise 的状态变为 rejected 时要调用的函数,其与完成时调用的函数类似,所有与失败状态相关的附加数据都会传递给这个拒绝函数(rejected function)。

then() 的两个参数都是可选的,所以你可以按照任意组合的方式来监听 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);
});

Promisecatch() 方法相当于只给其传入拒绝处理程序的 then() 方法。

promise.catch(function (err) {
  console.error(err.message);
});

// 与以下调用相同
promise.then(null, function(err) {
  // 拒绝
  console.error(err.message);
});

创建未完成的 Promise

Promise 构造函数可以创建新的 Promise,构造函数只接受一个参数:包含初始化 Promise 代码的执行器(executor)函数。执行器接受两个参数,分别是 resolve() 函数和 reject() 函数。执行器成功完成时调用 resolve() 函数,反之,失败时则调用 reject() 函数。

let fs = require('fs');

function readFile(filename) {
  return new Promise(function(resolve, reject) {
    // 触发异步操作
    fs.readFile(filename, { encoding: 'utf8' }, function(err, contents) {
      // 检查是否有错误
      if (err) {
        reject(err);
        return;
      }

      // 成功读取文件
      resolve(contents);
    });
  });
}

let promise = readFile('example.txt');

// 同时监听【执行完成】和【执行拒绝】
promise.then(
  function(contents) {
    // 完成
    console.log(contents);
  },
  function(err) {
    // 拒绝
    console.error(err.message);
  }
);

readFile() 方法被调用时执行器会立刻执行,在执行器中,无论是调用 resolve() 函数和 reject() 函数,都会向任务队列中添加一个任务来解决这个 Promise

let promise = new Promise(function (resolve, reject) {
  console.log('Promise'); // 1
  resolve();
  // 调用 resolve() 方法后会触发一个异步操作,
  // 传入 then() 和 catch() 方法的函数会被添加到任务队列中并异步执行。
})

// 3
promise.then((result) => {
  console.log(result);
}).catch((err) => {
  console.error(err.message);
});

console.log('Hi'); // 2

/*
输出内容:

Promise
Hi
result 或者 err.message
*/

创建已处理的 Promise

如何用 Promise 来表示一个已知值???

使用 Promise.resolve()Promise.reject() 方法来根据特定的值来创建已解决 Promise

使用 Promise.resolve()

Promise.resolve() 方法只接受一个参数并返回一个完成态的 Promise,也就是说不会有任务编排的过程,而且需要向 Promise 添加一至多个完成处理程序来获取值。

let promise = Promise.resolve(42);

promise.then(function (value) {
  console.log(value); // 42
});

这段代码创建了一个已完成 Promise,完成处理程序的形参 value 接受了传入值 42,由于该 Promise 永远不会存在拒绝状态,因而该 Promise 的拒绝处理程序永远不会被调用。

使用 Promise.reject()

也可以通过 Promise.reject() 方法来创建已拒绝 Promise,它与 Promise.resolve() 很像,唯一的区别是:创建出来的是拒绝状态的 Promise

let promise = Promise.reject(42);

promise.catch(function (value) {
  console.log(value); // 42
})

任何附加到这个 Promise 的拒绝处理程序都将被调用,但却不会调用完成处理程序。

非 Promise 的 Thenable 对象

Promise.resolve() 方法和 Promise.reject() 方法都可以接受「非 Promise 的 Thenable 对象」作为参数。如果传入一个「非 Promise 的 Thenable 对象」,则这些方法会创建一个新的 Promise,并在 then() 函数中被调用。
【拥有 then() 方法并且接受 resolvereject 这两个参数的普通对象】就是「非 Promise 的 Thenable 对象」,例如:

let thenable = {
  then: function (resolve, reject) {
    resolve(42);
  }
};

在此示例中,thenable 对象和 Promise 对象之间只有 then() 方法这一个相似之处,可以调用 Promise.resolve() 方法将 thenable 对象转换成一个已完成 Promise:

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

// Promise.resolve() 调用的是 thenable.then(),所以 Promise 的状态可以被检测到。
let p1 = Promise.resolve(thenable);
p1.then(function (value) {
  console.log(value); // 42
})

有了 Promise.resolve() 方法和 Promise.reject() 方法,我们可以更轻松地处理「非 Promise 的 Thenable 对象」。

在 ECMAScript 6 引入 Promise 对象之前,许多库都使用了 Thenable 对象,所以如果要向后兼容之前已有的库,则将 Thenable 对象转换为正式 Promise 的能力就显得至关重要了。

如果不确定某个对象是不是 Promise 对象,那么可以根据预期的结果将其传入 Promise.resolve() 方法中或 Promise.reject() 方法中,如果它是 Promise 对象,则不会有任何变化。

执行器错误

如果执行器内部抛出了一个错误,则 Promise 的拒绝处理程序就会被调用。

每个执行器内部都隐含一个 try-catch 块,所以错误会被捕获并传入拒绝处理程序。

let promise = new Promise(function(resolve, reject) {
  throw new Error('Explosion!')
});

promise
  .then(result => {
    console.log(result);
  })
  .catch(err => {
    console.log(err.message); // Explosion!
  });

默认情况下,执行器会捕获所有抛出的错误,但只有当错误处理程序存在时才会记录执行器中抛出的错误,否则错误会被忽略掉。

JavaScript 环境提供了一些捕获已拒绝 Promise 的钩子函数来解决这个问题!

全局的 Promise 拒绝处理

Promise 的特性决定了很难检测一个 Promise 是否被处理过:

let reject = Promise.reject(42);

// 此时,reject 还没有被处理

// 过了一会儿...
reject.catch(function (value) {
  // 现在,reject 已经被处理了
  console.log(value);
})

任何时候都可以调用 then() 方法或 catch() 方法,无论 Promise 是否已解决这两个方法都可以正常运行,但这样就很难知道一个 Promise 何时被处理。在此示例中, Promise 被立即拒绝,但是稍后才被处理。

尽管这个问题在未来版本的 ECMAScript 中可能会被解决,但是 Node.js 和浏览器环境都已分别做出了一些改变来解决开发者的这个痛点,这些改变不是 ECMAScript 6 标准的一部分,不过当你使用 Promise 的时候它们确实是非常有价值的工具。

Node.js 环境的拒绝处理

在 Node.js 中,处理 Promise 拒绝时会触发 process 对象上的两个事件:

  • unhandledRejection:在一个事件循环中,当 Promise 被拒绝,并且没有提供拒绝处理程序时,触发该事件。
  • rejectionHandled:在一个事件循环后,当 Promise 被拒绝时,若拒绝处理程序被调用,触发该事件。

设计这些事件是用来识别那些被拒绝却又没有被处理过的 Promise 的。

拒绝原因(通常是一个错误对象)及被拒绝的 Promise 作为参数被传入到 unhandledRejection 事件中。

let reject;

process.on('unhandledRejection', function (reason, promise) {
  console.log(reason.message); // Explosion!
  console.log(rejected === promise); // true
});

reject = Promise.reject(new Error('Explosion!'))

rejectionHandled 事件处理程序只有一个参数,即被拒绝的 Promise

let reject;

// 当  reject.catch() 处理过后才会监听到 rejectionHandled 事件并被触发
process.on('rejectionHandled', function(promise) {
  console.log(rejected === promise); // true
});

reject = Promise.reject(new Error('Explosion!'));

// 等待添加拒绝处理程序
setTimeout(function () {
  reject.catch(function (value) {
    console.log(value); // Explosin!
  })
}, 1000)

通过事件 rejectionHandled 和事件 unhandledRejection 将潜在未处理的拒绝存储为一个列表,等待一段时间后检查列表便能够正确地跟踪潜在的未处理拒绝。例如下面这个简单的未处理拒绝跟踪器:

let possiblyUnhandledRejections = new Map();

// 如果一个拒绝没有被处理,则将它添加到 Map 集合中
process.on('unhandledRejection', function(reason, promise) {
  possiblyUnhandledRejections.set(promise, reason);
});

// 已处理的 Promise 会从列表中划掉!
process.on('rejectionHandled', function(promise) {
  possiblyUnhandledRejections.delete(promise);
});

setInterval(function () {
  possiblyUnhandledRejections.forEach(function (reason, promise) {
    console.log(reason.message ? reason.message: reason);

    // 处理没有被及时处理的 Promise
    handleRejection(promise, reason);
  });
  
  // 所有拒绝被处理后,清空列表!
  possiblyUnhandledRejections.clear();
}, 60000);

浏览器环境的拒绝处理

浏览器也是通过触发两个事件来识别未处理的拒绝的,只不过这些事件是在 window 对象上被触发的。

  • unhandledrejection 事件,在一个事件循环中,当 Promise 被拒绝,并且没有提供拒绝处理程序时,触发该事件。
  • rejectionhandled 事件,在一个事件循环后,当 Promise 被拒绝时,若拒绝处理程序被调用,触发该事件。

...

串联 Promise

每次调用 then() 方法或 catch() 方法时,实际上创建并返回了另一个 Promise,只有当第一个 Promise 完成或被拒绝后,第二个才会被解决。

let p1 = new Promise(function (resolve, reject) {
  resolve(42);
});

p1.then(function (value) {
  console.log(value);
}).then(function () {
  console.log("Finish!");
})

这段代码输出以下内容:

42
finish!

捕获错误

「完成处理程序」或「拒绝处理程序」中可能会发生错误,而 Promise 链可以用来捕获这些错误:

let promise = new Promise(function(resolve, reject) {
  resolve(42);
});

promise.then((result) => {
  throw new Error('Boom!')
}).catch((err) => {
  console.log(err.message); // Boom!
});

Promise 链的返回值

Promise 链的另一个重要特性是:可以给下游 Promise 传递数据

let promise = new Promise(function(resolve, reject) {
  resolve(42);
});

promise
  .then(result => {
    console.log(result); // 42
    return result + 1;
  })
  .then(result => {
    console.log(result); // 43
  });

在 Promise 链中返回 Promise

...

响应多个 Promise

如果想要监听多个 Promise 来决定下一步的操作,则可以使用 Promise.all()Promise.race() 方法。

Promise.all() 方法

Promise.al1() 方法只接受一个参数并返回一个 Promise,该参数是一个含有多个受监视 Promise 的可迭代对象(例如,一个数组),只有当可迭代对象中所有 Promise 都被解决后返回的 Promise 才会被解决,只有当可迭代对象中所有 Promise 都被完成后返回的 Promise 才会被完成。

所有传入 Promise.al1() 方法的 Promise 只要有一个被拒绝,那么返回的 Promise 没等所有 Promise 都完成就立即被拒绝。

let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});

let p2 = new Promise(function(resolve, reject) {
  resolve(43);
});

let p3 = new Promise(function(resolve, reject) {
  resolve(44);
});

// 只有当 p1、p2、p3  都处于完成状态后 p4 才被完成。
let p4 = Promise.all([p1, p2, p3]);

p4.then(function (value) {
  console.log(Array.isArray(value)); // true
  console.log(value[0]); // 42
  console.log(value[1]); // 43
  console.log(value[2]); // 44
});

Promise.race() 方法

Promise.race() 方法监听多个 Promise 的方法稍有不同:它也接受含多个受监视 Promise 的可迭代对象作为唯一参数并返回一个 Promise,但只要有一个 Promise 被解决返回的 Promise 就被解决,无须等到所有 Promise 都被完成。一旦数组中的某个 Promise 被完成,Promise.race() 方法也会像 Promise.all() 方法一样返回一个特定的 Promise:

let p1 = Promise.resolve(42);

let p2 = new Promise(function(resolve, reject) {
  resolve(43);
});

let p3 = new Promise(function(resolve, reject) {
  resolve(44);
});

let p4 = Promise.race([p1, p2, p3]);

p4.then(function (value) {
  console.log(value); // 42
});

实际上,传递给 Promise.race() 方法的 Promise 会进行竞选,哪个先完成就先返回哪个,不管这个 Promise 返回的是已完成的还是已拒绝的。

自 Promise 继承

Promise 与其他内建类型一样,也可以作为基类派生其他类,所以你可以定义自己的 Promise 变量来扩展内建 Promise 的功能。

...

基于 Promise 的异步任务执行

将生成器与 Promise 结合。

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

推荐阅读更多精彩内容