学习Promise异步编程
JavaScript引擎建立在单线程事件循环的概念上。单线程( Single-threaded )意味着同一时刻只能执行一段代码。所以引擎无须留意那些“可能”运行的代码。代码会被放置在作业队列( job queue )中,每当一段代码准备被执行,它就会被添加到作业队列。当 JS 引擎结束当前代码的执行后,事件循环就会执行队列中的下一个作业.事件循环(event loop)是JS引擎的一个内部处理线程,能监视代码的执行并管理作业队列。关于事件循环可以阅读这篇文章 ---- 一文梳理JavaScript 事件循环(Event Loop)
1. 为什么要用Promise?
1.1 事件模型
当用户点击一个按钮或按下键盘上的一个键时,一个事件,例如 onclick 就被触发了。该事件可能会对此交互进行响应,从而将一个新的作业添加到作业队列的尾部。这就是 JavaScript 关于异步编程的最基本形式。事件处理程序代码直到事件发生后才会被执行,此时它会拥有合适的上下文。例如:
let button = document.getElementById("my-btn");
button.onclick = function(event) {
console.log("Clicked");
};
事件可以很好地工作于简单的交互,但将多个分离的异步调用串联在一起却会很麻烦。此外,还需确保所有的事件处理程序都能在事件第一次触发之前被绑定完毕。例如,若 button 在onclick被绑定之前就被点击,那就不会有任何事发生。因此虽然在响应用户交互或类似的低频功能时,事件很有用,但它在面对更复杂的需求时仍然不够灵活。
1.2 回调函数
回调函数模式类似于事件模型,因为异步代码也会在后面的一个时间点才执行。不同之处在于需要调用的函
数(即回调函数)是作为参数传入的。
eadFile("example.txt", function(err, contents) {
if (err) {
throw err;
}
console.log(contents);
});
console.log("Hi!");
使用回调函数模式,readFile() 会立即开始执行,并在开始读取磁盘时暂停。这意味着console.log("Hi!") 会在 readFile() 被调用后立即进行输出,要早于console.log(contents) 的打印操作。当 readFile() 结束操作后,它会将回调函数以及相关参数作为一个新的作业添加到作业队列的尾部。在之前的作业全部结束后,该作业才会执行。回调函数模式要比事件模型灵活得多,因为使用回调函数串联多个调用会相对容易。
这种模式运作得相当好,但容易陷入了回调地狱( callback hell ),这会在嵌套过多回调函数时发生。当想要实现更复杂的功能时,回调函数也会存在问题。如让两个异步操作并行运行,并且在它们都结束后提醒你;同时启动两个异步操作,但只采用首个结束的结果;在这些情况下,需要追踪多个回调函数并做清理操作, Promise 能大幅度改善这种情况。
2. Promise基础
Promise 是异步编程的一种解决方案,相比回调函数和事件,更加强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
Promise 是为异步操作的结果所准备的占位符。函数可以返回一个 Promise,而不必订阅一个事件或向函数传递一个回调参数。
/ readFile 承诺会在将来某个时间点完成
let promise = readFile("example.txt");
每个 Promise 都会经历一个短暂的生命周期,初始为pending ,这表示异步操作尚未结束。一个状态为pending的 Promise 也被认为是未决的( unsettled )。一旦异步操作结束, Promise就会被认为是已决的(settled),并进入两种可能状态之一:
- fulfilled(已完成): Promise 的异步操作已成功结束
- rejected(已拒绝):Promise 的异步操作未成功结束,可能是一个错误,或由其他原因导致
内部的 [[PromiseState]] 属性会被设置为 "pending" 、 "fulfilled" 或 "rejected" ,以反映 Promise 的状态。该属性并未在 Promise 对象上被暴露出来,因此你无法以编程方式判断 Promise 到底处于哪种状态。
2.1 Promise特质及优点
Promise对象有以下两个特点。
(1)对象的状态不受外界影响。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
2.2 Promise缺点
- 无法取消
Promise,一旦新建它就会立即执行,无法中途取消。 - 如果不设置回调函数,
Promise内部抛出的错误,不会反应到外部 - 当处于
pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
3.创建Promise对象
3.1 创建未决的Promise
ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。Promise 新建后就会立即执行。
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value); // done
});
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。
3.2 创建已决的Promise
使用Promise.resolve()和Promise.reject()方法能够创建已决的Promise对象,前提是传入参数不为pending态的Promise实例,并被Promise.resolve()方法调用。
(1) 参数为空
Promise.resolve()方法调用时不带参数,直接返回一个resolved状态的 Promise 对象。Promise.reject()方法调用时不带参数,直接返回一个rejected状态的 Promise 对象。
(2) 参数为Promise实例
注意:如果传递一个Promise给Promise.resolve(),则不做任何修改、原封不动地返回这个Promise。;传递给Promise.reject(),则会在原 Promise 上包装出一个新的 Promise。示例如下所示:
// 传入Promise状态为resolved
let promise1 = Promise.resolve(43);
let promise2 = Promise.resolve(promise1); // Promise { 43 }
console.log(promise2===promise1); // true
promise2.then(function(value){
console.log(value) // 43
});
let promise3 = Promise.reject(promise1); // Promise { <rejected> Promise { 43 } }
promise3.catch(function(value){
console.log(value===promise1) // true
console.log(value) // Promise { 43 }
});
// 传入Promise状态为rejected
let promise4 = Promise.reject(44)
let promise5 = Promise.reject(promise4);
console.log(promise5); // Promise { <rejected> Promise { <rejected> 44 } }
promise5.catch(function(value){
console.log(value===promise4) // true
value.catch(function(v){
console.log(v) // 44
})
});
let promise6 = Promise.resolve(promise4); // Promise {<rejected>: 44}
console.log(promise6===promise4); // true
promise6.catch(function(v){
console.log(v); // 44
});
// 传入Promise状态为pending
let promise7 = new Promise(function(resolve, reject){
try{
resolve();
}catch (err){
reject(err);
}
});
promise7.then(function(){
console.log('promise7 resolved');
},function(err){
console.log('promise7 rejected');
});
let promise8 = Promise.resolve(promise7);
console.log(promise8===promise7); // true
promise8.then(function(value){
console.log(value); // undefined
})
let promise9 = Promise.reject(promise7);
console.log(promise9); // Promise { <rejected> Promise { undefined } }
promise9.catch(function(value){
console.log(value===promise7); // true
console.log(value); // Promise { undefined }
})
(3) 参数为非Promise的Thenable
Promise.resolve() 与 Promise.reject() 都能接受非 Promise 的 thenable 作为参数。
当一个对象拥有一个能接受 resolve 与 reject 参数的 then() 方法,该对象就会被认为是一个非 Promise 的 thenable ,就像这样:
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
当传入了非 Promise 的 thenable 时,Promise.resolve()方法会将其转为Promise对象,然后立即执行thenable对象的then()方法。如下所示:
let thenable = {
then:function(resolve, reject){
resolve(43);
}
}
let p1 = Promise.resolve(thenable); // Promise { <pending> }
p1.then(function(value){
console.log(value); // 43
});
thenable = {
then:function(resolve, reject){
reject(44);
}
}
let p2 = Promise.resolve(thenable) // // Promise { <pending> }
p2.catch(function(value){
console.log(value); // 44
});
// p1,p2 等同于 new Promise(function(resolve, reject){
// try{
// resolve(43);
// } catch (err) {
// reject(44)
// }
// });
当传入了非 Promise 的 thenable 时,Promise.reject()方法则会在thenable对象上包装出一个Promise,状态为rejected,调用该Promise的catch方法则其value参数为thenable对象。
let thenable = {
then:function(resolve, reject){
resolve(43);
}
}
let p1 = Promise.reject(thenable); // Promise { <rejected> { then: [Function: then] } }
console.log(p1)
p1.catch(function(value){
console.log(value=== thenable); // true
});
thenable = {
then:function(resolve, reject){
reject(44);
}
}
let p2 = Promise.reject(thenable) // Promise { <rejected> { then: [Function: then] } }
p2.catch(function(value){
console.log(value===thenable); // true
});
(4) 参数为不具有then方法
如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved,Promise.reject()方法返回一个状态为rejected的Promise对象。
4. 单异步响应
Promise实例具有3个原型方法,用以平时处理单个异步操作,如下所示:
- Promise.prototype.then
- Promise.prototype.catch
- Promise.prototype.finally
4.1 Promise.prototype.then
then方法是定义在原型对象Promise.prototype上的。作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。
4.2 Promise.prototype.catch
Promise.prototype.catch()方法等同于.then(null, rejection)或.then(undefined, rejection),用于指定发生错误时的回调函数。所以catch方法返回的也是一个新的Promise实例,也可以采用链式写法。
如果异步操作抛出错误,Promise状态变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。
一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
4.3 Promise.prototype.finally
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。
finally本质上是then方法的特例。
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。
它的实现也很简单。
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
上面代码中,不管前面的 Promise 是fulfilled还是rejected,都会执行回调函数callback。
5. 并行异步响应
JavaScript中Promise有如下方法可并行处理多个异步操作:
- Promise.all()
- Promise.race()
- Promise.allSettled()
- Promise.any()
5.1 Promise.all()
Promise.all()方法接收单个可迭代对象(如数组)作为参数,可迭代对象的元素都为Promise实例,若不是则调用Promise.resolve()方法将其转化为Promise实例,再进一步处理。
const p = Promise.all([p1,p2,p3])
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
5.2 Promise.race()
Promise.race()也接受一个包含需监视的 Promise 的可迭代对象,并返回一个新的 Promise。和Promise.all()方法不同的是,一旦来源Promise中有一个被完成,所返回的Promise就会立刻完成,那个率先完成得Promsie的返回值会传递给返回的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
});
5.3 Promise.allSettled()
Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。
该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例。
let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
reject(44);
});
let p4 = Promise.allSettled([p1, p2, p3]);
p4.then(function(value) {
console.log(value);
console.log(p4);
});
0: {status: "fulfilled", value: 42}
1: {status: "fulfilled", value: 43}
2: {status: "rejected", reason: 44}

5.4 Promise.any()
ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled状态,
const p = Promise.any([p1,p2,p3]);
p的状态由p1、p2、p3决定,分成两种情况。
(1)只要p1、p2、p3的状态任意一个变成fulfilled,p的状态就变成fulfilled,并且首个fulfilled的Promise的返回值传递给p的回调函数。
(2)只有p1、p2、p3全部被rejected,p的状态就变成rejected,并且抛出一个AggregateError 错误。它相当于一个数组,每个成员对应一个被rejected的操作所抛出的错误。
let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
reject(44);
});
let p = Promise.any([p1, p2, p3]);
p.then(function(value) {
console.log(value);
console.log(p);
});
results:

let p1 = Promise.reject(42);
let p2 = new Promise(function(resolve, reject) {
reject(43);
});
let p3 = new Promise(function(resolve, reject) {
reject(44);
});
let p = Promise.any([p1, p2, p3]);
p.then(function(value) {
console.log(value);
});
results:

6.小结
Promise被设计用于改善JS中的异步编程,与事件和回调函数对比,在异步操作中给我们提供了更多的控制权与组合型。Promise具有三种状态:挂起、已完成、已拒绝。一个Promise起始于挂起态,并在成功时转为完成态,或在失败时转为拒绝态。在这两种情况下,处理函数都能被添加以表明Promise何时被解决。then()方法允许你绑定完成处理函数与拒绝处理函数,而 catch()方法则只允许你绑定拒绝处理函数。并且Promise能用多种方式串联在一起,并在它们之间传递信息。每个对 then() 的调用都创建并返回了一个新的Promise,在前一个Promise被决议时,新Promise也会被决议。Promise链可被用于触发对一系列异步事件的响应。除此之外,我们能够使用Promsie.all()/Promise.race()/Promise.allSettled()/Promise.any()同时监听多个Promise,并行性相应的响应。
学习Promise异步编程的更多相关文章
- ES6笔记(7)-- Promise异步编程
系列文章 -- ES6笔记系列 很久很久以前,在做Node.js聊天室,使用MongoDB数据服务的时候就遇到了多重回调嵌套导致代码混乱的问题. JS异步编程有利有弊,Promise的出现,改善了这一 ...
- ES6入门八:Promise异步编程与模拟实现源码
Promise的基本使用入门: ——实例化promise对象与注册回调 ——宏任务与微任务的执行顺序 ——then方法的链式调用与抛出错误(throw new Error) ——链式调用的返回值与传值 ...
- promise异步编程的原理
一.起源 JavaScript中的异步由来已久,不论是定时函数,事件处理函数还是ajax异步加载都是异步编程的一种形式,我们现在以nodejs中异步读取文件为例来编写一个传统意义的异步函数: var ...
- nodejs学习笔记 —— 异步编程解决方案
在js或者node编程中,由于异步的频繁和广度使用,使得回调和嵌套的深度导致编程的体验遇到一些挑战,如果写出优雅和好看的代码,本文主要针对异步编程的主流方案做一些总结 1.事件发布/订阅模式 事件监听 ...
- async/await actor promise 异步编程
Python协程:从yield/send到async/await http://blog.guoyb.com/2016/07/03/python-coroutine/ Async/Await替代Pro ...
- Promise异步编程解决方案
Promise是ES6中新增的异步编程解决方案,体现在代码中它是一个对象,可以通过 Promise 构造函数来实例化. 其最基本的使用 new Promise(function(resolve,rej ...
- 09-Node.js学习笔记-异步编程
同步API,异步API 同步API:只有当前API执行完成后,才能继续执行下一个API console.log('before'); console.log('after'); 异步API:当前API ...
- NodeJS学习之异步编程
NodeJS -- 异步编程 NodeJS最大的卖点--事件机制和异步IO,对开发者并不透明 代码设计模式 异步编程有很多特有的代码设计模式,为了实现同样的功能,使用同步方式和异步方式编写代码会有很大 ...
- 【ES6】Generator+Promise异步编程
一.概念 首先我们要理解Generator和Promise的概念. Generator:意思是生成器,可以在函数内部通过yeild来控制语句的执行或暂停状态. *Foo(){ yeild consol ...
随机推荐
- LeetCode 027 Remove Element
题目要求:Remove Element Given an array and a value, remove all instances of that value in place and retu ...
- Android自带图标库
Java Usage example: myMenuItem.setIcon(android.R.drawable.ic_menu_save); Resource Usage example: and ...
- JJWT 使用示例
一.添加依赖包 <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api ...
- C#数据结构-二叉树-顺序存储结构
什么是二叉树:每个树的节点只有两个子树的树形结构. 为什么使用顺序存储结构:使用数组存放满二叉树的各结点非常方便,可以根据一个结点的索引号很容易地推算出它的双亲.孩子.兄弟等结点的编号,从而对这些结点 ...
- 并发编程实战-J.U.C核心包
J.U.C - AQS java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心.它核心是利用volatile和一个维护队列. AQS其实就是ja ...
- PyQt(Python+Qt)学习随笔:QListWidget的currentRow属性
QListWidget的currentRow属性保存当前项的位置,为整型,从0开始计数,在某些选择模式下,当前项可能也是选中项. currentRow属性可以通过方法currentRow().setC ...
- 【聊技术】在Android中实现自适应文本大小显示
本周的聊技术话题和大家说说如何在Android中实现自适应文本大小显示. 想象一下,在布局中,通常显示文本的区域大小是固定的,但是文本长度并不总是固定的.比如列表中的文章标题.界面下方的按钮文本等等. ...
- uniapp 微信授权登陆
准备工作: 1.微信开发者账号 2.AppId .AppSecret (这些可以在开放平台申请到) 第一步 添加移动应用,仔细添加上述信息: 审批通过后,即刻 第二步 打开uniapp,开启OAuth ...
- Day6 Scrum 冲刺博客
一.站立式会议# 1. 会议照片 2. 工作进度+燃尽图 团队成员 昨日完成工作 今日工作计划 遇到的困难 周梓波 将方块旋转变形 添加键盘监听事件 不熟悉监听事件的操作 纪昂学 左右 ...
- LeetCode初级算法之字符串:242 有效的字母异位词
有效的字母异位词 题目地址:https://leetcode-cn.com/problems/valid-anagram/ 给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位 ...