Promise使用手册

本篇以Promise为核心, 逐步展开, 最终分析process.nextTick , promise.then , setTimeout , setImmediate 它们的异步机制.javascript

导读

Promise问世已久, 其科普类文章亦不可胜数. 遂本篇初衷不为科普, 只为可以温故而知新.php

好比说, catch能捕获全部的错误吗? 为何有些时候会抛出"Uncaught (in promise) …"? Promise.resolvePromise.reject 处理Promise对象时又有什么不同的地方?html

Promise

引子

阅读此篇以前, 咱们先体验一下以下代码:java

setTimeout(function() {
  console.log(4)
}, 0);
new Promise(function(resolve) {
  console.log(1);
  for (var i = 0; i < 10000; i++) {
    i == 9999 && resolve()
  }
  console.log(2);
}).then(function() {
  console.log(5)
});
console.log(3);复制代码

这里先卖个关子, 后续将给出答案并提供详细分析.node

和往常文章同样, 我喜欢从api入手, 先具象地了解一个概念, 而后再抽象或扩展这个概念, 接着再谈谈概念的具体应用场景, 一般末尾还会有一个简短的小结. 这样, 查询api的读者能够选择性地阅读上文, 但愿深刻的读者能够继续剖析概念, 固然我更但愿你能耐心地读到应用场景处, 这样便能升华对这个概念或技术的运用, 也能避免踩坑.react

new Promise

Promise的设计初衷是避免异步回调地狱. 它提供更简洁的api, 同时展平回调为链式调用, 使得代码更加清爽, 易读.git

以下, 即建立一个Promise对象:github

const p = new Promise(function(resolve, reject) {
  console.log('Create a new Promise.');
});
console.log(p);复制代码

new Promise

建立Promise时, 浏览器同步执行传入的第一个方法, 从而输出log. 新建立的promise实例对象, 初始状态为等待(pending), 除此以外, Promise还有另外两个状态:web

  • fulfilled, 表示操做完成, 实现了. 只在resolve方法执行时才进入该状态.
  • rejected, 表示操做失败, 拒绝了. 只在reject方法执行时或抛出错误的状况下才进入该状态.

以下图展现了Promise的状态变化过程(图片来自MDN):ajax

Promise state

从初始状态(pending)到实现(fulfilled)或拒绝(rejected)状态的转换, 这是两个分支, 实现或拒绝即最终状态, 一旦到达其中之一的状态, promise的状态便稳定了. (所以, 不要尝试实现或拒绝状态的互转, 它们都是最终状态, 无法转换)

以上, 建立Promise对象时, 传入的回调函数function(resolve, reject){}默认拥有两个参数, 分别为:

  • resolve, 用于改变该Promise自己的状态为实现, 执行后, 将触发then的onFulfilled回调, 并把resolve的参数传递给onFulfilled回调.
  • reject, 用于改变该Promise自己的状态为拒绝, 执行后, 将触发 then | catch的onRejected回调, 并把reject的参数传递给onRejected回调.

Promise的原型仅有两个自身方法, 分别为 Promise.prototype.then , Promise.prototype.catch . 而它自身仅有四个方法, 分别为 Promise.reject , Promise.resolve , Promise.all , Promise.race .

then

语法: Promise.prototype.then(onFulfilled, onRejected)

用于绑定后续操做. 使用十分简单:

p.then(function(res) {
  console.log('此处执行后续操做');
});
// 固然, then的最大便利之处即是能够链式调用
p.then(function(res) {
  console.log('先作一件事');
}).then(function(res) {
  console.log('再作一件事');
});
// then还能够同时接两个回调,分别处理成功和失败状态
p.then(function(SuccessRes) {
  console.log('处理成功的操做');
}, function(failRes) {
  console.log('处理失败的操做');
});复制代码

不只如此, Promise的then中还可返回一个新的Promise对象, 后续的then将接着继续处理这个新的Promise对象.

p.then(function(){
  return new Promise(function(resolve, reject) {
    console.log('这里是一个新的Promise对象');
    resolve('New Promise resolve.');
  });
}).then(function(res) {
  console.log(res);
});复制代码

那么, 若是没有指定返回值, 会怎么样?

根据Promise规范, then或catch即便未显式指定返回值, 它们也老是默认返回一个新的fulfilled状态的promise对象.

catch

语法: Promise.prototype.catch(onRejected)

用于捕获并处理异常. 不管是程序抛出的异常, 仍是主动reject掉Promise自身, 都会被catch捕获到.

new Promise(function(resolve, reject) {
  reject('该prormise已被拒绝');
}).catch(function(reason) {
  console.log('catch:', reason);
});复制代码

同then语句同样, catch也是能够链式调用的.

new Promise(function(resolve, reject){
  reject('该prormise已被拒绝');
}).catch(function(reason){
  console.log('catch:', reason);
  console.log(a);
}).catch(function(reason){
  console.log(reason);
});复制代码

以上, 将依次输出两次log, 第一次输出promise被拒绝, 第二次输出"ReferenceError a is not defined"的堆栈信息.

catch能捕获哪些错误

那是否是catch能够捕获全部错误呢? 能够, 怎么不能够, 我之前也这么天真的认为. 直到有一天我执行了以下的语句, 我就学乖了.

new Promise(function(resolve, reject){
  Promise.reject('返回一个拒绝状态的Promise');
}).catch(function(reason){
  console.log('catch:', reason);
});复制代码

执行结果以下:

为何catch没有捕获到该错误呢? 这个问题, 待下一节咱们了解了Promise.reject语法后再作分析.

Promise.reject

语法: Promise.reject(value)

该方法返回一个拒绝状态的Promise对象, 同时传入的参数做为PromiseValue.

//params: String
Promise.reject('该prormise已被拒绝');
.catch(function(reason){
  console.log('catch:', reason);
});
//params: Error
Promise.reject(new Error('这是一个error')).then(function(res) {
  console.log('fulfilled:', res);
}, function(reason) {
  console.log('rejected:', reason); // rejected: Error: 这是一个error...
});复制代码

即便参数为Promise对象, 它也同样会把Promise看成拒绝的理由, 在外部再包装一个拒绝状态的Promise对象予以返回.

//params: Promise
const p = new Promise(function(resolve) {
  console.log('This is a promise');
});
Promise.reject(p).catch(function(reason) {
  console.log('rejected:', reason);
  console.log(p == reason);
});
// "This is a promise"
// rejected: Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
// true复制代码

以上代码片断, Promise.reject(p) 进入到了catch语句中, 说明其返回了一个拒绝状态的Promise, 同时拒绝的理由就是传入的参数p.

错误处理

咱们都知道, Promise.reject返回了一个拒绝状态的Promise对象. 对于这样的Promise对象, 若是其后续then | catch中都没有声明onRejected回调, 它将会抛出一个 "Uncaught (in promise) ..."的错误. 如上图所示, 原语句是 "Promise.reject('返回一个拒绝状态的Promise');" 其后续并无跟随任何then | catch语句, 所以它将抛出错误, 且该错外部的Promise没法捕获.

不只如此, Promise之间泾渭分明, 内部Promise抛出的任何错误, 外部Promise对象都没法感知并捕获. 同时, 因为promise是异步的, try catch语句也没法捕获其错误.

所以养成良好习惯, promise记得写上catch.

除了catch, nodejs下Promise抛出的错误, 还会被进程的unhandledRejectionrejectionHandled事件捕获.

var p = new Promise(function(resolve, reject){
  //console.log(a);
  reject('rejected');
});
setTimeout(function(){
  p.catch(function(reason){
    console.info('promise catch:', reason);
  });
});
process.on('uncaughtException', (e) => {
  console.error('uncaughtException', e);
});
process.on('unhandledRejection', (e) => {
  console.info('unhandledRejection:', e);
});
process.on('rejectionHandled', (e) => {
  console.info('rejectionHandled', e);
});
//unhandledRejection: rejected
//rejectionHandled Promise { <rejected> 'rejected' }
//promise catch: rejected复制代码

即便去掉以上代码中的注释, 输出依然一致. 可见, Promise内部抛出的错误, 都不会被uncaughtException事件捕获.

链式写法的好处

请看以下代码:

new Promise(function(resolve, reject) {
  resolve('New Promise resolve.');
}).then(function(str) {
  throw new Error("oops...");
},function(error) {
    console.log('then catch:', error);
}).catch(function(reason) {
    console.log('catch:', reason);
});
//catch: Error: oops...复制代码

可见, then语句的onRejected回调并不能捕获onFulfilled回调内抛出的错误, 尾随其后的catch语句却能够, 所以推荐链式写法.

Promise.resolve

语法: Promise.resolve(value | promise | thenable)

thenable 表示一个定义了 then 方法的对象或函数.

参数为promise时, 返回promise自己.

参数为thenable的对象或函数时, 将其then属性做为new promise时的回调, 返回一个包装的promise对象.(注意: 这里与Promise.reject直接包装一个拒绝状态的Promise不一样)

其余状况下, 返回一个实现状态的Promise对象, 同时传入的参数做为PromiseValue.

//params: String
//return: fulfilled Promise
Promise.resolve('返回一个fulfilled状态的promise').then(function(res) {
  console.log(res); // "返回一个fulfilled状态的promise"
});

//params: Array
//return: fulfilled Promise
Promise.resolve(['a', 'b', 'c']).then(function(res) {
  console.log(res); // ["a", "b", "c"]
});

//params: Promise
//return: Promise self
let resolveFn;
const p2 = new Promise(function(resolve) {
  resolveFn = resolve;
});
const r2 = Promise.resolve(p2);
r2.then(function(res) {
  console.log(res);
});
resolveFn('xyz'); // "xyz"
console.log(r2 === p2); // true

//params: thenable Object
//return: 根据thenable的最终状态返回不一样的promise
const thenable = {
  then: function(resolve, reject) { //做为new promise时的回调函数
    reject('promise rejected!');
  }
};
Promise.resolve(thenable).then(function(res) {
  console.log('res:', res);
}, function(reason) {
  console.log('reason:', reason);
});复制代码

可见, Promise.resolve并不是返回实现状态的Promise这么简单, 咱们还需基于传入的参数动态判断.

至此, 咱们基本上不用指望使用Promise全局方法中去改变其某个实例的状态.

  • 对于Promise.reject(promise), 它只是简单地包了一个拒绝状态的promise壳, 参数promise什么都没变.
  • 对于Promise.resolve(promise), 仅仅返回参数promise自己.

Promise.all

语法: Promise.all(iterable)

该方法接一个迭代器(如数组等), 返回一个新的Promise对象. 若是迭代器中全部的Promise对象都被实现, 那么, 返回的Promise对象状态为"fulfilled", 反之则为"rejected". 概念上相似Array.prototype.every.

//params: all fulfilled promise
//return: fulfilled promise
Promise.all([1, 2, 3]).then(function(res){
  console.log('promise fulfilled:', res); // promise fulfilled: [1, 2, 3]
});

//params: has rejected promise
//return: rejected promise
const p = new Promise(function(resolve, reject){
  reject('rejected');
});
Promise.all([1, 2, p]).then(function(res){
  console.log('promise fulfilled:', res);
}).catch(function(reason){
  console.log('promise reject:', reason); // promise reject: rejected
});复制代码

Promise.all特别适用于处理依赖多个异步请求的结果的场景.

Promise.race

该方法接一个迭代器(如数组等), 返回一个新的Promise对象. 只要迭代器中有一个Promise对象状态改变(被实现或被拒绝), 那么返回的Promise将以相同的值被实现或拒绝, 而后它将忽略迭代器中其余Promise的状态变化.

Promise.race([1, Promise.reject(2)]).then(function(res){
  console.log('promise fulfilled:', res);
}).catch(function(reason){
  console.log('promise reject:', reason);
});
// promise fulfilled: 1复制代码

若是调换以上参数的顺序, 结果将输出 "promise reject: 2". 可见对于状态稳定的Promise(fulfilled 或 rejected状态), 哪一个排第一, 将返回哪一个.

Promise.race适用于多者中取其一的场景, 好比同时发送多个请求, 只要有一个请求成功, 那么就以该Promise的状态做为最终的状态, 该Promise的值做为最终的值, 包装成一个新的Promise对象予以返回.

Fetch进阶指南 一文中, 我曾利用Promise.race模拟了Promise的abort和timeout机制.

Promises/A+规范的要点

promise.then(onFulfilled, onRejected)中, 参数都是可选的, 若是onFulfilled或onRejected不是函数, 那么将忽略它们.

catch只是then的语法糖, 至关于promise.then(null, onRejected).

任务队列之谜

终于, 咱们要一块儿来看看文章起始的一道题目.

setTimeout(function() {
  console.log(4)
}, 0);
new Promise(function(resolve) {
  console.log(1);
  for (var i = 0; i < 10000; i++) {
    i == 9999 && resolve()
  }
  console.log(2);
}).then(function() {
  console.log(5)
});
console.log(3);复制代码

这道题目来自知乎(机智的你可能早已看穿, 但千万别戳破😂), 能够戳此连接 Promise的队列与setTimeout的队列有何关联 围观点赞.

围观完了, 别忘了继续读下去, 这里请容许我站在诸位知乎大神的肩膀上, 继续深刻分析.

以上代码, 最终运行结果是1,2,3,5,4. 并非1,2,3,4,5.

  1. 首先前面有提到, new Promise第一个回调函数内的语句同步执行, 所以控制台将顺序输出1,2, 此处应无异议.
  2. console.log(3), 这里是同步执行, 所以接着将输出3, 此处应无异议.
  3. 剩下即是setTimeout 和 Promise的then的博弈了, 同为异步事件, 为何then后注册却先于setTimeout执行?

以前, 咱们在 Ajax知识体系 一文中有提到:

浏览器中, js引擎线程会循环从 任务队列 中读取事件而且执行, 这种运行机制称做 Event Loop (事件循环).

不只如此, event loop至少拥有以下两种队列:

  • task queue, 也叫macrotask queue, 指的是宏任务队列, 包括rendering, script(页面脚本), 鼠标, 键盘, 网络请求等事件触发, setTimeout, setInterval, setImmediate(node)等等.
  • microtask queue, 指的是微任务队列, 用于在浏览器从新渲染前执行, 包含Promise, process.nextTick(node), Object.observe, MutationObserver回调等.

以下是HTML规范原文:

An event loop has one or more task queues. A task queue is an ordered list of tasks, which are algorithms that are responsible for such work as: events, parsing, callbacks, using a resource, reacting to DOM manipulation...

Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue.

浏览器(或宿主环境) 遵循队列先进先出原则, 依次遍历macrotask queue中的每个task, 不过每执行一个macrotask, 并非当即就执行下一个, 而是执行一遍microtask queue中的任务, 而后切换GUI线程从新渲染或垃圾回收等.

上述代码块能够看作是一个macrotask, 对于其执行过程, 不妨做以下简化:

  1. 首先执行当前macrotask, 将setTimeout回调以一个新的task形式, 加入到macrotask queue末尾.
  2. 当前macrotask继续执行, 建立一个新的Promise, 同步执行其回调函数, 输出1; for循环1w次, 而后执行resolve方法, 将该Promise回调加入到microtask queue末尾, 循环结束, 接着输出2.
  3. 当前macrotask继续执行, 输出3. 至此, 当前macrotask执行完毕.
  4. 开始顺序执行microtask queue中的全部任务, 也包括刚刚加入到队列末尾 Promise回调, 故输出5. 至此, microtask queue任务所有执行完毕, microtask queue清空.
  5. 浏览器挂起js引擎, 可能切换至GUI线程或者执行垃圾回收等.
  6. 切换回js引擎, 继续从macrotask queue取出下一个macrotask, 执行之, 而后再取出microtask queue, 执行之, 后续全部的macrotask均如此重复. 天然, 也包括刚刚加入到队列末尾的setTimeout回调, 故输出4.

这里直接给出事件回调优先级:

process.nextTick > promise.then > setTimeout ? setImmediate复制代码

nodejs中每一次event loop称做tick. _tickCallback在macrotask queue中每一个task执行完成后触发. 实际上, _tickCallback内部共干了两件事:

  1. 执行nextTick queue中的全部任务, 包括process.nextTick注册的回调.
  2. 第一步完成后执行 _runMicrotasks函数, 即执行microtask queue中的全部任务, 包括promise.then注册的回调.

所以, process.nextTick优先级比promise.then高.

那么setTimeout与setImmediate到底哪一个更快呢? 回答是并不肯定. 请看以下代码:

setImmediate(function(){
    console.log(1);
});
setTimeout(function(){
    console.log(0);
}, 0);复制代码

先后两次的执行结果以下:

测试时, 我本地node版本是v5.7.0.


本问就讨论这么多内容,你们有什么问题或好的想法欢迎在下方参与留言和评论.

本文做者: louis

本文连接: louiszhai.github.io/2017/02/25/…

参考文章

相关文章
相关标签/搜索