本篇以Promise为核心, 逐步展开, 最终分析process.nextTick , promise.then , setTimeout , setImmediate 它们的异步机制.javascript
Promise问世已久, 其科普类文章亦不可胜数. 遂本篇初衷不为科普, 只为可以温故而知新.php
好比说, catch能捕获全部的错误吗? 为何有些时候会抛出"Uncaught (in promise) …"? Promise.resolve
和 Promise.reject
处理Promise对象时又有什么不同的地方?html
阅读此篇以前, 咱们先体验一下以下代码: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
Promise的设计初衷是避免异步回调地狱. 它提供更简洁的api, 同时展平回调为链式调用, 使得代码更加清爽, 易读.git
以下, 即建立一个Promise对象:github
const p = new Promise(function(resolve, reject) {
console.log('Create a new Promise.');
});
console.log(p);复制代码
建立Promise时, 浏览器同步执行传入的第一个方法, 从而输出log. 新建立的promise实例对象, 初始状态为等待(pending), 除此以外, Promise还有另外两个状态:web
以下图展现了Promise的状态变化过程(图片来自MDN):ajax
从初始状态(pending)到实现(fulfilled)或拒绝(rejected)状态的转换, 这是两个分支, 实现或拒绝即最终状态, 一旦到达其中之一的状态, promise的状态便稳定了. (所以, 不要尝试实现或拒绝状态的互转, 它们都是最终状态, 无法转换)
以上, 建立Promise对象时, 传入的回调函数function(resolve, reject){}
默认拥有两个参数, 分别为:
Promise的原型仅有两个自身方法, 分别为 Promise.prototype.then
, Promise.prototype.catch
. 而它自身仅有四个方法, 分别为 Promise.reject
, Promise.resolve
, Promise.all
, Promise.race
.
语法: 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对象.
语法: 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能够捕获全部错误呢? 能够, 怎么不能够, 我之前也这么天真的认为. 直到有一天我执行了以下的语句, 我就学乖了.
new Promise(function(resolve, reject){
Promise.reject('返回一个拒绝状态的Promise');
}).catch(function(reason){
console.log('catch:', reason);
});复制代码
执行结果以下:
为何catch没有捕获到该错误呢? 这个问题, 待下一节咱们了解了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抛出的错误, 还会被进程的unhandledRejection
和 rejectionHandled
事件捕获.
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(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.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对象. 只要迭代器中有一个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机制.
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.
console.log(3)
, 这里是同步执行, 所以接着将输出3, 此处应无异议.以前, 咱们在 Ajax知识体系 一文中有提到:
浏览器中, js引擎线程会循环从
任务队列
中读取事件而且执行, 这种运行机制称做Event Loop
(事件循环).
不只如此, event loop至少拥有以下两种队列:
以下是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, 对于其执行过程, 不妨做以下简化:
这里直接给出事件回调优先级:
process.nextTick > promise.then > setTimeout ? setImmediate复制代码
nodejs中每一次event loop称做tick. _tickCallback在macrotask queue中每一个task执行完成后触发. 实际上, _tickCallback内部共干了两件事:
所以, 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/…
参考文章