夜听城嚣 夜听城嚣
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏

dwfrost

前端界的小学生
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏
  • 第1章 什么是JavaScript
  • 第2章 HTML中的JavaScript
  • 第3章 语言基础
  • 第4章 变量、作用域与内存
  • 第5章 基本引用类型
  • 第6章 集合引用类型
  • 第7章 迭代器与生成器
  • 第8章 对象、类与面向对象编程
  • 第9章 代理与反射
  • 第10章 函数
  • 第11章 异步编程
    • 第12章 BOM
    • 第14章 DOM
    • 第15章 DOM扩展
    • 第16章 DOM2和DOM3
    • 第17章 事件
    • 第18章 动画与Canvas图形
    • 第19章 表单脚本
    • 第20章 JavaScript API
    • 第21章 错误处理与调试
    • 第23章 JSON
    • 第24章 网络请求与远程资源
    • 第25章 客户端存储
    • 第26章 模块
    • 第27章 工作者进程
    • 第28章 最佳实践
    • 《JavaScript高级程序设计》
    frost
    2022-01-02

    第11章 异步编程

    本章节主要介绍Promise的特性和使用,优雅地执行异步任务。

    # 异步编程

    • 同步任务

      对应内存中顺序执行的处理器指令。上一条指令执行完成后,可以立即得到作用后的状态。比如,

      let x = 1 // 初始化定义
      x += 1 // 更改值
      console.log(x) // 获取值 2
      
      1
      2
      3

      同步意味着顺序,阻塞,也给出明确的结果。

    • 异步任务

      类似系统中断,即代码在当前进程外部执行。好处在于任务不会阻塞,可以并行执行,但同样不容易推断结果。

      let x = 1 // 初始化定义
      setTimeout(() => {
        x += 1 // 更改值
      }, 0)
      console.log(x) // 获取值 1
      
      1
      2
      3
      4
      5

      以往是通过回调函数的方式来获取异步任务的结果,不过这会引发著名的“回调地狱”。

      function test(callback) {
        let x = 1 // 初始化定义
        setTimeout(() => {
          x += 1 // 更改值
          callback(x)
        }, 0)
      }
      
      test((res) => {
        console.log(res) // 获取值 2
      })
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

    # Promise

    ES6新增的一个引用类型Promise,使用方式如下

    let p = new Promise(()=>{}) // 传入执行器函数
    
    1

    # Promise状态机

    • pending 待定状态
    • resolved 解决状态,附带一个解决值。
    • rejected 拒绝状态,附带一个拒绝理由。

    注意这3种状态都有可能存在,并且不可修改和直接通过js检测。

    let p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('ok')
      }, 0)
    })
    console.log(p1) // Promise { <pending> }
    setTimeout(() => {
      console.log(p1) // Promise { 'ok' }
    }, 100)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('ok')
      }, 0)
    })
    console.log(p1) // Promise { <pending> }
    setTimeout(() => {
      console.log(p1) //  UnhandledPromiseRejectionWarning: ok
    }, 100)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    # Promise.resolve()

    该静态方法可以直接返回一个resolved状态的Promise实例,如果有值,会包装后返回。下面两个实例是等价的。

    let p1 = new Promise((resolve) => resolve())
    let p2 = Promise.resolve()
    
    1
    2

    此外该方法是一个幂等方法。

    let p = Promise.resolve(3)
    setTimeout(() => {
      console.log(p === Promise.resolve(p)) // true
      console.log(p === Promise.resolve(Promise.resolve(p))) // true
    }, 0)
    
    1
    2
    3
    4
    5

    # Promise.reject()

    与Promise.resolve()用法类似,区别是非幂等的,它接收的参数都会作为拒绝promise的理由。

    let p1 = new Promise((resolve, reject) => reject())
    let p2 = Promise.reject()
    
    1
    2
    let p = Promise.reject(Promise.resolve(10))
    console.log(p) // Promise { <rejected> Promise { 10 } }
    // UnhandledPromiseRejectionWarning: #<Promise>
    
    1
    2
    3

    注意try/catch不能捕获异步模式的错误,只能捕获同步模式的错误。

    promise的错误只能通过promise的方法或者async/await下的try/catch来捕获。

    try {
      throw {
        a: '1',
      }
    } catch (error) {
      console.log('error:', error) // error: { a: '1' }
    }
    
    async function test() {
      try {
        // await Promise.reject({ reason: 'promise is not return' })
        Promise.reject({ reason: 'promise is not return' })
          .then((res) => {
            console.log('res', res)
          })
          .catch((e) => {
            console.log('e', e) // e { reason: 'promise is not return' }
          })
      } catch (err) {
        console.log('err:', err)
      }
    }
    
    test()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    # Promise.prototype.then()

    接收最多2个回调参数,第一个回调在进入resolved状态时执行,第二个回调在进入rejected状态时执行。返回值为新的promise实例。

    let p = new Promise((resolve, reject) => resolve(3))
    p.then(
      (result) => {
        console.log(result)
      },
      (error) => {
        console.log(error)
      }
    )
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    # Promise.prototype.catch()

    相当于Promise.prototype.then(null,onRejected)的语法糖。

    let p = Promise.reject()
    p.catch(() => {
      console.log('rejected')
    })
    
    1
    2
    3
    4

    # Promise.prototype.finally()

    在promise切换为resolved或rejected时都会执行。一个完整的promise调用如下:

    let p = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(1)
      }, 0)
    })
    
    p.then((res) => {
      console.log(res)
    })
      .catch((err) => {
        console.log(err)
      })
      .finally(() => {
        // 无法获取是否resolved或rejected
        console.log('finally')
      })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    # 执行顺序

    let p = new Promise((resolve) => {
      console.log(1)
      resolve('promise')
      console.log(2)
    })
    
    p.then(() => {
      console.log(4)
    })
    
    console.log(3)
    // 1 2 3 4
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    再看个复杂点的

    setTimeout(() => {
      console.log(5)
    }, 0)
    Promise.resolve().then(() => {
      console.log(3)
    })
    let p = new Promise((resolve) => {
      setTimeout(() => {
        console.log(6)
        resolve('promise')
        console.log(7)
      }, 0)
      Promise.resolve().then(() => {
        console.log(4)
      })
      console.log(1)
    })
    
    p.then(() => {
      console.log(8)
    })
    
    console.log(2)
    
    // 1 2 3 4 5 6 7 8
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    1. 5是异步宏任务,挂起

    2. 3是异步微任务,挂起

    3. 6,7是异步宏任务,挂起

    4. 4是异步微任务,挂起

    5. 1是同步任务,执行 输出1

    6. 8是异步微任务,此时还未resolved,不执行

    7. 2是同步任务,执行 输出2

    8. 执行优先级:微任务>宏任务,输出3

    9. 输出4 此时8也是微任务,但Promise还没有扭转状态

    10. 同优先级的异步任务,看谁先在任务队列等候。 输出5 输出6

    11. 扭转Promise状态后,7在任务队列等待执行,8会让它先出列执行。输出7 8

    注意第11步,这个顺序有Js运行时保证,被称为“非重入”特性。

    处理程序会等到运行的消息队列让它出列时才执行。

    # 拒绝错误处理

    promise可以以任何理由拒绝,包括undefined,但最好统一使用错误对象。因为创建错误对象可以让浏览器捕获错误对象中的栈追踪信息,方便调试。

    正常情况下,通过throw()关键字抛出错误时,JS运行时的错误处理机制会停止执行抛出错误之后的任何指令:

    function outputError() {
      throw Error('foo')
      console.log('bar') // 不执行
    }
    outputError()
    console.log(1) // 不执行
    
    1
    2
    3
    4
    5
    6

    但是,在promise中,异步错误是从消息队列中抛出的,不会阻止运行时继续执行同步指令:

    function rejectError() {
      Promise.reject(Error('foo')) // UnhandledPromiseRejectionWarning: Error: foo
      console.log('bar') // bar
    }
    
    rejectError()
    
    console.log('next ') // next
    
    1
    2
    3
    4
    5
    6
    7
    8

    # 异步串行

    其实就是.then().then()结构,举个例子

    function delay(time) {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve()
        }, time * 1000)
      })
    }
    function run() {
      delay(1)
        .then(() => {
          console.log(1)
          return delay(1)
        })
        .then(() => {
          console.log(2)
          return delay(1)
        })
        .then(() => {
          console.log(3)
        })
    }
    run()
    // 1 (1秒后)
    // 2 (2秒后)
    // 3 (3秒后)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    改写下,变得更简洁。

    function delay(time, str) {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log(str)
          resolve()
        }, time * 1000)
      })
    }
    
    function run() {
      delay(1, 1)
        .then(() => delay(1, 2))
        .then(() => delay(1, 3))
    }
    
    run()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    使用async/await,更简洁。

    async function run() {
      await delay(1, 1)
      await delay(1, 2)
      await delay(1, 3)
    }
    
    1
    2
    3
    4
    5

    # 异步并行

    # Promise.all()

    创建的Promise会在一组promise全部解决之后再解决。使用如下:

    let p = Promise.all([
      new Promise((resolve) => {
        setTimeout(() => {
          resolve(1)
        }, 1000)
      }),
      Promise.resolve(2),
    ])
    p.then((res) => {
      console.log(res) // [1,2] (1秒钟后)
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let p = Promise.all([
      new Promise((resolve) => {
        setTimeout(() => {
          resolve(1)
        }, 1000)
      }),
      Promise.resolve(2),
      new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(3)
        }, 1000)
      }),
      Promise.reject(4),
    ])
    p.then((res) => {
      console.log('result', res)
    }).catch((err) => {
      console.log('reject:', err) // 返回第一个reject
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    注意:

    • 如果所有promise都成功解决,则Promise.all的解决值就是包含所有promise解决值的数组,顺序不变。
    • 如果有promise拒绝,则第一个拒绝的promise会将自己的理由作为Promise.all的拒绝理由。
    # Promise.race()

    返回一个包装promise,是一组集合中最先解决或拒绝的promise镜像。

    let p = Promise.race([
      new Promise((resolve) => {
        setTimeout(() => {
          resolve(1)
        }, 2000)
      }),
      new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(2)
        }, 1000)
      }),
    ])
    p.then((res) => {
      console.log('result', res)
    }).catch((err) => {
      console.log('reject:', err) // reject: 2
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    即不管是resolved还是rejected,Promise.race只看谁第一个落定promise,就已谁作为值返回。

    # Promise扩展

    ES6还未涉及的2个特性:promise取消和进度追踪。本文就不细说了。

    # 异步函数

    即async/await。

    # async

    用于声明异步函数,大多情况下异步函数和同步函数区别不大,唯一区别在于return。

    异步函数,如果return返回了值,那这个值会被Promise.resolve()包装成一个promise对象。

    async function foo() {
      console.log(3)
      return 1
    }
    // 等价于
    // function foo() {
    //   console.log(3)
    //   return Promise.resolve(1)
    // }
    
    foo().then((res) => {
      console.log(res)
    })
    
    console.log(2)
    // 3 2 1
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    注意,此时foo返回值是一个promise对象,而不是数值1。

    async function foo() {
      console.log(3)
      return 1
    }
    
    const a = foo()
    console.log(a)
    console.log(2)
    // 3
    // Promise { 1 }
    //  2
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    注意,在异步函数中抛出错误会返回拒绝的promise。

    async function foo() {
      console.log(1)
      throw 3
    }
    
    foo().catch(console.log)
    console.log(2)
    // 1 2 3
    
    1
    2
    3
    4
    5
    6
    7
    8

    但是,拒绝promise的错误不会被异步函数捕获。

    async function foo() {
      console.log(1)
      Promise.reject(3)
    }
    
    foo().catch(console.log)
    console.log(2)
    
    // 1
    // 2
    //  UnhandledPromiseRejectionWarning: 3
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    加一个return或者await 就可以了。

    async function foo() {
      console.log(1)
      return Promise.reject(3)
      // await Promise.reject(3)
    }
    
    foo().catch(console.log)
    console.log(2)
    
    // 1
    // 2
    // 3
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    不过最好不要混用,async/await捕获错误可以与try/catch配合使用。

    async function foo() {
      console.log(1)
      try {
        await Promise.reject(3)
        console.log(4) // 这行代码不会执行
      } catch (err) {
        console.log('err', err)
      }
    }
    
    foo()
    console.log(2)
    
    // 1
    // 2
    // err 3
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    # await

    async/await就是*/yield的语法糖,可以暂停和恢复代码执行。

    await不能单独使用,必须有async一起。

    看下几个例子来理解await是如何暂停和恢复代码执行的。

    async function foo() {
      const msg = await Promise.resolve('foo')
      console.log(msg)
    }
    
    async function bar() {
      console.log(await 'bar')
    }
    
    async function baz() {
      console.log('baz')
    }
    
    foo()
    bar()
    baz()
    
    // baz
    // foo
    // bar
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    JS运行时在碰到await关键字时,会记录在哪里暂停执行。等await右边的值可用了,JS运行时会向消息队列中推送一个任务,这个任务在恢复异步函数的时候执行。注意await前后的区别。

    async function foo() {
      console.log(2)
      await null
      console.log(4)
    }
    console.log(1)
    foo()
    console.log(3)
    // 1 2 3 4
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    解释如下:

    1. 打印1
    2. 调用异步函数foo()
    3. 打印2
    4. await关键字暂停执行,得到立即可用的值null,向消息队列中推送一个任务
    5. foo()退出
    6. 打印3
    7. 同步线程代码执行完毕
    8. JS运行时从消息队列中取出任务,恢复异步函数foo()执行
    9. await取得null值
    10. 打印4
    11. foo()返回

    再看个更复杂的例子:

    async function foo() {
      console.log(2)
     	console.log(await Promise.resolve(8))
      console.log(9)
    }
    
    async function bar() {
      console.log(4)
      console.log(await 6)
      console.log(7)
    }
    console.log(1)
    foo()
    console.log(3)
    bar()
    console.log(5)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    注意,《高程》的输出和解释有误,答案应该是

    1 2 3 4 5 8 9 6 7
    
    1

    亲测,nodejs和浏览器环境都是如此。再来一道头条的面试题。

    async function async1() {
      console.log('1')
      await async2()
      console.log('2')
    }
    
    async function async2() {
      console.log('3')
    }
    
    console.log('4')
    
    setTimeout(function () {
      console.log('5')
    }, 0)
    
    async1()
    
    new Promise(function (resolve) {
      console.log('6')
      resolve()
    }).then(function () {
      console.log('7')
    })
    
    console.log('8')
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    注意并不是说遇到await后,右侧代码就不执行了,await是在等待,它等的是右侧的表达式的值,如果这个求值过程中有同步代码,那就会立即执行。等拿到右侧的值,则会将其推到任务队列中,等待主线程代码执行完毕后再取出,恢复代码执行。

    # 异步函数实践

    # sleep
    async function sleep(delay) {
      return new Promise((resolve) => setTimeout(resolve, delay))
    }
    
    async function foo() {
      const t0 = Date.now()
      await sleep(1500) // 暂停1500ms
      const t1 = Date.now()
      console.log(t1 - t0)
    }
    
    foo()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 平行加速

    假设有5个请求,每个耗时在0-1000ms之间,连续请求总共耗时是多少。

    方案一:

    串行请求,每个请求完成之后请求下一个,保证了顺序,但总耗时太长。

    async function randomDelay(id) {
      const delay = Math.random() * 1000
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log(`${id} finished`)
          resolve(id)
        }, delay)
      })
    }
    
    async function foo() {
      const t0 = Date.now()
    
      for (let i = 0; i < 5; i++) {
        await randomDelay(i)
      }
      console.log(`${Date.now() - t0} elapsed`)
    }
    foo()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    方案二:

    一次性初始化promise,分别收集结果。

    async function foo() {
      const t0 = Date.now()
    
      // for (let i = 0; i < 5; i++) {
      //   await randomDelay(i)
      // }
      let promises = Array(5)
        .fill(null)
        .map((_, i) => randomDelay(i))
      for (let i = 0; i < 5; i++) {
        console.log(`await ${await promises[i]}`)
      }
      console.log(`${Date.now() - t0} elapsed`)
    }
    foo()
    // 3 finished
    // 1 finished
    // 2 finished
    // 0 finished
    // await 0
    // await 1
    // await 2
    // await 3
    // 4 finished
    // await 4
    // 922 elapsed
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    方案三:

    使用Promise.all()

    
    async function foo() {
      const t0 = Date.now()
    
      // for (let i = 0; i < 5; i++) {
      //   await randomDelay(i)
      // }
      let promises = Array(5)
        .fill(null)
        .map((_, i) => randomDelay(i))
      const results = await Promise.all(promises)
      for (let i = 0; i < results.length; i++) {
        console.log(`await ${results[i]}`)
      }
      console.log(`${Date.now() - t0} elapsed`)
    }
    foo()
    // 4 finished
    // 0 finished
    // 1 finished
    // 3 finished
    // 2 finished
    // await 0
    // await 1
    // await 2
    // await 3
    // await 4
    // 704 elapsed
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    #前端笔记#异步编程
    上次更新: 2022/01/14, 08:52:26
    第10章 函数
    第12章 BOM

    ← 第10章 函数 第12章 BOM→

    最近更新
    01
    提交代码时修改commit消息
    04-09
    02
    如何快速定位bug
    02-20
    03
    云端web项目开发踩坑
    08-25
    更多文章>
    Theme by Vdoing | Copyright © 2021-2025 dwfrost | 粤ICP备2021118995号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×