期约(promise)、异步函数(async/await)

219 阅读7分钟

以下参考红宝书第11章“期约与异步函数”


期约

同步:南海造内存中顺序执行的处理器指令。

异步:类似于系统中断,当前进程外部的实体可以触发代码执行。(同步操作中必须要等长时间的操作结束后才能执行下一个,异步可以暂时中断,等同步任务都结束了再执行)

以往的异步编程

  1. 传值,用回调实现
function double(value,callback){
  setTimeout(()=>callback(value*2),1000)
}
double(3,(x)=>console.log(`given:${x}`))
//given:6

    将value*2通过回调传到x

  1. 操作的失败处理try...catch...
  2. 嵌套(异步返回值又依赖另一个异步返回值)

    回调的嵌套越来越复杂,称为回调地狱。

期约的理解

期约形象化理解为可以给你的异步操作提供一个期约(类似于叫号单,可以凭此去拿东西),先暂停一下这个函数的执行,等所有同步代码执行完毕之后,处理期约当中的代码。

期约是一个有状态的对象,包含三种状态:pending(待定)、fulfilled(完成,有时称为resolved)、rejected(拒绝),一旦定为完成或者拒绝,其状态就不可逆了,不可更改,一直保持下去。

期约用途:

  • 抽象的表明异步的动作,即表明异步现在的状态,是否完成。
  • 传递异步操作的返回值。完成就传递返回值(例如HTTP请求返回的json数据);拒绝就传递对应的理由。

执行器函数

由于期约的状态是私有的(避免javascript读取到状态而以同步的方式来处理期约对象),所以内部的操作在期约的执行器函数当中完成。

  1. 执行器作用:
  • 初始化期约(执行器是同步执行的)
  • 控制状态的转换。通过resolve()和reject()两个函数来转换状态
let p1 = new promise((resolved,reject)=> reslove())
//打印结果为:Promise<resolved>
//只能通过setTimeout(cnosole.log,0,p1)来打印,因为状态改变为异步任务。同步无法捕捉。

因为状态一经确定就不可更改了,所以resolve()和reject()只能被调用其中一个,接着调用第二个是无效的。

let p2 = new promise((resolved,reject)=>{
  resloved();
  reject();//无效
} )
//Promise<resolved>
  1. Promise.resolve()

期约必需通过执行器才能将待定状态转换为落定状态。但直接调用Promise.resolve()也可以实例化一个状态为完成的期约。以下两种是等效的。

//以下两者等效
let p1 = new promise((resolved,reject)=> reslove())
let p2 = Promise.resolve()
//Promise<resolved>

Promise.resolve()有幂等性,如果传入的参数本身就是一个期约,那再通过Promise.resolve()包装多少次,最终也还是一样的,等同于期约。

  1. Promise.reject()

同理Promise.reject()等效于执行器中调用reject()。

但不同的是,如果传递的是期约,会当作拒绝的理由,而不会将期约传递下去。

期约的实例方法

期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁。

  1. Thenable接口

异步结构中,任何对象都有一个then()方法。拥有then方法的称为thenable接口。Promise类型实现了Thenable接口(即有then方法)

  1. then()

then()是为期约实例添加处理程序的主要方法。其最多接受两个参数onResolved( )和onRejected( )。第一个参数为onResolved函数(无的时候用null表示),第二个参数为onRejected( )。两个函数分别在期约进入fulfilled和rejected状态时执行。

let p1 = new Promise((resolve, reject) => setTimeout(resolve, 1000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 1000));

function onResolved(data) {
  setTimeout(console.log, 0, data, "resolved");
}
function onRejected(reason) {
  setTimeout(console.log, 0, reason, "rejected");
}
p1.then(
  () => onResolved("p1"),
  () => onRejected("p1")
);
p2.then(
  () => onResolved("p2"),
  () => onRejected("p2")
);
//p1 resolved
//p2 rejected

then方法调用之后会返回一个新的期约实例,该新期约实例如果是“完成”状态,则依据onResolved函数的返回值来构建。

let p1 = Promise.resolve("foo");
let p2 = p1.then();//Promise<resolved>:foo  因为没有传入处理程序,直接往后传
let p3 = p1.then(()=>undefined);//Promise<resolved>:undefined
let p4 = p1.then(()=>{ });//Promise<resolved>:undefined

当返回的是期约时,就按照期约的状态来确定。

let p5 = p1.then(()=>new Promise(()=>{}))//Promise<pending>
let p6 = p1.then(()=>Promise.reject())//Promise<resolved>:undefined

同样,该实例若是“拒绝”状态,则根onRejected函数的返回值来构建。

  1. catch( )

给期约添加拒绝处理程序,只接受一个参数:onRejected处理程序。实际就是一个语法糖,调用catch( )相当于调用then(null, onRejected)。

catch和then(null, onRejected)一样也会返回一个新的期约实例。

  1. finally()

无论状态是“完成”还是“拒绝”,都会执行此当中的处理程序。主要用来清理代码。

如果父期p1的状态确定,那么finally返回的期约实例是父期约的状况。

例如将之前的then换为finally之后,p2,p3,p4,p5都只会显示为“Promise:foo” 。与finally的处理程序无关。

let p1 = Promise.resolve("foo");
let p2 = p1.then();//Promise<resolved>:foo
let p3 = p1.then(()=>undefined);//Promise<resolved>:foo
let p4 = p1.then(()=>{ });//Promise<resolved>:foo
let p5 = p1.then(()=>new Promise(()=>{}))//Promise<resolved>:foo
let p6 = p1.then(()=>Promise.reject())//Promise<resolved>:foo

如果返回的是一个待定的期约或者显式返回一个拒绝期约的时候时,则会返回相应的期约。

  1. 执行顺序

执行器当中是同步代码,按顺序执行。但当状态确定之后,then()当中的代码不会立即执行,而是会等到所有(包括then之后的)的同步代码执行完毕之后才开始执行,是一个微任务(此处可看一下event loop时间循环当中的宏任务,微任务)

  1. 传递完成值和拒绝理由

到了落定状态后,期约会提供对应处理程序对返回值进行操作。在执行函数中,解决的值和拒绝的理由分别作为resolve()和reject()的第一个参数往后传。然后这些值又会传给它们各自的处理程序,作为onResolved或onRejected处理程序的唯一参数。

let p1 = new Promise((resolve, reject) => resolve('foo'));
p1.then((value)=>console.log(value));//foo
let p2 = new Promise((resolve, reject) => reject('bar'));
p1.catch((reason)=>console.log(reason));//bar
  1. 拒绝处理

异步错误只能通过异步的onRejected()来捕获,不能用try...catch...这种同步错误捕获。正确方式如下:

Promise.reject(Error('foo')).catch((e)=>{})

期约连锁与期约合成

因为每个期约实例的方法(then,catch,finally)都会返回一个新的期约对象,所以可以将期约串联起来,构成所谓的“期约连锁”。链式调用 p1.then(...).then(...).then(...)进行依次执行。如此即可串行化异步任务。解决回调地狱问题。

Promise.all( )

promise.all( )创建的合成期约会等所有期约全部落定为“完成”状态之后才“完成”,传入的多个对象以数组的形式呈现

let p = Promise.all([Promise.resolve(),
                     new Promise((resolve)=>setTimeout(resolve,1000))
]);
//打印p,Promise<pending>
p.then(()=>setTimeout(console.log,0,"all resolved!!"))
//"all resolved!!"(大概1s后)

至少有一个期约待定,合成期约就会待定,只要有一个拒绝,合成期约就会拒绝。

所有期约全部“完成”之后,合成期约的“完成”值就是包含所有期约“完成”的数组。

如果有一个期约拒绝,就以第一个期约的拒绝理由为合成期约的拒绝理由。

Promise.race( )

Promise.race返回一个包装期约,是一组集合中最先“完成”或“拒绝”的期约的镜像。

期约扩展

期约取消:期约正在处理过程,但程序已经不想要其结果,如何取消?

期约进度通知:有时候需要监控期约的执行进度。

上述两者ES6均不支持。原因是会导致期约连锁与期约合成过度复杂化。

异步函数

亦称为“async/await”,为期约在ECMAScript函数当中的应用。为ES8新增的,让同步代码能够异步执行。旨在解决异步结构组织代码的问题。

使用async关键字可以让函数具有异步特征,但整体上代码还是同步求值的。不过,异步函数如果使用了return关键字返回了值(无return返回undefined),这个值会被Promise.resolve( )包装成一个期约对象。异步函数始终返回期约对象,在函数外部调用这个函数可以得到返回的期约。

async所在的函数中,如果有return返回值,该值会自动包装成一个Promise.resolve( )对象。

async function foo() {
  console.log(1);
  return 3;
}
foo().then(console.log);
console.log(2);
//1
//2
//3

异步函数return一个已解决期约,则返回值被当作已经解决的期约。

await与async搭配使用。async声明异步函数,但该异步函数还是同步执行,await会暂停执行异步函数后面的代码,等异步函数外的同步代码执行结束后,等待期约来解决。

await右边的值可用的时候,会向消息队列中推送一个任务,该任务会恢复异步函数执行。

async function foo() {
  console.log(2);
  await null;
  console.log(4);
}
console.log(1);
foo();
console.log(3);
//1,2,3,4