Promise是ES6异步编程的一种解决方案(目前最先进的解决方案是async和await的搭配(ES8),但是它们是基于promise的),从语法上讲,Promise是一个对象或者说是构造函数,用来封装异步操作并可以获取其成功或失败的结果。
最重要也是最主要的一个场景就是ajax和axios请求。通俗来说,由于网速的不同,可能你得到返回值的时间也是不同的,但是我们下一步要执行的代码依赖于上一次请求返回值,这个时候我们就需要等待,结果出来了之后才知道怎么样继续下去。
promise的好处:
- 防止出现回调地狱;
- 提高代码的可读性;
- 像同步操作那样去执行异步操作。
前置说明
ECMAScript 6 新增了正式的 Promise(期约)引用类型,支持优雅地定义和组织异步逻辑。接下来几个版本增加了使用 async 和 await 关键字定义异步函数的机制。
JavaScript 是单线程事件循环模型。异步行为是为了优化因计算量大而时间长的操作,只要你不想为等待某个异步操作而阻塞线程执行,那么任何时候都可以使用。
同步与异步
JS中同步任务会立即执行并放入调用栈中,而异步任务会被放入事件队列中,等待调用栈中的任务执行完毕后再被推入调用栈中执行。当异步任务被推入调用栈中执行时,它就变成了同步任务。这种机制被称为事件循环。
同步行为
对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。这样的执行流程容易分析程序在执行到代码任意位置时的状态(比如变量的值)。
例如
1 | let x = 3; |
在程序执行的每一步,都可以推断出程序的状态。这是因为后面的指令总是在前面的指令完成后才会执行。等到最后一条指定执行完毕,存储在 x 的值就立即可以使用。
异步行为
类似于系统中断,即当前进程外部的实体可以触发代码执行。异步代码不容易推断
例如,在定时回调中执行一次简单的数学计算:
1 | let x = 3; |
这段程序虽与上面同步代码执行的任务一样,都是把两个数加在一起,但这一次执行线程不知道 x 值何时会改变,因为这取决于回调何时从消息队列出列并执行。
虽然这个例子对应的低级代码最终跟前面的例子没什么区别,但第二个指令块(加操作及赋值操作)是由系统计时器触发的,这会生成一个入队执行的中断。到底什么时候会触发这个中断,这对 JavaScript 运行时来说是一个黑盒。
异步编程主要包含以下几类,异步编程传统的解决方案是回调函数
,这种传统的方式容易导致“回调地狱”,即函数多层嵌套,让代码难以阅读,维护困难,空间复杂度大大增加。
fs 文件操作
1
require('fs').readFile('./index.html', (err,data)=>{})
数据库操作
AJAX
1
$.get('/server', (data)=>{})
定时器
1
setTimeout(()=>{}, 2000);
Promise 对象
Promise属于ES6规范, 是异步编程的一种解决方案
,比传统的解决方案——回调函数和事件
——更合理和更强大。期约故意将异步行为封装起来,从而隔离外部的同步代码。
- 从语法上来说: Promise 是一个构造函数
- 从功能上来说: promise 对象用来封装一个异步操作并可以获取其成功/失败的结果值
1 | const p=new Promise(()=>{}) |
Promise 的状态及结果
Promise 构造函数: Promise (excutor)
- executor 函数: 执行器 (resolve, reject) => {}
- executor 会在 Promise 内部立即同步调用(即executor函数直接放入调用栈中,而不是消息队列,new Promise()执行时就会立即执行executor 函数),异步操作在执行器中执行
1 | let p = new Promise((resolve, reject) => { |
期约是一个有状态的对象,Promise实例对象中的属性PromiseState存储着该Promise实例的状态,可能处于如下 3 种状态之一:
- 待定(pending):期约的最初始状态,在待定状态下,期约可以落定(settled)为resolved状态或reject状态。无论落定为哪种状态都是不可逆的。只要从待定转换为解决或拒绝,期约的状态就不再改变。
例如使用一个空函数对象来应付一下解释器:
1 | let p = new Promise(() => {}); |
之所以说是应付解释器,是因为如果不提供执行器函数,就会抛出 SyntaxError。
无论 resolve()和 reject()中的哪个被调用,状态转换都不可撤销了。于是继续修改状态会静默失败,如下所示:
1 | let p = new Promise((resolve, reject) => { |
- 解决(resolved,有时候也称为“兑现”,fulfilled):代表成功
- 拒绝(rejected):代表失败
无论变为成功还是失败, 都会有一个结果数据(这个数据存储在Promise实例对象的PromiseResult属性中),成功的结果数据一般称为 value, 失败的结果数据一般称为 reason
期约主要有两大用途:
- 首先是抽象地表示一个异步操作。期约的状态代表期约是否完成。“待定”表示尚未开始或者正在执行中。“解决”表示已经成功完成,而“拒绝”则表示没有成功完成
- 在另外一些情况下,期约封装的异步操作会实际生成某个值,而程序期待期约状态改变时可以访问这个值。相应地,如果期约被拒绝,程序就会期待期约状态改变时可以拿到拒绝的理由。
为了支持这两种用例,每个期约只要状态切换为解决,就会有一个私有的内部值(value)。类似地,每个期约只要状态切换为拒绝,就会有一个私有的内部理由(reason)。无论是值还是理由,都是包含原始值或对象的不可修改的引用。二者都是可选的,而且默认值为 undefined。在期约到达某个落定状态时执行的异步代码始终会收到这个值或理由。
由于期约的状态是私有的,所以只能在内部进行操作。内部操作在期约的执行器函数中完成。执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。其中,控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为 resolve()和 reject()。调用resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。另外,调用 reject()也会抛出错误
1 | let p1 = new Promise((resolve, reject) => resolve()); |
所谓Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理
有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易
Promise方法
Promise.prototype.then()
Promise.prototype.then([onResolved|null],[onRejected|null])是为期约实例添加处理程序的主要方法。
参数:onResolved 处理程序和 onRejected 处理程序。这两个参数都是可选的,如果提供的话,则会在期约分别进入“兑现”和“拒绝”状态时执行。
返回值:一个新的Promise对象
可以指定多个回调,当promise对象变为相应的状态的时候就都会执行.
1 | let p = new Promise((resolve, reject) => { |
问:then()返回的Promise对象的状态是如何决定的呢?
① 如果抛出异常, 新 promise 变为 rejected, reason 为抛出的异常
1 | let p = new Promise((resolve, reject) => { |
② 如果返回的是非 promise 的任意值, 新 promise 变为 resolved, value 为返回的值
1 | let p = new Promise((resolve, reject) => { |
③ 如果返回的是另一个新 promise, 此 promise 的结果就会成为新 promise 的结果
1 | let p = new Promise((resolve, reject) => { |
不管调用的是onResolved还是onRejected函数,返回的新promise对象主要由返回值决定(抛出错误的情况除外)
then()的链式调用
问:promise 如何串连多个操作任务?
(1) promise 的 then()返回一个新的 promise, 可以形成 then()的链式调用
(2) 通过 then 的链式调用串连多个同步/异步任务
1 | let p = new Promise((resolve, reject) => { |
1 | p.then(value => { |
中断then()链
方法:在then()的回调函数中返回一个 pendding 状态的 promise 对象。因为pendding状态的promise对象不会触发onResolved()或onRejected()函数
1 | let p = new Promise((resolve, reject) => { |
穿透:当没有指定相应Promise状态的回调函数时,就可以跳过执行该then()
练习:分析输出结果
1 | let p = new Promise((resolve, reject) => { |
分析:p.then(null,reason => { console.log(111);})的返回值是Promise (resolved):undefined, 后面没有对应的onResolved的回调函数,就跳过了执行,所以控制台只输出了111
1 | let p = new Promise((resolve, reject) => { |
p.then(value => {throw ‘失败啦!’;})返回Promise (rejected): ‘失败啦!’,后面没有对应的onRejected()回调函数,就跳过了执行,所以就直接执行catch()来捕获错误信息
Promise.prototype.catch()
Promise.prototype.catch(onRejected)
catch()基于then()做了一个单独的封装,只接收rejected状态的回调函数
给Promise对象设置回调函数的方法有Promise.prototype.then()和Promise.prototype.catch(),注意Promise.prototype.catch()只能指定错误的回调函数
Promise.resolve()
静态方法,不是实例方法
期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。通过调用Promise.resolve()静态方法,可以实例化一个解决的期约。
下面两个期约实例实际上是一样的:
1 | let p1 = new Promise((resolve, reject) => resolve()); |
参数:成功的数据或 promise 对象
说明: 返回一个成功/失败的 promise 对象
- 参数为非Promise对象,则返回一个成功的Promise对象,成功的结果为该参数
1 | let p1 = Promise.resolve(521); |
1 | let p = Promise.resolve(new Error('foo')); |
- 参数为Promise对象时,则返回该Promise对象,对这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此,Promise.resolve()可以说是一个幂等方法
1 | let p = Promise.resolve(7); |
1 | let p2 = Promise.resolve(new Promise((resolve, reject) => { |
Promise.reject()
与 Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误(这个错误不能通过 try/catch 捕获,而只能通过拒绝处理程序捕获)。
下面的两个期约实例实际上是一样的:
1 | let p1 = new Promise((resolve, reject) => reject()); |
参数:失败的理由或 promise 对象
说明: 返回一个失败的 promise 对象
- 参数为非Promise对象时,返回的失败Promise对象的理由就为该参数值
1 | let p1 = Promise.reject(521); |
- 参数为Promise对象时,返回的失败Promise对象的理由就为该参数值(即失败的理由就是该Promise对象)
1 | let p2 = Promise.reject(new Promise((resolve, reject) => { |
Promise.all()
参数: 包含 n 个 promise 的数组
说明: 返回一个新的 promise, 只有所有的 promise 都成功该promise才成功,成功结果为所有promise成功结果组成的数组。只要有一个promise失败了该promise就失败,失败理由为第一个失败的promise的失败理由
- 当所有promise都成功时,返回一个新的成功的promise,成功结果为所有promise成功结果组成的数组
1 | let p1 = new Promise((resolve, reject) => { |
- 当有失败的promise时该promise就失败,失败理由为第一个失败的promise的失败理由
1 | let p1 = new Promise((resolve, reject) => { |
Promise.race()
通过这种方式,可以检测页面中某个请求是否超时,并输出相关的提示信息。
参数: 包含 n 个 promise 的数组
说明: 返回一个新的 promise, 该promise等于第一个完成的 promise(即第一个确定状态的promise)
1 | let p1 = new Promise((resolve, reject) => { |
改变Promise对象状态的方式
- promise回调函数中改变
1 | let p = new Promise((resolve, reject) => { |
- 调用实例方法Promise.resolve()或Promise.reject()
ES6 规定,Promise
对象是一个构造函数,用来生成Promise
实例
1 | const promise = new Promise(function(resolve, reject) { |
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己部署
Promise
实例生成以后,可以用then
方法分别指定resolved
状态和rejected
状态的回调函数。
1 | promise.then(function(value) { |
下面是一个抽奖示例
1 | <body> |
加载图片资源例子
1 |
|
Promise对象_Ajax实操
1 |
|
util.promisefy()函数
传入一个遵循常见的错误优先的回调风格的函数(即以(err, value)=>{…}回调作为最后一个参数),并返回一个返回值为promise对象的函数版本
“错误优先”是指回调函数中,错误参数为回调函数的第一个参数,node.js环境中fs模块中的异步api大多是这种风格的
参数:函数
返回值:函数,返回的该函数的返回值是promise对象
将函数promise化的好处:函数promise化之后,函数的返回值就变成了promise对象,这样就可以调用promise的实例方法then()或catch(),利用它们的链式写法,就能避免回调函数多层嵌套
示例:调用util.promisefy()函数,将fs.readFile()函数promise化
1 | // 引入 util 模块 |
封装promisefy函数
在实际应用中,一个函数满足这几个条件,就可以被 promisify 化:
- 该方法必须包含回调函数
- 回调函数必须执行
- 回到函数第一个参数代表 err 信息,第二个参数代表成功返回的结果
1 | // promisefy()返回一个函数,返回的该函数的返回值是Promise对象 |
封装Promise类实战
1 | // 在异步任务中不能抛出错误,即使是内置的Promise也捕获不到 |
Async 函数
async 英文单词的意思是异步,虽然它是 ES8 中新增加的一个关键字,但它的本质是一种语法糖写法(语法糖是一种简化后的代码写法,它能方便程序员的代码开发),async 通常写在一个函数的前面,表示这是一个异步请求的函数,将返回一个 Promise 对象,并可以通过 then 方法取到函数中的返回值
使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。而在参数或闭包方面,异步函数仍然具有普通 JavaScript 函数的正常行为。异步函数的返回值会被 Promise.resolve()包装成一个期约对象。异步函数始终返回期约对象。
ES2017 标准引入了 async 函数,使得异步操作变得更加方便
async函数可以将异步操作变为同步操作
- 函数的返回值为 promise 对象
- promise 对象的结果由 async 函数执行的返回值决定
- 如果返回值是一个非Promise类型的数据,相当于执行了Promise.resolve(), 则返回的Promise实例状态为fulfilled
- 如果返回值是一个Promise对象,相当于执行了Promise.resolve(返回值),则返回的Promise实例等效于该Promise对象
1 | async function main(){ |
示例代码
1 | function print(){ |
基本语法
1 | function timeout(ms) { |
异步应用
1 | function ajax(url){ |
await 表达式
await 可以理解为 async wait 的简写,表示等待异步执行完成。
await 后可以返回任意的表达式,如果是正常内容,则直接执行,如果是异步请求,必须等待请求完成后,才会执行下面的代码
- await 右侧的表达式一般为 promise 对象, 但也可以是其它的值
- 如果表达式是 promise 对象, await 返回的是 promise 成功的值, promise对象状态是rejected时,需要捕获错误来查看错误理由
- 如果表达式是其它值, 直接将此值作为 await 的返回值
注意
- await 必须写在 async 函数中, 但 async 函数中可以没有 await
- 如果 await 的 promise 失败了, 就会抛出异常, 需要通过 try…catch 捕获处理
1 | // 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求 |
根据页面效果,源代码解析如下:
- fn 函数执行后,首先,会按照代码执行流程,先输出“1.开始”。
- 其次,对于没有异步请求的内容,在 await 后面都将会正常输出,因此,再输出“2.正在操作”。
- 如果 await 后面是异步请求,那么,必须等待请求完成并获取结果后,才会向下执行。
- 根据上述分析,由于 方法 p 是一个异步请求,因此,必须等待它执行完成后,并将返回值赋给变量 p1,再执行向下代码。
- 所以,最后的执行顺序是,先输出 “3.异步请求”,再输出 “4.结束”,在 async 函数中的执行顺序,如下图所示。
1 | async function main(){ |
async与await结合实践
1.txt
1 | 观书有感 |
2.txt
1 | 问渠那得清如许? |
3.txt
1 |
|
1 | const fs = require('fs') |
async与await结合发送AJAX请求
1 | // axios是基于Promise封装的AJAX请求库 |