# 异步编程

# EventLoop1

# 浏览器的进程模型

# 进程和线程

有了进程后,就可以运行程序的代码了。

运行代码的[人]称之为[线程]。

一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程(主线程结束,进程就结束了)。

如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程。

# 浏览器有哪些进程和线程

浏览器是一个多进程多线程的应用程序。

浏览器的内部工作极其复杂。

为了避免互相影响,为了减少连环崩溃的几率,但启动浏览器后,它会自动启动多个进程。

其中,最最要的进程有:

  1. 浏览器进程:主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。
  2. 网络进程:负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。
  3. 渲染进程:渲染进程启动后,会开启一个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不互相影响。

# 渲染主线程是如何工作的

渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:

  • 解析 HTML
  • 解析 CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面画 60 次
  • 执行全局 JS 代码
  • 执行事件处理函数
  • 执行计时器的回调函数
  • ......

要处理这么多的任务,主线程遇到了一个前所未有的难题:如何调度任务?

比如:

  • 我正在执行一个 JS 函数,执行到一半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
  • 我正在执行一个 JS 函数,执行到一半的时候某个计时器到达了时间,我该立即去执行它的回调吗?
  • 浏览器进程通知我“用户点击了按钮”,与此同时,某个计时器也到达了时间,我应该处理哪一个呢?

渲染主线程想出来一个绝妙的主意来处理这个问题:排队

事件循环

  1. 在最开始的时候,渲染主线程会进入一个无限循环
  2. 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则会进入休眠状态。
  3. 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务。

这样一来,就可以让每个任务有条不紊的、持续的进行下去了。

整个过程被称为事件循环(消息循环)。

# 若干解释

# 何为异步

代码在执行过程中,会遇到一些无法立即处理的任务,比如:

  • 计时完成后需要执行的任务——setTimeoutsetInerval
  • 网络通信完成后需要执行的任务——XHRFetch
  • 用户操作后需要执行的任务——addEventLisetener

如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于[阻塞]的状态,从而导致浏览器[卡死]。以下是同步的图示。

事件循环2

渲染主线程承担着极其重要的工作,无论如何都不能阻塞!

因此,浏览器选择异步来解决这个问题。以下是异步的图示。

事件循环3

使用异步的方式,渲染主线程永不阻塞。

# 面试题:如何理解 JS 的异步

参考答案:

JS 是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。而渲染主线程承担着诸多工作,渲染页面、执行 JS 都在其中运行。如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。 所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其它线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。 在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。

# JS 为何会阻塞渲染

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>test</title>
  </head>
  <body>
    <h1>我是标题</h1>
    <button>change</button>
    <script>
      var h1 = document.querySelector("h1");
      var btn = document.querySelector("button");

      function delay(time) {
        var start = Date.now();
        while (Date.now() - start < time) {}
      }
      btn.onclick = function () {
        h1.textContent = "标题改变了";
        delay(3000);
      };
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

点击后,h1.textContent被赋予新值,页面收到重绘通知并生成重绘任务放到消息队列中。继续执行下一行代码delay(3000),进而遇到 3 秒钟的死循环。3 秒过后,当前任务执行完了,渲染主线程从消息队列中调度新任务来执行,那么页面重绘就会开始进行。始终记住,JS 执行和页面渲染是在同一线程也就是渲染主线程中进行的,JS 是有可能阻塞页面渲染的。

# 任务有优先级吗

任务没有优先级,在消息队列中先进先出。

但消息队列是由优先级的

根据 W3C 的最新解释:

  • 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列,在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
  • 浏览器必须准备好一个微队列。微队列中的任务优先所有其他任务执行。

随着浏览器的复杂度急剧提升,W3C 不再使用宏队列的说法。

在目前的 chrome 的视线中,至少包含了下面的队列:

  • 延时队列:用于存放计时器到达后的回调任务,优先级[中]
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级[高]
  • 微队列:用户存放需要最快执行的任务,优先级[最高]

添加任务到微队列的主要方式主要是使用Promise、MutationObserver

# 面试题:阐述一下 JS 的事件循环

事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。 在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。 过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。 根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务,但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。

# JS 中的计时器能做到精确计时吗?为什么?

不行,因为

  1. 计算机硬件没有原子钟,无法做到精确计时
  2. 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,也就是携带了这些偏差。
  3. 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层,则会带有 4 毫秒的最少时间,这样在计时时间少于 4 毫秒时又带来了偏差。
  4. 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差。

# EventLoop2

参考资料 1:8 Web application APIs (opens new window)
参考资料 2:跟着 whatwg 看一遍事件循环 (opens new window)
参考资料 3:HTML 系列:macrotask 和 microtask (opens new window)

js 引擎是单线程运行的,同一时刻只能执行一个代码块。比如请求接口时,如果 js 引擎停下来等服务端传来响应,那就会阻塞其他代码运行。所以 js 引擎处理接口是异步的,会将接口响应任务存消息队列里然后就执行其他代码了,等后面执行栈空闲了再来处理响应任务。类似的还有定时器、侦听器等,它们可以说是异步请求同步执行,意思是请求我发给你了,但我得处理其他主要事情(异步请求);就算你响应立马给我了,我也只是暂时排在任务列表里,手头里主要事情做完了才会去处理响应(同步处理)。

js 引擎对这些异步操作就是使用EventLoop 事件循环来调配处理的,所用的消息队列分为task 队列microtask 队列。EventLoop 的一个处理回合:首先会先执行task 队列中的一个任务,然后再执行microtask 队列里的所有任务,最后再根据需要与否去渲染页面。在这个回合中是同步执行的,不会出现既要处理这个任务也要处理那个任务。

EventLoop 中task 队列可能有多个,原因是其中一个 task 队列负责优先级比较高的任务(鼠标键盘事件等),另一个 task 队列负责普通的任务(定时器等);还有task 队列Set而不是Queue,EventLoop 会从中取最早并且可执行的任务出来并推入执行栈中执行。比较常见的 task:setTimeoutsetIntervalsetImmediaterequestAnimationFrameI/O,还有类似clickajax还有<script>标签里的代码。

EventLoop 中microtask 队列一般只有一个,并且是Queue,找元素用的是出队列的方式。比较常见的 microtask:PromiseObject.observeMutationObserver

<html>
  <head>
    <script>
      (function test() {
        setTimeout(() => {
          console.log(1);
        }, 0);
        new Promise((resolve) => {
          console.log(2);
          for (var i = 0; i < 10000; i++) {
            i == 9999 && resolve(3);
          }
          console.log(4);
        }).then((data) => {
          console.log(data);
        });
        console.log(5);
      })();
    </script>
    <script>
      (function test2() {
        setTimeout(() => {
          console.log(6);
        }, 0);
        new Promise((resolve) => {
          console.log(7);
          for (var i = 0; i < 10000; i++) {
            i == 9999 && resolve(8);
          }
          console.log(9);
        }).then((data) => {
          console.log(data);
        });
        console.log(10);
      })();
    </script>
  </head>
  <body></body>
  <html></html>
</html>
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
29
30
31
32
33
34
35
36
37
38
39
40

上面这段程序在谷歌浏览器中执行的,其打印顺序就是:2 4 5 3 7 9 10 8 1 6

  • 首先两个<script>标签里代码的运行是两个 task,先添加进 task 队列里;
  • 从 task 队列里取可执行的 task,也就是第一个<script>标签开始执行;
  • 遇到setTimeout,将它放入 task 队列里;
  • 因为 Promise 内部是同步的,所以先打印2 和 4,在resolve()后会把 Promise 的 then 回调函数放入 microtask 队列里;
  • 遇到console.log(5),打印完5才表示一个 task 执行完了,现在得去执行 microtask 队列所有任务;
    • microtask 队列目前就一个 then 回调函数,那么打印3,结束 microtask 队列,进入新的 task。
  • 从 task 队列里取可执行的 task,也就是第二个<script>标签开始执行;
  • 遇到setTimeout,将它放入 task 队列里;
  • 因为 Promise 内部是同步的,所以先打印7 和 9,在resolve()后会把 Promise 的 then 回调函数放入 microtask 队列里;
  • 遇到console.log(10),打印完10才表示一个 task 执行完了,现在得去执行 microtask 队列所有任务;
    • microtask 队列目前就一个 then 回调函数,那么打印8,结束 microtask 队列,进入新的 task。
  • 从 task 队列里取可执行的 task,也就第一个setTimeout,打印1
  • 从 task 队列里取可执行的 task,也就第二个setTimeout,打印6
  • 结束了,以上省略了 UI 渲染,因为没有涉及到页面的操作。

上面这些都是说浏览器的 EventLoop,其实 node 环境中还是不一样的,node.js有用到process.nextTick,它并不是 EventLoop 中的一部分,它是优先于microtask执行的;还有一个setImmediate,是个定时器,在 I/O 操作中先于任何其他定时器的,但非 I/O 操作里setImmediate的先后就不一定了,比较受线程性能的影响(比如设置毫秒数是 0 到几十的时候,setTimeout 一般优于 setImmediate)。有兴趣可以查看Node.js 事件循环 (opens new window)

(() => {
  setImmediate(() => {
    console.log(1);
  }, 0);
  setTimeout(() => {
    console.log(2);
  }, 0);
  function a() {
    setImmediate(() => {
      console.log(3);
    }, 0);
    setTimeout(() => {
      console.log(4);
    }, 0);
    new Promise((resolve) => {
      console.log(5);
      resolve(6);
      console.log(7);
    }).then((data) => {
      console.log(data);
    });
    console.log(8);
    process.nextTick(() => {
      console.log(9);
    });
    console.log(10);
  }
  new Promise((resolve) => {
    console.log(11);
    resolve(12);
    console.log(13);
  }).then((data) => {
    console.log(data);
  });
  a();
  console.log(14);
  process.nextTick(() => {
    console.log(15);
  });
  console.log(16);
})();
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
29
30
31
32
33
34
35
36
37
38
39
40
41

上面这段程序在node中执行的,其打印顺序就是:11 13 5 7 8 10 14 16 9 15 12 6 2 4 1 3。具体不展开了,有时间再来扩展 node 中 EventLoop。

# Promise

上一节后,我们对异步操作的背后有了比较清晰的认知,但这都是浏览器内部的处理,我们书写互相依赖的异步操作的时候,就非常容易形成“回调地狱”,所以后面 es6 提出了使用Promise来解决这种问题。

# 什么是 Promise

Promise内存储着一个异步操作,这个异步操作要等到未来才会有结果。Promise 存储的异步操作有三种状态:pending(进行中)fulfilled(已兑现)rejected(已拒绝),这三种状态就是异步操作的结果赋予的,其他人改变不了。

还有一个经常跟 Promise 一起使用的术语:resolved(已决议)。它表示异步操作敲定好了状态,该状态可能是 fulfilled 也可能是 rejected。

# 调用处使用 Promise

未推出 Promise 时,一般做法就是回调函数会作为异步函数的入参,异步函数里的异步操作结束后会根据结果来执行对应的入参(执行回调函数)。推出 Promise 后,不会将回调函数传给异步函数,而是异步函数会返回 Promise 对象,异步操作结束后会根据结果来调用 Promise 对象的then中对应的回调函数。

// 以前的调用:将sucFun和failFun以参数的形式传给异步函数doA,根据需要会执行sucFun和failFun
doA(data, sucFun, failFun);
// 使用Promise:异步函数doA会返回一个promise对象,根据需要会执行promise对象的then方法里的sucFun和failFun
doA(data).then(sucFun, failFun);
1
2
3
4

.then(onFulfilled[, onRejected])中第一个参数是成功回调函数,第二个参数是失败回调函数(可选参数),并且 then 会返回了一个 Promise 对象,那就可以继续使用 then,这样就会形成一种链式调用。以前接口互相依赖时采用嵌套写法,写起来非常占位置也不美观,现在使用 Promise 会非常清爽。

// 以前,函数嵌套
doA(
  data,
  (rlt) => {
    doB(
      rlt,
      (newRlt) => {
        doC(
          newRlt,
          (finRlt) => {
            console.log("FinalResult:", finRlt);
          },
          failFun
        );
      },
      failFun
    );
  },
  failFun
);

// 使用Promise,链式
doA()
  .then((rlt) => {
    return doB(rlt); // 返回doB的Promise对象,好让后面继续使用链式结构
  })
  .then((newRlt) => {
    return doC(newRlt);
  })
  .then((finRlt) => {
    console.log("FinalResult:", finRlt);
  })
  .catch(failFun);
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
29
30
31
32
33

注意:onFulfilled 或 onRejected 执行完后,then 是返回的一个新 promise。也就是说你在 onFulfilled 或 onRejected 里自己返回一个值或者 Error 时,也是返回一个 promise,可以继续在后面写 then 这种链式调用。

# Promise 对象内部

上一节是说怎么使用 Promise 的方式调用异步函数,并且展示了多个异步函数时使用 Promise 方式的优势。这一节说的是异步函数内部会返回一个Promise 对象,这个过程会是一个什么样子呢?

首先在异步函数里创建一个 Promise 对象,new Promise((resolve, reject) => {});Promise 构造函数会传递一个参数,这个参数是一个executor 处理器函数,是专门用于处理具体的异步操作的;操作完异步操作当然还要通知调用方,executor 函数的两个入参resolvereject就是专门做这些事的;当异步任务顺利完成且返回结果值时,会执行resolve方法,将 Promise 对象的状态置为fulfilled;而当异步任务失败且返回失败原因,会执行resolve方法,将 Promise 对象的状态置为rejected

function doA(data) {
    // Promise对象内部(当前小节的内容)
    return new Promise((resolve, reject) => { // 这个匿名函数其实是executor函数
        /* 某某异步操作 */
        if (/* 操作成功 */)
            resolve(result); // 通知操作成功,data是异步操作的结果
        else
            reject(error); // 通知操作失败,error是一个异常对象
    });
}
// 调用处使用Promise(上一小节的内容)
doA(data).then((result) => { // 接收resolve通知
    console.log(result);
}, (error) => { // 接收reject通知
    console.log(error);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

一个使用 Promise 封装 ajax 的例子:

function get(url) {
  return new Promise((resolve, reject) => {
    $.ajax({
      url: url,
      method: "get",
      success: (data) => {
        resolve(data);
      },
      error: (xhr, statusText) => {
        reject(statusText);
      },
    });
  });
}
get("xxx").then(
  (result) => {
    return result;
  },
  (error) => {
    return error;
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 关于错误处理

错误处理:遇到异常抛出时,会顺着 Promise 链寻找下一个onRejected失败回调函数(then 中的 onRejected),如果没有一个 onRejected,就会去.catch()里指定的函数里执行。可以看调用处使用 promise的第二个例子,其中有三次 failFun,但最后只用.catch()来处理。

可以对错误进行精准的捕获,只是需要嵌套 Promise。下面这个例子中内部嵌套了一个 Promise,并且还带了一个 catch,这个 catch 能精准捕获到 doB 和 doC 的失败,而 doA 只会被最外层的 catch 会捕获。

// 使用Promise,链式
doA()
  .then((rlt) =>
    doB(rlt)
      .then((optRlt) => doC(optRlt))
      .catch((e) => {
        console.log(e.message);
      })
  )
  .then((newRlt) => doD(newRlt))
  .then((finRlt) => {
    console.log("FinalResult:", finRlt);
  })
  .catch(failFun);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 时序和拒绝事件

已经变成 resolve 状态的 Promise 对象,会通知 then()里对应的函数,这个通知传递的过程是异步的。也就是说传递到 then()中的回调函数是会被放到一个 microtask 队列里的,不过 Promise 对象里其他的执行还是同步,这就能印证EventLoop里第一个例子中24为什么先于3打印。

拒绝事件:当 Promise 被拒绝时,如果有 reject 函数处理了,就会派发 rejectionhandled 事件到全局作用域;如果没有经过 reject 函数处理,则会派发 unhandledrejection 事件到全局作用域。两个事件都有promise属性和reason属性,其中 promise 属性是指向被驳回的 promise,reason 属性是说明被驳回的原因。

window.addEventListener(
  "unhandledrejection",
  (event) => {
    /* 你可以在这里添加一些代码,以便检查
     event.promise 中的 promise 和
     event.reason 中的 rejection 原因 */
    event.preventDefault();
  },
  false
);
1
2
3
4
5
6
7
8
9
10

# Promise 组合工具

Promise.resolve(value):手动创建一个已经 resolve 的 Promise 对象,相当于new Promise(resolve => resolve(value))(但不完全是,因为下一节会讲new Promise(r => r(v))v是个 Promise 对象的情况)。

Promise.reject(value):手动创建一个已经 reject 的 Promise 对象,相当于new Promise((resolve, reject) => reject(value))

var p = Promise.all([p1, p2, p3]):all 接受一个 Promise 对象数组(也可以是 Iterator 并且返回 Promise 对象),当数组里所有的 Promise 对象状态为 fulfilled 时,p 这个 Promise 对象状态才为 fulfilled,并且结果也会组成数组传递给 p 的回调函数;当数组里只要一个被 rejected 了,那么 p 就是 rejected 状态,其结果会传递给 p 的回调函数。

var p = Promise.race([p1, p2, p3]):race 接受一个 Promise 对象数组(也可以是 Iterator 并且返回 Promise 对象),当数组里只要一个被确定状态了,那么 p 就是随之确定状态了,其结果会传递给 p 的回调函数。

// 1.并行操作
Promise.all([func1(), func2(), func3()]).then(([result1, result2, result3]) => {});
// 2.归并,从Promise.resolve()开始执行func1再执行func2再执行func3,最后到result3
[func1, func2, func3]
  .reduce((p, f) => p.then(f), Promise.resolve())
  .then((result3) => {
    /* use result3 */
  });
// 3.跟上面2是一样的,直接后面追加加,上面肯定要灵活一些
Promise.resolve().then(func1).then(func2).then(func3);
// 4.其实就是用函数封装了2
const composeAsync = (...funcs) => (x) => funcs.reduce(applyAsync, Promise.resolve(x));
// 5.其实就是用函数封装了3
const applyAsync = (acc, val) => acc.then(val);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# resolve 一个 Promise 对象

你可能会遇到这样的问题:Promise.resolve(Promise.resolve(1))Promise.resolve(new Promise(r => r(1)))new Promise(r => r(Promise.resolve(1)))new Promise(r => r(new Promise(nr => nr(1)))),这四个有什么区别?

console.log(Promise.resolve(Promise.resolve(1)));
console.log(Promise.resolve(new Promise((r) => r(1))));
console.log(new Promise((r) => r(Promise.resolve(1))));
console.log(new Promise((r) => r(new Promise((nr) => nr(1)))));
1
2
3
4

在控制台里打印上面的代码,会有如下结果:

resolve-promise

其实就是问Promise.resolve(v)new Promise(r => r(v))的区别。

  • Promise.resolve(v):如果v是普通值,会将这个普通值包装成已经fulfilled的 Promise 实例;如果v本就是一个 Promise 实例,那就会原封不动返回v
  • new Promise(r => r(v)):如果v是普通值,那整体会是一个已经fulfilled的 Promise 实例;但是如果v本就是一个 Promise 实例,那底层会调用ResolvePromise(promise, resolution)处理,其中入参promisenew Promise(r => r(xxx))本身,入参resolutionv。具体的:
    • 处理时会创建 PromiseResolveThenableJob,创建的 PromiseResolveThenableJob 会作为一个 microtask;
    • 然后会运行 PromiseResolveThenableJob 让v的 then 里的回调函数进入 microtask 队列,也就是占了一个 microtask;
    • 运行完后,new Promise(r => r(xxx))本身状态会变为fulfilled,那么总的来说new Promise(r => r(v)整体变为fulfilled占用两个 microtask 时序

另外一个知识点,then()对外暴露应该是一个 Promise 对象,如果在 then 内部return v;,这个return v;要分两种情况,其实和上面的new Promise(r => r(v))是一样的。

  • 如果return v;语句的值是一个是普通值,那 then 对外暴露的 Promise 对象其实就是将这个普通值v包装成已经fulfilled的 Promise 对象。
  • 如果return v;语句的值本就是一个 Promise 对象,那么底层会调用ResolvePromise(promise, resolution)处理,入参promise就是 then 对外暴露的 Promise 对象,resolution 就是v这个 Promise 对象。会经过 PromiseResolveThenableJob 的创建和运行(占用两个 microtask 时序),最后让 then 对外暴露的 Promise 对象的状态变为fulfilled

为什么不直接让v赋给目标 Promise 对象,这样就太简单粗暴了,v的状态是pending还是fulfilled是不确定(即使是fulfilled,但它的 then 回调函数还没执行)。得使用 PromiseResolveThenableJob 来确定v的状态并执行它的 then 回调函数,这些走完后才能让目标 Promise 也fulfilled

// begin 1 then 2 3 4
const p = new Promise((resolve) => {
  console.log("begin");
  resolve("then");
});
// 原封不动返回p,所以相当于p.then(v => console.log(v)),那么"then"会比2先打印
Promise.resolve(p).then((v) => console.log(v));

new Promise((resolve) => {
  console.log(1);
  resolve();
})
  .then(() => console.log(2))
  .then(() => console.log(3))
  .then(() => console.log(4));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// begin 1 2 3 then 4
const p = new Promise((resolve) => {
  console.log("begin");
  resolve("then");
});
// resolve(p),创建PromiseResolveThenableJob会占用一个microtask
// 运行PromiseResolveThenableJob,让p的then回调函数执行,这也会占用一个microtask
new Promise((resolve) => {
  resolve(p);
}).then((v) => console.log(v)); // 经过两个时序(打印了2和3)才打印了"then"

new Promise((resolve) => {
  console.log(1);
  resolve();
})
  .then(() => console.log(2))
  .then(() => console.log(3))
  .then(() => console.log(4));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// begin 1 test 2 3 4 then
const p = new Promise((resolve) => {
  console.log("begin");
  resolve("then");
});
// Promise.resolve(p).then相当于p.then,那么"test"会先于2,这个同第一个例子
// 第一个then里是return Promise.resolve(v),这会触发ResolvePromise(promise, resolution),
// promise入参是代表第一个then的promise,resolution入参是v。跟上一个例子一样要占用两个micrtask时序
Promise.resolve(p)
  .then((v) => {
    console.log("test");
    return Promise.resolve(v); // 其实如果是return v;那就不会触发ResolvePromise了
  })
  .then((v) => console.log(v)); // 但是比上一例子多了一个then,那么"then"会在4后面打印了

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

参考资料:

# async 和 await

# 简单使用 async 和 await

asyncawait的搭配使用,可以写出更简洁的基于 Promise 的异步行为。async 用来修饰函数表示函数中即将使用 await 表达式,async 函数一定会返回一个 promise 对象(就算没有也会被 Promise.resolve 转换);await 用于暂停整个 async 函数的执行进程并出让其控制权,只有当其等待的基于 promise 的异步操作被兑现或被拒绝之后才会恢复进程;其实 await 可以用于普通函数,也就是 await 等到的是一个普通值,它就是不会让出控制权(不阻塞后面的代码)。基本语法:

async function name([param[, param[, ... param]]]) {
    statements // 0个或者多个await表达式
}
1
2
3

我们来使用asyncawait重写之前 Promise 的例子

// 使用Promise,链式
doA()
  .then((rlt) => {
    return doB(rlt); // 返回doB的Promise对象,好让后面继续使用链式结构
  })
  .then((newRlt) => {
    return doC(newRlt);
  })
  .then((finRlt) => {
    console.log("FinalResult:", finRlt);
  })
  .catch(failFun);

// 使用async和await
async function doSomething() {
  try {
    const rlt = await doA();
    const newRlt = await doB(rlt);
    const finRlt = await doC(newRlt);
    console.log("FinalResult:", finRlt);
  } catch (failFun) {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

改写精准的错误捕获例子

// 使用Promise,链式
doA()
  .then((rlt) =>
    doB(rlt)
      .then((optRlt) => doC(optRlt))
      .catch((e) => {
        console.log(e.message);
      })
  )
  .then((newRlt) => doD(newRlt))
  .then((finRlt) => {
    console.log("FinalResult:", finRlt);
  })
  .catch(failFun);

// 使用async和await
async function doSomething() {
  try {
    const rlt = await doA();
    let optRlt = null;
    let newRlt = null;
    try {
      optRlt = await doB(rlt);
      newRlt = await doC(optRlt);
    } catch (e) {
      console.log(e.message);
    }
    const finRlt = await doD(newRlt);
    console.log("FinalResult:", finRlt);
  } catch (failFun) {}
}
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
29
30
31

# async 和 await 相关执行顺序

我们先看一下 EventLoop、Promise、async/await 等结合起来的题目:

// 输出依次为5 1 6 2 3 7 4
function testSometing() {
  console.log("1");
  return "2";
}
async function testAsync() {
  console.log("3");
  return Promise.resolve("4");
}
async function test() {
  console.log("5");
  // await "2",会转换为await Promise.resolve("2"),状态是`fulfilled`
  const v1 = await testSometing();
  console.log(v1);
  const v2 = await testAsync();
  console.log(v2);
}

test();

const promise = new Promise((resolve) => {
  console.log("6");
  resolve("7");
});
promise.then((val) => console.log(val));
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
// 输出依次为5 1 6 2 3 7 4
function testSometing() {
  console.log("1");
  return Promise.resolve("2");
}
async function testAsync() {
  console.log("3");
  return Promise.resolve("4");
}
async function test() {
  console.log("5");
  // await Promise.resolve("2"),状态是`fulfilled`
  const v1 = await testSometing();
  console.log(v1);
  const v2 = await testAsync();
  console.log(v2);
}

test();

const promise = new Promise((resolve) => {
  console.log("6");
  resolve("7");
});
promise.then((val) => console.log(val));
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
// 输出依次为5 1 6 2 3 7 4
async function testSometing() {
  console.log("1");
  return "2";
}
async function testAsync() {
  console.log("3");
  return Promise.resolve("4");
}
async function test() {
  console.log("5");
  // await Promise.resolve("2"),状态是`fulfilled`
  const v1 = await testSometing();
  console.log(v1);
  const v2 = await testAsync();
  console.log(v2);
}

test();

const promise = new Promise((resolve) => {
  console.log("6");
  resolve("7");
});
promise.then((val) => console.log(val));
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
// 输出依次为5 1 6 7 2 3 4
async function testSometing() {
  console.log("1");
  return Promise.resolve("2");
}
async function testAsync() {
  console.log("3");
  return Promise.resolve("4");
}
async function test() {
  console.log("5");
  // testSometing()里用了async标识,并且返回是`return Promise.resolve("2")`
  // 这会导致要经过ResolvePromise(promise, resolution)的处理,入参promise是
  // testSometing()对外暴露的Promise对象,入参resolution是Promise.resolve("2")
  // 处理完后`await testSometing()`这里才`fulfilled`,那么`7`会先于`2`打印
  const v1 = await testSometing();
  console.log(v1);
  const v2 = await testAsync();
  console.log(v2);
}

test();

const promise = new Promise((resolve) => {
  console.log("6");
  resolve("7");
});
promise.then((val) => console.log(val));
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

要解释上面这些例子必须先弄懂不带async/await 的异步执行顺序问题,也就是resolve 一个 Promise 对象的内容。

然后还要知道 async 函数的对外暴露的得是一个 Promise 对象,如果在 sync 函数内部return v;,它的处理和then()一样:

  • 如果return v;语句的值是一个是普通值,那 async 函数的对外暴露的 Promise 对象其实就是将这个普通值v包装成已经fulfilled的 Promise 对象。
  • 如果return v;语句的值本就是一个 Promise 对象,那么底层会调用ResolvePromise(promise, resolution)处理,入参promise就是 async 函数的对外暴露的 Promise 对象,入参resolution就是v这个 Promise 对象。会经过 PromiseResolveThenableJob 的创建和运行(占用两个 microtask 时序),最后让 async 函数的对外暴露的 Promise 对象的状态变为fulfilled

最后还要知道 await 等到的值是普通值是会被Promise.resolve()进行隐式转换(转换后是fulfilled的)。