学习链接

阮一峰:Generator 函数的异步应用

阮一峰:async 函数

Async/Await 如何通过同步的方式实现异步

JS异步解决方案的发展历程以及优缺点

第 8 题:setTimeout、Promise、Async/Await 的区别

异步操作

所谓”异步”,简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。

callback

回调函数是 JavaScript 语言对异步编程的一种实现。

所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。

优点:简单、容易理解和实现

缺点:不利于代码的阅读和维护(尤其是多个回调函数嵌套的情况,即回调地狱)

  • 各个部分之间高度耦合,使得程序结构混乱,不易阅读和调试
  • 错误处理不方便
    执行分成两段,第一段执行完以后,任务所在的上下文环境就已经结束了。在这以后抛出的错误,原来的上下文环境已经无法捕捉
  • 每个任务只能指定一个回调函数

Promise

Promise 是异步编程的一种解决方案,就是为了解决 callback 的问题而提出的。

它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。

  • 它不是新的语法功能,而是一种新的写法
    允许将回调函数的嵌套,改成链式调用,使得异步任务的两段执行看得更清楚
  • 也就是说,Promise 相当于提供了一个容器,里面保存了某个未来才会结束的事件(通常为异步操作),
    然后将异步操作的结果用 Promise 包装后传递给后面的方法(回调)
  • Promise 提供了一个统一的接口,使得控制异步操作变得更加容易

缺点:

  • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消
  • 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部
  • 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
  • Promise 自身的 API 会使得代码产生一些冗余,让原来的语义变得不清楚

Generator

异步编程(多任务)的一种解决方案——“协程”(coroutine),意思是多个线程互相协作,完成异步任务,是协作式多任务的轻量级线程。

协程有点像函数(子例程),又有点像线程。它的运行流程大致如下。

  • 第一步,协程 A 开始执行。
  • 第二步,协程 A 执行到一半,进入暂停,执行权转移到协程 B
  • 第三步,(一段时间后)协程 B 交还执行权。
  • 第四步,协程 A 恢复执行。

上面流程的协程 A,就是异步任务,因为它分成两段(或多段)执行。

同子例程的比较

说与子例程像,主要是指在转移执行权方面的关系。

  • 子例程可以调用其他的子例程,这样的子例程之间存在调用的父子关系
  • 协程之间可以通过 yield (让步),即暂时让出执行权来调用其他协程,
    这种方式转移执行权的协程之间不是调用的父子关系,而是彼此对称、平等
  • 子例程的生命期遵循后进先出,子例程调用其他子例程,调用者等待被调用者结束后继续执行
    协程的生命期完全由对它们的使用需要来决定
  • 子例程的起始处是惟一的入口点,每当子例程被调用时,执行都从被调用子例程的起始处开始
    协程可以有多个入口点,协程的起始处是第一个入口点,每个 **yield** 返回出口点都是再次被调用执行时的入口点
  • 子例程只在结束时一次性返回全部结果值
    协程可以在 **yield** 时不调用其他协程,而是每次返回一部分的结果值,这种协程常称为生成器迭代器

子例程可以看作是特定状况的协程,任何子例程都可转写为不调用 yield 的协程。

同线程的比较

说与线程像,主要是指在处理多任务方面的关系。

协程是协作式 多任务的,而线程是抢占式 多任务的。

即资源分配时

  • 哪个线程先得到资源,由运行环境决定
  • 协程的执行权由协程自行分配

这也意味着协程提供并发性而非并行性

即在同一时间

  • 可有多个线程处于运行状态
  • 只有一个协程处于运行态,其他都是暂停状态

在协程之间的切换非常方便,不需要涉及任何的系统调用或者任何阻塞调用,这使得协程比线程更加适合实时运算的场景。

协程是完全由程序所控制,也就是在用户态执行,这样的好处就是性能得到了很大的提升,不会像切换线程那样消耗资源。

**Generator** 函数,是 ES6 对协程的不完全实现,也称为 “半协程” ,是协程的子集。

  • 生成器只能把控制权转交给调用生成器的调用者
    即只有生成器的调用者,能将程序执行权还给生成器
  • 完全实现的协程,有能力控制将控制权转交给哪个协程
    即若实现的是协程而非半协程,那么任何函数都可让暂停的 Generator 函数继续运行
  • Generator 函数中的 yield 语句不指定要跳转到的协程,而是向父例程传递返回值

优点

  • Generator 函数是可以暂停执行和恢复执行的
    在生成器函数内部执行一段代码,遇到 yield 关键字,js引擎会返回关键字后面的内容给外部,并暂停该函数的执行。外部函数可以通过 next 这类方法恢复函数的执行。

缺点

  • 流程管理不方便(即何时执行第一阶段、何时执行第二阶段)

async/await

异步的角度来看,async 算是 Generator + Promise 的语法糖。

async 函数对 Generator 函数的改进,体现在以下四点。

  • 内置执行器
    • Generator 函数的执行需要调用 next 方法,或者用 co 模块,也就是需要依靠执行器,才能真正执行,得到最后结果
    • async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行
  • 更好的语义
    • asyncawait,比起星号和 yield,语义更清楚了
    • async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果
  • 更广的适用性
    • co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise 对象
    • async 函数的 await 命令后面,可以是 Promise 对象和任意类型的值
      (数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)
  • 返回值是 **Promise**
    • async 函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。可以用 then 方法指定下一步的操作
    • 进一步说,async 函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖

与微任务的关系

async 函数中,可将 await 当作 then 来看待,以此作为分界线。

本次 await 后跟着的同步语句(或者说本轮宏任务),加上与上一个 await 之间的宏任务,

即为本轮的微任务。(暂未想到合适的表述,具体效果见 async 例)

Promise

  1. new Promise(resolve => {
  2. console.log(1, 1);
  3. resolve();
  4. console.log(1, 2);
  5. }).then(() => {
  6. console.log(1, 3);
  7. }).then(() => {
  8. console.log(1, 4);
  9. })
  10. new Promise(resolve => {
  11. console.log(2, 1);
  12. resolve();
  13. console.log(2, 2);
  14. }).then(() => {
  15. console.log(2, 3);
  16. }).then(() => {
  17. console.log(2, 4);
  18. })
  19. // 1 1
  20. // 1 2
  21. // 2 1
  22. // 2 2
  23. // 1 3
  24. // 2 3
  25. // 1 4
  26. // 2 4

async

  1. setTimeout(() => console.log('setTimeout'));
  2. async function test() {
  3. console.log('async', 1);
  4. await new Promise((resolve, reject) => {
  5. console.log('async', 2);
  6. resolve();
  7. })
  8. .then(() => console.log('async', 3))
  9. .then(() => console.log('async', 4));
  10. console.log('async', 5);
  11. await console.log('async', 6);
  12. await console.log('async', 7);
  13. }
  14. test();
  15. console.log(3);
  16. new Promise(resolve => {
  17. console.log('Promise', 1);
  18. resolve();
  19. console.log('Promise', 2);
  20. })
  21. .then(() => {
  22. console.log('Promise', 3);
  23. })
  24. .then(() => {
  25. console.log('Promise', 4);
  26. })
  27. .then(() => {
  28. console.log('Promise', 5);
  29. })
  30. .then(() => {
  31. console.log('Promise', 6);
  32. });
  33. // async 1
  34. // async 2
  35. // 3
  36. // Promise 1
  37. // Promise 2
  38. // async 3
  39. // Promise 3
  40. // async 4
  41. // Promise 4
  42. // async 5
  43. // async 6
  44. // Promise 5
  45. // async 7
  46. // Promise 6
  47. // setTimeout

Generator

  1. function* test() {
  2. console.log('Generator', 1);
  3. yield console.log('Generator', 2);
  4. yield new Promise(resolve => {
  5. console.log('Generator', 3);
  6. resolve();
  7. }).then(() => {
  8. console.log('Generator', 4);
  9. });
  10. yield console.log('Generator', 5);
  11. }
  12. const temp = test();
  13. temp.next();
  14. temp.next();
  15. temp.next();
  16. temp.next();
  17. new Promise(resolve => {
  18. console.log('Promise', 1);
  19. resolve();
  20. })
  21. .then(() => {
  22. console.log('Promise', 2);
  23. })
  24. .then(() => {
  25. console.log('Promise', 3);
  26. })
  27. // Generator 1
  28. // Generator 2
  29. // Generator 3
  30. // Generator 5
  31. // Promise 1
  32. // Generator 4
  33. // Promise 2
  34. // Promise 3