# 异步

阅读本文前,如果你对事件循环、宏任务、微任务还不清楚,先阅读上一篇事件循环

# 事件模型

DOM的点击事件

# 回调函数

setTimeout(function() {
  console.log(2)
}, 0)
console.log(1)

// 1
// 2
1
2
3
4
5
6
7

回调函数实现异步原理: 事件循环

优点:解决了同步的问题

用回调函数实现异步的缺点:

  1. 缺乏顺序性:大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流 程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码 是坏代码,会导致坏 bug。
  2. 缺乏可信任性:回调会受到控制反转的影响,因为回调暗中把控制权交给第三 方(通常是不受你控制的第三方工具!)来调用你代码中的 continuation。这种控制转移导 致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期。
  3. 回调地狱:多级回调函数嵌套,使代码难以理解和维护

# Promise

TIP

本节中 Promise 代表 Promise 构造函数, promise 代表 Promise 实例

Promise的出现主要解决了回调函数缺乏可信任性和回调函数过多嵌套的问题,意为“承诺”。Promise将回调嵌套改为链式调用,增加可读性和可维护性。

promise有三种状态:

  • 初始状态(pending)
  • 已完成(fullfilled)
  • 已拒绝(rejected)

一旦决议(resolve),promise就处于完成或拒绝状态,并且不会再被修改。如果没有使用任何值显式决议promise, 那么这个实例的决议值就是 undefined。任何Promise 实例都只有一个决议值

# thenable

then()方法:接受两个参数:完成回调和拒绝回调

thenable: 任何具有then()方法的对象,即是thenable的

# new Promise()

new Promise(function(resolve, reject) {

})
1
2
3

Promise(..) 必须和 new 一起使用,并且必须提供一个回调函数——执行器。
执行器接受两个回调函数作为参数,通常分别命名为resolve 和 reject。执行器函数是同步的,会被立即调用。

如果在执行器中:

  1. 使用多个参数调用 resovle(..) 或者 reject(..),第一个参数之后的所有参数都会被默默忽略
  2. 多次调用resovle(..) 和 reject(..),只有第一个生效,后续的都会被忽略

如果在执行器中,向 resolve(..):

  • 传递一个非 promise、非 thenable 的立即值,promise的决议值就是这个立即值
  • 传递一个真正的 promise 或 thenable 值,promise 或 thenable 值会被递归展开,直到提取出一个具体的非 promise 和 非 thenable 的最终值,promise的决议值就是这个最终值

如果在执行器中,向 reject(..):

  • 传递一个非 promise、非 thenable 的立即值,Promise 实例p的决议值就是这个立即值
  • 传递一个真正的 promise 或 thenable 值,promise 或 thenable 值会被原封不动的设置为拒绝理由,后续的拒绝处理函数接收到的就是传给reject() 的那个 promise 或 thenable 值

如果在执行器内部抛出一个错误,则promise的拒绝处理程序就会被调用

WARNING

通过 p instanceof Promise 来检查p是不是Promise实例时,注意:

  1. promise 值可能是从其他浏览器窗口(iframe 等)接收到的
  2. 库或框架可能会选择实现自己的 Promise,而不是使用原生 ES6 Promise 实现

# Promise.resolve()和Promise.reject()

这两个方法的调用不会有任务编排的过程

Promise.resolve()和Promise.reject() 分别是创建一个已经决议和已被拒绝的 Promise实例的快捷方式

如果向 Promise.resolve(..):

  • 传递一个非 promise、非 thenable 的立即值,就会返回一个用这个值填充的 Promise实例
  • 传递了一个非 promise 的 thenable 值,会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法(即:试图展开这个 thenable 值,持续到提取出一个具体的非类 promise 的最终值)
  • 传递一个真正的 promise,就会返回同一个 promise (注意此处与执行器函数中调用resolve(..)不同)

Promise.reject(..) 不会像 Promise.resolve(..) 一样进行展开。如果向 Promise.reject(..) 传入一个 Promise/thenable 值,它会把这个值原封不动地设置为返回的promise的 拒绝理由。后续的拒绝处理函数接收到的是你实际传给 Promise.reject(..) 的那个 Promise/thenable,而不是其底层的立即值。

# promise.then()和promise.catch()

  1. 每次你对 promise 调用 then(..)和 catch(..),它都会创建并返回一个新的 promise
  2. 每次调用then()方法或catch()方法都会创建一个新任务,当promise被解决时会被加入到当前事件循环回合的微任务队列(microtask queue)

Promise.then(),接受两个参数,第一个用于完成回调,第二个用于拒绝回调:

  1. 不管从 then(..)的完成回调返回的值是什么(或者返回一个异常),它都会被自动设置 为被链接的 promise的完成值
  2. 不管从 then(..) 的拒绝回调返回的值是什么,它都会被自动设置 为被链接的 promise的拒绝值
  3. 在 then(..) 的完成拒绝回调或者拒绝回调中抛出一个错误,它都会被自动设置 为被链接的 promise的拒绝值

如果从完成或拒绝处理函数:

  • 返回一个立即值,这个值就会被用作返回promise的完成值或拒绝值
  • 返回 thenable 或者 Promise 的时候会发生同样的展开,而且展开过程会持续到提取出一个具体的非thenable或非 Promise 的最终值

如果你调用 promise 的 then(..),并且只传入一个完成处理函数,一个默认拒绝处理函数 就会顶替上来。默认拒绝处理函数只是把错误重新抛出

如果没有给 then(..) 传递一个适当有效的函数作为完成处理函数参数,还是会有作为替代 的一个默认处理函数。默认的完成处理函数只是把接收到的任何传入值传递给下一个步骤

promise.catch(..)等同于promise.then(null, function(){})

# Promise.all()和Promise.race()

Promise.all():

  1. 传给 Promise.all([ .. ]) 的数组中的值可以是 Promise、 thenable,甚至是立即值。就本质而言,列表中的每个值都会通过 Promise.resolve(..) 过滤,以确保要等待的是一个真正的 Promise,所以立即值会 被规范化为这个值构建的 Promise。如果数组是空的,主 Promise 就会立 即完成。
  2. 从Promise.all([ .. ])返回的主promise在且仅在所有的成员promise都完成后才会完 成。如果这些promise中有任何一个被拒绝的话,主Promise.all([ .. ])promise就会立 即被拒绝,并丢弃来自其他所有 promise 的全部结果。(即只会得到第一个拒绝promise的拒绝理由值)
  3. 如果数组列表中的promise有自己的catch()方法,则一旦它被rejected,并不会触发Promise.all()的catch方法
  Promise.all = function(arr) {
    return new Promise(function(resolve, reject){
      let resArr = []
      let count = 0
      for (let [i, p] of arr.entries()) {
        Promise.resolve(p).then( res => {
          ++count
          resArr[i] = res
          if (count === arr.length) {
            resolve(resArr)
          }
        }, err => {
          reject(err)
        })
      }
    })
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Promise.race():

  1. Promise.race([ .. ])接受单个数组参数,数组中的值可以是 Promise、 thenable和立即值。显然数组中的第一个立即值永远都是第一个完成的
  2. Promise.race([ .. ])中,一旦有任何一个Promise决议为完成/拒绝,Promise.race([ .. ])就会完成/拒绝
  3. 如果你传入了一个空数组,Promise. race([..]) 永远不会决议
  Promise.race = function(iterable) {
    return new Promise(function(resolve, reject){
      for (const p of iterable) {
        Promise.resolve(p).then(resolve, reject)
      }
    })
  }
1
2
3
4
5
6
7

# Promise.prototype.finally()

Promise.prototype.finally = function(cb) {
  const P = this.constructor
  const onFulFilled = function(data) {
    // P.resolve(cb())是为了兼容cb是异步函数的情况
    return P.resolve(cb()).then(function() {
      return data
    })
  }

  const onRejected = function(err) {
    return P.resolve(cb()).then(function() {
      throw err
    })
  }

  return this.then(onFulFilled, onRejected)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Promise拒绝事件

Node.js中:

  • unhandledRejection 在一个事件循环中,当Promise被拒绝,并且没有提供拒绝处理程序时触发。事件处理程序的参数为一个错误对象和被拒绝的promise
  • rejectHandled 在一个事件循环后,当Promise被拒绝,若拒绝处理程序被调用时触发。事件处理程序的参数只有一个:被拒绝的promise

浏览器中:

  • unhandledrejection 触发条件同Node.js的unhandledRejection
  • rejecthandled 触发条件同Node的rejectHandled

浏览器中这两个事件的事件处理程序都只接受一个事件对象为参数

# 总结

优点: Promise主要解决了回调函数缺乏可信任性和回调函数过多嵌套的问题,意为“承诺”。Promise将回调嵌套改为链式调用,增加可读性和可维护性。 Promise 缺点:

  1. 无法取消
  2. 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
  3. 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

# 生成器与迭代器

# 生成器

生成器(generator)的出现主要解决了回调函数缺乏顺序性的问题,用顺序、看似同步的表达风格控制异步流程(具有依赖关系的Promise依然会嵌套调用Promise,并且链式的Promise难以处理某些条件下需要断开链式调用的场景)。

Generator实现的核心在于上下文的保存,函数并没有真的被挂起,每一次yield,其实都执行了一遍传入的生成器函数,只是在这个过程中间用了一个context对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样

生成器是一种返回迭代器的函数
yield只可以用在生成器内部,并且和return一样,不能穿透函数边界,即yield只能用在生成器函数的直接作用域中 yield表达式如果用在另一个表达式之中,必须放在圆括号里面
yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号 不能用箭头函数来创建生成器
声明格式:

// 第一种
function* foo1(){}

// 第二种
function *foo2(){}

// 第三种
function*foo3(){}
1
2
3
4
5
6
7
8

# 迭代器

迭代器(iterator)的出现旨在消除多个循环嵌套产生的复杂性并减少循环中的错误

迭代器是一种特殊对象,具有一个next()方法,每次调用这个方法都返回一个对象,这个对象中有两个属性:

  1. value:表示下一个将要返回的值
  2. done:布尔类型,当没有更多可返回数据时返回true

迭代器中还保存有一个内部指针,用来指向当前集合中值的位置,每调用一次next(),都返回下一个可用的值

iterable(可迭代)对象:指包含一个可以在其值上迭代的迭代器的对象
iterable 对象内部必须支持一个函数属性,其名称 是专门的 ES6 符号值 Symbol.iterator。调用这个函数时,它会返回一个迭代器。

生成器函数默认会为通过生成器创建的迭代器对象添加Symbol.iterator属性,故此类迭代器都是可迭代对象

默认情况下,开发者定义的对象都不是可迭代对象

for-of循环与迭代器息息相关,每执行一次都会调用可迭代对象的next()方法,并将返回的结果对象的value属性存储在一个变量中,循环持续执行到返回对象的done属性为true

数组、Map、Set都内建了三种迭代器:entries()、values()、keys()

数组的keys()迭代器,使用for-of只会针对数字类型的索引。但对数组用for-in,则会包含数组属性(可以开发者自己添加)

数组和Set的默认迭代器是values(),Map的是entries()
默认的数组迭代器并不关心通过 next(..) 调用发送的任何消息

展开运算符可以用于任何可迭代对象

# 迭代器与生成器的合作

调用生成器函数生成一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器来控制的是这个生成器实例。

规范和所有兼容浏览器都会默默丢弃传递给第一个 next() 的任何东西。因此,启动生成器时一定要用不带参数的 next()。

迭代器调用throw()方法抛出的错误如果在生成器内部被捕获了,会继续执行下一条yield语句,返回结果对象

在生成器中使用return,结果对象的done属性会被设为true,如果return了值,则结果对象的value属性为该值

可以在外部通过return(..) 手工终止生成器的迭代器实例:it.return( "Hello World" )

# 委托生成器

在生成器函数内部,将星号放在yield和生成器函数名之间,则产生了委托生成器
委托生成器内部的return回的值不会反映在外部的生成器,但是可以将委托生成器函数的返回值赋给一个变量(如result),再添加一条yield语句(如yield result),则外部的生成器的迭代器的返回对象中的value值为该值,done值不受该return影响

# 总结

生成器优点:可以控制函数的执行

# Async

async 函数会返回一个 promise,可以使用then方法添加回调函数。当函数执行的时候一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行该 async 函数体内后面的语句(统一放在微任务里处理)。在 async 函数完全结束之后,其返回的 promise 会决议。

async函数内部return语句返回的值,会成为then方法回调函数的参数

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值

另一种情况是,await命令后面是一个thenable对象(即定义了then方法的对象),那么await会将其等同于 Promise 对象(即等待then方法执行完)

在 Promise 中 resolve 一个 thenable 对象”,需要先将 thenable 转化为 Promsie,然后立即调用 thenable 的 then 方法,并且 这个过程需要作为一个 job 加入微任务队列,以保证对 then 方法的解析发生在其他上下文代码的解析之后

对于 await v: await 后的值 v 会被转换为 Promise 即使 v 是一个已经 fulfilled 的 Promise,还是会新建一个 Promise,并在这个新 Promise 中 resolve(v) await v 后续的代码的执行类似于传入 then() 中的回调

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行,等同于async函数返回的 Promise 对象被reject

# 多线程

# 总结

获得 Promise 和生成器最大效用的最自然的方法就是 yield 出来一个 Promise,然后通过这个 Promise 来控制生成器的迭代器。

在异步控制流程方面,生成器的关键优点是:生成器内部的代码是以自然的同步 / 顺序方 式表达任务的一系列步骤。其技巧在于,我们把可能的异步隐藏在了关键字 yield 的后面, 把异步移动到控制生成器的迭代器的代码部分。 换句话说,生成器为异步代码保持了顺序、同步、阻塞的代码模式,这使得大脑可以更自 然地追踪代码,解决了基于回调的异步的两个关键缺陷之一。

Last Updated: 11/9/2022, 6:38:52 AM