js红宝石 第十一章-期约与异步函数

ES6新增了正式的Promise类型,还增加了async和await关键字定义异步函数的机制

11.1 异步编程

异步是为了优化因为计算量大而时间长的操作

11.1.1 同步与异步

同步就是从上往下执行

异步行为类似系统中断

异步操作经常是必要的,因为一直等着一个长时间代码是不可行的

11.1.2 以往的异步编程

以前只支持定义回调函数来表明异步操作完成

串联多个异步操作,会形成回调地狱

异步返回值

回调函数callback可以在异步操作后使用包含异步返回值的代码

      function double (value,callback){
        setTimeout(()=>callback(value*2),1000)
      }

      double(3,(x)=>console.log(x))

如果异步返回值又依赖另一个异步返回值,那么回调的情况会变得更加复杂.这种策略是不具有拓展性的,被称为回调地狱

11.2 期约 promise

11.2.2 期约基础

ES6新增的Promise类型,可以通过new操作符实例化

创建Promise时需要传入解释器(executor)作为参数,不然会报错

1.期约状态机

期约是一个有状态的对象

待定pending

兑现fulfilled 也称为解决resolved

拒绝rejected

期约的状态是私有的

2.解决值,拒绝理由以及期约用例

期约的用途:抽象的表示一个异步操作;或是在期约封装的异步操作会生成某个值,而程序期待期约状态改变时可以访问这个值

对此,每个期约只要状态切换为兑现,就会有一个私有的内部;切换为拒绝时,就会有一个私有的内部理由

3.执行函数控制期约状态

期约的状态是私有的,因此只能在内部进行操作

内部操作在期约的执行器函数中完成

执行器函数有两个功能:初始化期约的异步行为,控制状态的最终转换

控制状态的转换通过调用他的两个函数参数实现,通常命名为resolve()和reject()

调用reject()还会跑出错误

      let p1 = new Promise((resolve,reject)=>resolve())
      setTimeout(console.log,0,p1)

      let p2 = new Promise((resolve,reject)=>reject())
      setTimeout(console.log,0,p2)

 可以设置一个定时退出功能,避免一直卡在待定状态

      let p1 = new Promise((resolve,reject)=>{
        setTimeout(reject,1000)
      })

4.Promise.resolved()

调用Promise.resolved()可以实例化一个解决的期约

      // 等价
      let p1 = Promise.resolve()
      let p2 = new Promise((resolve,reject)=>resolve())

函数的第一个参数代表解决期约的值。用这个静态方法可以把所有值都转化为一个期约

      setTimeout(console.log, 0, Promise.resolve(3))

5.Promise.reject()

和.Promise.resolved()类似,但是传入的第一个参数表示的是拒绝理由

这个方法还会抛出一个错误

      setTimeout(console.log, 0, Promise.reject(3))

6.同步/异步执行的二元性 

期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介

因为代码一旦以异步模式开始执行,与之交互的方式就是使用异步结构--期约的方法

 11.2.3 期约的实例方法

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

1.实现Thenable接口

在ECMAScript暴露的异步接口中,任何对象都有then()函数,可以实现Thenable接口

2.Promise.prototype.then()

为期约实例添加处理程序的主要方法

接受两个参数:onResolved和onRejected 在进入兑现和拒绝状态时执行

      // onResloved() 和 onRejected()函数
      function onResloved(id){
        setTimeout(console.log,0,id,'resloved')
      }
      function onRejected(id){
        setTimeout(console.log,0,id,'rejected')
      }
      let p1 = new Promise((reslove,reject) => setTimeout(reslove,3000))
      let p2 = new Promise((reslove,reject) => setTimeout(reject,3000))

      p1.then(()=>onResloved('p1'),()=>onRejected('p1'))
      p2.then(()=>onResloved('p2'),()=>onRejected('p2'))

      // 3s后
      // p1 resloved
      // p2 rejected 

3.Promise.prototype.catch()

给期约添加拒绝处理程序,实际上就是个语法糖

等价于Promise.prototype.then(null,onRejectd)

4.Promise.prototype.finally()

解决或拒绝状态都会执行

只接受一个参数

目的是避免冗余代码

5.非重入期约代码

非重入特性:当期约进入落定状态时,与之相关的代码仅仅只是被排期,而后面的同步代码必定会先执行

无论如何,都是先处理同步代码

6.邻近处理程序的执行顺序

会按照顺序依次执行

7.传递解决值和拒绝理由

解决值、拒绝理由是作为resolve()、reject()的第一个参数传递的

      let p1 = new Promise((reslove,reject) => reslove('foo'))
      let p2 = new Promise((reslove,reject) => reject('bar'))

      p1.then(value => console.log(value))
      p2.catch(reason => console.log(reason))

      // 3s后
      // foo
      // bar

8.拒绝期约与拒绝错误处理

拒绝期约类似throw()表达式,会抛出未捕获错误

但是错误是在消息队列中异步抛出的,同步代码还能继续执行

异步错误只能用onRejected()处理程序捕获

11.2.4 期约连锁和期约合成

1.期约连锁

每个期约方法都能返回一个新的期约对象,因此可以形成期约连锁

      let p = new Promise((resolve, reject) => {
        console.log('1')
        resolve()
      })

      p.then(() => console.log('2'))
        .then(() => console.log('3'))
        .then(() => console.log('4'))

      // 1
      // 2
      // 3
      // 4

把例子改写就能执行异步任务了,让每一个执行器都返回一个期约实例

      let p = new Promise((resolve, reject) => {
        console.log('1')
        setTimeout(resolve, 1000)
      })

      p.then(
        () =>
          new Promise((resolve, reject) => {
            console.log('2')
            setTimeout(resolve, 1000)
          })
      )
        .then(
          () =>
            new Promise((resolve, reject) => {
              console.log('3')
              setTimeout(resolve, 1000)
            })
        )
        .then(
          () =>
            new Promise((resolve, reject) => {
              console.log('4')
              setTimeout(resolve, 1000)
            })
        )

      // 1
      // 2
      // 3
      // 4

这样子就解决了之前的回调地狱问题

2.期约图

二叉树的层序遍历

      let A = new Promise((resolve, reject) => {
        console.log('A')
        resolve()
      })

      let B = A.then(()=> console.log('B'))
      let C = A.then(()=> console.log('C'))

      let D = B.then(()=> console.log('D'))
      let E = B.then(()=> console.log('E'))
      let F = C.then(()=> console.log('F'))

3.Promise.all()  Promise.race()

Promise.all()会在一组期约全部解决之后再执行

该静态方法接收一个可迭代对象,会把可迭代对象中的元素通过Promise.resolve()转化成期约

      let p1 = Promise.all([
        Promise.resolve(3),
        Promise.resolve(4),
        Promise.reject(5)// 一次拒绝会导致合成的期约拒绝
      ])

Promise.race()和Promise.all()类似

返回第一个解决或拒绝的对象

4.串行期约的合成

基于后续期约使用之前期约的返回值来串联期约时期约的基本功能,很想函数合成

可以提炼出一个通用的合成函数

      function addTwo(x) {
        return x + 2
      }
      function addThree(x) {
        return x + 3
      }
      function addFour(x) {
        return x + 4
      }

      function compose(...fns) {
        return (x) => fns.reduce((promise, fn) => promise.then(fn), promise.resolve(x))
      }

      let addTen = compose(addTwo,addThree,addFour)

      addTen(8).then(console.log)

11.2.5 期约拓展

ES6的期约有两个未涉及的特性:

1.期约取消

ES6的期约只要开始执行,就无法停止直到完成

这个特性在现有实现基础上提供一种临时封装就可以实现

      class CancelToken{
        constructor(cancelFn){
          this.promise = new Promise((resolve,reject)=>{
            cancelFn(resolve)
          })
        }
      }

2.期约进度通知

可以拓展Promise类,为它添加notify()方法

class TrackablePromise extends Promise {
  constructor(executor){
    const notifyHandlers = []

    super((resolve,reject)=>{
      return executor(resolve,reject,(status)=>{
        notifyHandlers.map((handler) => handler(status))
      })
    })
  }

  notify(notifyHandler){
    this.notifyHandlers.push(notifyHandler)
    return this
  }
}

11.3 异步函数

也成为async/await(语法关键字)

11.3.1 异步函数

async用于声明异步函数,可以让函数拥有异步特征。但总体还是同步求值的

但是如果异步函数使用return返回了值,那么这个值会被Promise.resolved()包装成期约对象

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

      foo().then(console.log)
      console.log(2);

在异步函数中使用throw()抛出的错误会返回拒绝的期约,可以被Promise.catch()捕捉到

但是使用Promise.reject()返回的不能被捕捉

await

await关键字在async函数中使用,会暂停异步执行函数后面的代码,等期约执行后再执行

单独的Promise.reject()不会被异步函数捕捉,但是加上await就会使拒绝期约返回

      async function foo(){
        console.log(1);
        await Promise.reject(3)
        console.log(4);// 不会执行
      }
      foo().catch(console.log)
      console.log(2);

11.3.2 停止和恢复执行

异步函数不包含await,和普通函数没什么区别

await不只是等待一个值这么简单。当函数执行到可用关键字后,就算后面跟着一个立即可用的值,其余部分也会进入异步队列中

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

 

11.3.3 异步执行策略

1.实现sleep()

      async function sleep(delay){
        return new Promise((resolve) => setTimeout(resolve,delay))
      }

      async function foo(){
        const t0 = Date.now()
        await sleep(1500)// 暂停
        console.log(Date.now()-t0);
      }

2.平行执行

使用await可能错过平行加速的机会

      async function randomDelay(id){
        // 延迟0-1000ms
        const delay = Math.random() * 1000
        return new Promise((resolve) => setTimeout(() => {
          console.log(`${id} finished`);
          resolve()
        }, delay))
      }

      async function foo(){
        const t0 = Date.now()
        for(let i=0;i<5;i++){
          await randomDelay(i)
        }

        console.log(`${Date.now() - t0}ms delay`);
      }

      foo()

顺序等待了5个随机的事件

优化一下代码

      async function randomDelay(id){
        // 延迟0-1000ms
        const delay = Math.random() * 1000
        return new Promise((resolve) => setTimeout(() => {
          console.log(`${id} finished`);
          resolve(id)
        }, delay))
      }

      async function foo(){
        const t0 = Date.now()
        
        const promise = Array(5).fill(null).map((_,i) => randomDelay(i))

        for(const p of promise){
          console.log(`awaited ${await p}`);
        }

        console.log(`${Date.now() - t0}ms delay`);
      }

      foo()

虽然不是按顺序执行的,但是await按顺序收到了每个期约的值

3.串行执行期约

      async function addTwo(x) {
        return x + 2
      }
      async function addThree(x) {
        return x + 3
      }
      async function addFour(x) {
        return x + 4
      }

      async function addTen(x){
        for(const fn of [addTwo,addThree,addFour]){
            x = await fn(x)
        }
        return x;
      }

      addTen(8).then(console.log)

 

4.栈追踪与内存管理

期约与异步函数功能类似,但是在内存中差别很大

JS引擎在创建期约时会尽可能保存调用栈信息,这回占用一些内存,但是可以在抛出错误时由运行时的错误逻辑获取

但是使用异步函数不会带来额外的内存消耗,在重视性能的应用中可以优先考虑

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值