同步和异步
程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行)。而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系。
同步 | 异步 | |
---|---|---|
理解 | 同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去 | 异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率 |
阻塞/非阻塞 | 阻塞 | 非阻塞 |
优点 | 同步是按照顺序一个一个来,不会乱掉,更不会出现上面代码没有执行完就执行下面的代码 | 异步是接取一个任务,直接给后台,在接下一个任务,一直一直这样,谁的先读取完先执行谁的 |
缺点 | 解析的速度没有异步的快 | 没有顺序 ,谁先读取完先执行谁的 ,会出现上面的代码还没出来下面的就已经出来了,会报错 |
同步异步 , 举个例子来说,一家餐厅吧来了5个客人,同步的意思就是说,来第一个点菜,点了个鱼,好, 厨师去捉鱼杀鱼,过了半小时鱼好了给第一位客人,开始下位一位客人,就这样一个一个来,按顺序来。
相同, 异步呢,异步的意思就是来第一位客人,点什么,点鱼,给它一个牌子,让他去一边等吧,下一位客人接着点菜,点完接着点让厨师做去吧,哪个的菜先好就先端出来。
使用异步的情况——阻塞
阻塞
在前端编程中(甚至后端有时也是这样),我们在处理一些简短、快速的操作时,例如计算 1 + 1 的结果,往往在主线程中就可以完成。主线程作为一个线程,不能够同时接受多方面的请求。所以,当一个事件没有结束时,界面将无法处理其他请求。
现在有一个按钮,如果我们设置它的 onclick 事件为一个死循环,那么当这个按钮按下,整个网页将失去响应。这叫做阻塞。
为了避免这种情况的发生,我们常常用子线程来完成一些可能消耗时间足够长以至于被用户察觉的事情,比如读取一个大文件或者发出一个网络请求。因为子线程独立于主线程,所以即使出现阻塞也不会影响主线程的运行。但是子线程有一个局限:一旦发射了以后就会与主线程失去同步,我们无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,我们是无法将它合并到主线程中去的。
为了解决这个问题,JavaScript 中的异步操作函数往往通过回调函数来实现异步任务的结果处理。
js是单线程的
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
一个线程是一个基本的处理过程,程序用它来完成任务。每个线程一次只能执行一个任务:
1 | Task A --> Task B --> Task C |
每个任务顺序执行,只有前面的结束了,后面的才能开始。
支持多线程的编程语言可以使用计算机的多个内核,同时完成多个任务:
1 | Thread 1: Task A --> Task B |
WebWorker
当在 HTML 页面中执行脚本时,页面的状态是不可响应的,直到脚本已完成。
web worker 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。您可以继续做任何愿意做的事情:点击、选取内容等等,而此时 web worker 在后台运行。
通过 Web workers 可以把一些任务交给一个名为worker的单独的线程,这样就可以同时运行多个JavaScript代码块。一般来说,用一个worker来运行一个耗时的任务,主线程就可以处理用户的交互(避免了阻塞)
1 | Main thread: Task A --> Task C |
下面的代码检测是否存在 worker,如果不存在,- 它会创建一个新的 web worker 对象,然后运行 “demo_workers.js” 中的代码:
1 | if(typeof(w)=="undefined") |
然后我们就可以从 web worker 发生和接收消息了。
向 web worker 添加一个 “onmessage” 事件监听器:
1 | w.onmessage=function(event){ |
当我们创建 web worker 对象后,它会继续监听消息(即使在外部脚本完成之后)直到其被终止为止。
如需终止 web worker,并释放浏览器/计算机资源,请使用 terminate() 方法:
1 | w.terminate(); |
WebWorker的局限性
Webworker不能访问DOM,无法对UI进行更新。
虽然在worker里面运行的代码不会产生阻塞,但是基本上还是同步的。当一个函数依赖于几个在它之前运行的过程的结果,这就会成为问题。
- ```
Main thread: Task A –> Task B在这种情况下,假设Task D 要同时使用 Task B 和Task C的结果,如果我们能保证这两个结果同时提供,程序可能正常运行,但是这不太可能。如果Task D 尝试在其中一个结果尚未可用的情况下就运行,程序就会抛出一个错误。1
2
3
4
5
6
在这种情况下,比如说Task A 正在从服务器上获取一个图片之类的资源,Task B 准备在图片上加一个滤镜。如果开始运行Task A 后立即尝试运行Task B,你将会得到一个错误,因为图像还没有获取到。
2. ```
Main thread: Task A --> Task B --> |Task D|
Worker thread: Task C -----------> | |
所以需要像下面这样异步运行:
1 | Main thread: Task A Task B |
异步JS
一个异步过程通常是这样的:
主线程发起一个异步请求,相应的工作线程接收请求并告知主线程已收到(异步函数返回);主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,通知主线程;主线程收到通知后,执行一定的动作(调用回调函数)。
同步JS的情况:每一个操作在执行的时候,其他任何事情都没有发生 — 网页的渲染暂停。任何时候只能做一件事情, 只有一个主线程,其他的事情都阻塞了,直到前面的操作完成。
异步callbacks回调函数
回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)代码。
在JavaScript中函数也是对象的一种,同样对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。
1 | // 这里的print就是回调函数 |
特点
不会立刻执行:作为参数传给一个函数时,传入的只是函数的定义而不会立刻执行,在调用它的函数中需要()运算符的调用才能执行
闭包:回调函数是闭包,能够访问外面的变量
例如:
1
2
3
4
5
6A=(x)=>{alert(x)};
B=(callback)=>{
let y=1;
callback(y)
};
B(A);执行前最好确认是个函数
1
2
3
4
5
6function add(num1, num2, callback){
var sum = num1 + num2;
if(typeof callback === 'function'){
callback(sum);
}
}this的执行上下文:上下文不是它自身的上下文,而是调用它的函数所在的上下文
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// add的上下文指向的是window,所以sum赋值给window
var obj = {
sum: 0,
add: function(num1, num2){
this.sum = num1 + num2;
}
};
function add(num1, num2, callback){
callback(num1, num2);
};
add(1,2, obj.add);
console.log(obj.sum); //=>0
console.log(window.sum); //=>3
// 通过apply解决
var obj = {
sum: 0,
add: function(num1, num2){
this.sum = num1 + num2;
}
};
function add(num1, num2, callbackObj, callback){
callback.apply(callbackObj, [ num1, num2 ]);
};
add(1,2, obj, obj.add);
console.log(obj.sum); //=>3
console.log(window.sum); //=>undefined允许传递多个回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function successCallback() {
// Do stuff before send
}
function successCallback() {
// Do stuff if success message received
}
function completeCallback() {
// Do stuff upon completion
}
function errorCallback() {
// Do stuff if error received
}
$.ajax({
url: "http://fiddle.jshell.net/favicon.png",
success: successCallback,
complete: completeCallback,
error: errorCallback
});回调地狱:一个回调函数中可以嵌入另一个回调函数,对于这种情况出现多层嵌套时,代码会难以阅读和维护,这个时候可以采用命名回调函数的方式调用,或者采用模块化管理函数,也可以用promise模式编程。
优点
- DRY,避免重复代码。
- 可以将通用的逻辑抽象。
- 加强代码可维护性。
- 加强代码可读性。
- 分离专职的函数。
使用场景
- 异步编程。
- 事件监听、处理。
- setTimeout、setInterval方法。
- 通用功能,简化逻辑。
注意点
请注意,不是所有的回调函数都是异步的 — 有一些是同步的。一个例子就是使用Array.prototype.forEach()来遍历数组
1 | const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus']; |
缺陷
- 回调地狱
- 每层嵌套都需要故障回调,而Promise只需要一个catch
- 异步回调不是很优雅。
- Promise回调总是按照它们放在事件队列中的严格顺序调用;异步回调不是。
- 当传入到一个第三方库时,异步回调对函数如何执行失去完全控制。
setTimeout()
1 | var timeoutID = setTimeout(function[, delay, arg1, arg2, ...]); |
指定的时间(或延迟)不能保证在指定的确切时间之后执行,而是最短的延迟执行时间。在主线程上的堆栈为空之前,传递给这些函数的回调将无法运行。
例子:
1 | // 等待两秒钟后显示alert内容 |
1 | // 非匿名函数 |
1 | // 传参 |
1 | // 清除超时 |
使用0用作setTimeout()的回调函数会立刻执行,但是在主线程代码运行之后执行。
1 | // 先输出Hello后输出World |
setInterval()
这与setTimeout()
的工作方式非常相似,只是作为第一个参数传递给它的函数,重复执行的时间不少于第二个参数给出的毫秒数,而不是一次执行。
1 | // 每隔1000毫秒,即1秒显示当前时间 |
1 | // 清除interval |
重复运行相同代码的方法
1 | // setTimeout递归实现 |
1 | // setInterval |
两者区别:
- 递归
setTimeout()
保证执行之间的延迟相同,例如在上述情况下为100ms。 代码将运行,然后在它再次运行之前等待100ms,因此无论代码运行多长时间,间隔都是相同的。 - 使用
setInterval()
的示例有些不同。 我们选择的间隔包括执行我们想要运行的代码所花费的时间。假设代码需要40毫秒才能运行 - 然后间隔最终只有60毫秒。 - 当递归使用
setTimeout()
时,每次迭代都可以在运行下一次迭代之前计算不同的延迟。 换句话说,第二个参数的值可以指定在再次运行代码之前等待的不同时间(以毫秒为单位)。 - 当你的代码有可能比你分配的时间间隔,花费更长时间运行时,最好使用递归的
setTimeout()
- 这将使执行之间的时间间隔保持不变,无论代码执行多长时间,你不会得到错误。
requestAnimationFrame()
requestAnimationFrame()
是一个专门的循环函数,旨在浏览器中高效运行动画。它基本上是现代版本的setInterval()
—— 它在浏览器重新加载显示内容之前执行指定的代码块,从而允许动画以适当的帧速率运行,不管其运行的环境如何。
使用 setTimeout
或 setInterval
来执行动画之类的视觉变化,但这种做法的问题是,回调将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会使我们丢失帧,导致卡顿。
常见用法:
1 | function draw() { |
requestAnimationFrame()
总是试图尽可能接近60帧/秒的值,当然有时这是不可能的如果你有一个非常复杂的动画,你是在一个缓慢的计算机上运行它,你的帧速率将更少。
如果使用setInterval代替以上代码:另一方面setInterval()
需要指定间隔。我们通过公式1000毫秒/60Hz得出17的最终值,然后将其四舍五入。
1 | function draw() { |
1 | //撤销 |
缺陷
无法使用requestAnimationFrame()
选择特定的帧速率。如果需要以较慢的帧速率运行动画,则需要使用setInterval()
或递归的setTimeout()
。
Promise
Promise 是一个 ECMAScript 6 提供的类,目的是更加优雅地书写复杂的异步任务。Promise对象用于表示一个异步操作的最终完成 (或失败)及其结果值。
一个 Promise
必然处于以下几种状态之一:
- 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。在创建Promise时候的状态。
- 已兑现(fulfilled): 意味着操作成功完成。它返回一个值,可以通过将
.then()
块链接到promise链的末尾来访问该值。.then()
块中的执行程序函数将包含promise的返回值。 - 已拒绝(rejected): 意味着操作失败。它返回一个原因(reason),一条错误消息,说明为什么拒绝promise。可以通过将
.catch()
块链接到promise链的末尾来访问此原因。
如果一个 promise 已经被兑现(fulfilled)或被拒绝(rejected),那么我们也可以说它处于已敲定(settled)状态。已决议(resolved),它表示 promise 已经处于**已敲定(settled)**状态,或者为了匹配另一个 promise 的状态被”锁定”了。
因为 Promise.prototype.then
和 Promise.prototype.catch
方法返回的是 promise, 所以它们可以被链式调用。
链式调用
我们可以用 promise.then()
,promise.catch()
和 promise.finally()
这些方法将进一步的操作与一个变为已敲定状态的 promise 关联起来。这些方法还会返回一个新生成的 promise 对象,这个对象可以被非强制性的用来做链式调用,就像这样:
1 | const myPromise = |
任何不是 throw
的终止都会创建一个”已决议(resolved)”状态,而以 throw
终止则会创建一个”已拒绝”状态。
当 .then()
中缺少能够返回 promise 对象的函数时,链式调用就直接继续进行下一环操作。因此,链式调用可以在最后一个 .catch()
之前把所有的 handleRejection
都省略掉。类似地, .catch()
其实只是没有给 handleFulfilled
预留参数位置的 .then()
而已。
链式调用中的 promise 们就像俄罗斯套娃一样,是嵌套起来的,但又像是一个栈,每个都必须从顶端被弹出。链式调用中的第一个 promise 是嵌套最深的一个,也将是第一个被弹出的。
1 | (promise D, (promise C, (promise B, (promise A) ) ) ) |
对于下面的代码,promiseA
向”已敲定”(”settled”)状态的过渡会导致两个实例的 .then
都被调用。
1 | const promiseA = new Promise(myExecutorFunc); |
一个已经处于”已敲定”(”settled”)状态的 promise 也可以接收操作。在那种情况下,(如果没有问题的话,)这个操作会被作为第一个异步操作被执行。注意,所有的 promise 都一定是异步的。因此,一个已经处于”已敲定”(”settled”)状态的 promise 中的操作只有 promise 链式调用的栈被清空了和一个事件循环过去了之后才会被执行。这种效果跟 setTimeout(action, 10)
特别相似。
1 | const promiseA = new Promise( (resolutionFunc,rejectionFunc) => { |
Promise的创建
Promise
对象是由关键字 new
及其构造函数来创建的。该构造函数会把一个叫做“处理器函数”(executor function)的函数作为它的参数。这个“处理器函数”接受两个函数——resolve
和 reject
——作为其参数。当异步任务顺利完成且返回结果值时,会调用 resolve
函数;而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用reject
函数。
1 | const myFirstPromise = new Promise((resolve, reject) => { |
想要某个函数拥有promise功能,只需让其返回一个promise即可。
1 | function myAsyncFunction(url) { |
示例:
1 | let myFirstPromise = new Promise(function(resolve, reject){ |
Promise的使用
resolve 和 reject 都是函数,其中调用 resolve 代表一切正常,reject 是出现异常时所调用的:
1 | new Promise(function (resolve, reject) { |
.then() 可以将参数中的函数添加到当前 Promise 的正常执行序列,.catch() 则是设定 Promise 的异常处理序列,.finally() 是在 Promise 执行的最后一定会执行的序列。 .then() 传入的函数会按顺序依次执行,有任何异常都会直接跳到 catch 序列:
1 | new Promise(function (resolve, reject) { |
回调函数造成的困难
1 | chooseToppings(function(toppings) { |
这很麻烦且难以阅读(通常称为“回调地狱”),需要多次调用failureCallback()
(每个嵌套函数一次),还有其他问题。
使用promise修改
1 | chooseToppings() |
promise与事件监听器类似,但有一些差异:
- 一个promise只能成功或失败一次。它不能成功或失败两次,并且一旦操作完成,它就无法从成功切换到失败,反之亦然。
- 如果promise成功或失败并且你稍后添加成功/失败回调,则将调用正确的回调,即使事件发生在较早的时间。
缺陷
Promise链可能很复杂,难以解析。如果你嵌套了许多promises,你最终可能会遇到类似的麻烦来回调地狱。
最好使用promises的链功能,这样使用更平顺,更易于解析的结构。
Promises 对比 callbacks
promises与旧式callbacks有一些相似之处。它们本质上是一个返回的对象,您可以将回调函数附加到该对象上,而不必将回调作为参数传递给另一个函数。
然而,Promise
是专门为异步操作而设计的,与旧式回调相比具有许多优点:
- 您可以使用多个then()操作将多个异步操作链接在一起,并将其中一个操作的结果作为输入传递给下一个操作。这种链接方式对回调来说要难得多,会使回调以混乱的“回调地狱”告终。(也称为末日金字塔)。
Promise
总是严格按照它们放置在事件队列中的顺序调用。- 错误处理要好得多——所有的错误都由块末尾的一个.catch()块处理,而不是在“金字塔”的每一层单独处理。
async/await
它们是基于promises的语法糖,使异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是老式同步代码。
1 | function print(delay, message) { |
1 | // 处理异常 |
async
语法
1 | async function name([param[, param[, ... param]]]) { statements } |
- name: 函数名称。
- param: 要传递给函数的参数的名称。
- statements: 函数体语句。
返回值
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。这个promise要么会通过一个由async函数返回的值被解决,要么会通过一个从async函数中抛出的(或其中没有被捕获到的)异常被拒绝。
1 | async function helloAsync(){ |
async 函数中可能会有 await 表达式,async 函数执行时,如果遇到 await 就会先暂停执行 ,等到触发的异步操作完成后,恢复 async 函数的执行并返回解析值。
await 关键字仅在 async function 中有效。如果在 async function 函数体外使用 await ,你只会得到一个语法错误。
1 | function testAwait(){ |
1 | function resolveAfter2Seconds() { |
async函数可能包含0个或者多个await
表达式。await表达式会暂停整个async函数的执行进程并出让其控制权,只有当其等待的基于promise的异步操作被兑现或被拒绝之后才会恢复进程。promise的解决值会被当作该await表达式的返回值。使用async
/ await
关键字就可以在异步代码中使用普通的try
/ catch
代码块。
async
/await
的目的为了简化使用基于promise的API时所需的语法。async
/await
的行为就好像搭配使用了生成器和promise。
async函数一定会返回一个promise对象。如果一个async函数的返回值看起来不是promise,那么它将会被隐式地包装在一个promise中。
如下代码:
1 | async function foo() { |
等价于:
1 | function foo() { |
async函数的函数体可以被看作是由0个或者多个await表达式分割开来的。从第一行代码直到(并包括)第一个await表达式(如果有的话)都是同步运行的。这样的话,一个不含await表达式的async函数是会同步运行的。然而,如果函数体内有一个await表达式,async函数就一定会异步执行。
例如:
1 | async function foo() { |
等价于
1 | function foo() { |
在await表达式之后的代码可以被认为是存在在链式调用的then回调中,多个await表达式都将加入链式调用的then回调中,返回值将作为最后一个then回调的返回值。
在接下来的例子中,我们将使用await执行两次promise,整个foo
函数的执行将会被分为三个阶段。
foo
函数的第一行将会同步执行,await将会等待promise的结束。然后暂停通过foo
的进程,并将控制权交还给调用foo
的函数。- 一段时间后,当第一个promise完结的时候,控制权将重新回到foo函数内。示例中将会将
1
(promise状态为fulfilled)作为结果返回给await表达式的左边即result1
。接下来函数会继续进行,到达第二个await区域,此时foo
函数的进程将再次被暂停。 - 一段时间后,同样当第二个promise完结的时候,
result2
将被赋值为2
,之后函数将会正常同步执行,将默认返回undefined
。
1 | async function foo() { |
注意:promise链不是一次就构建好的,相反,promise链是分阶段构造的,因此在处理异步函数时必须注意对错误函数的处理。
例如,在下面的代码中,在promise链上配置了.catch
处理程序,将抛出未处理的promise错误。这是因为p2
返回的结果不会被await处理。
1 | async function foo() { |
例子:
1 | var resolveAfter2Seconds = function() { |
async/await和Promise#then对比以及错误处理: 警告:
大多数async函数也可以使用Promises编写。但是,在错误处理方面,async函数更容易捕获异常错误
上面例子中的concurrentStart
函数和concurrentPromise
函数在功能上都是等效的。在concurrentStart
函数中,如果任一await
ed调用失败,它将自动捕获异常,async函数执行中断,并通过隐式返回Promise将错误传递给调用者。
在Promise例子中这种情况同样会发生,该函数必须负责返回一个捕获函数完成的Promise
。在concurrentPromise
函数中,这意味着它从Promise.all([]).then()
返回一个Promise。事实上,在此示例的先前版本忘记了这样做!
但是,async函数仍有可能然可能错误地忽略错误。
以parallel
async函数为例。 如果它没有等待await
(或返回)Promise.all([])
调用的结果,则不会传播任何错误。
虽然parallelPromise
函数示例看起来很简单,但它根本不会处理错误! 这样做需要一个类似于return ``Promise.all([])
处理方式。
async重写Promise链
返回 Promise
的 API 将会产生一个 promise 链,它将函数肢解成许多部分。例如下面的代码:
1 | function getProcessedData(url) { |
可以重写为单个async函数:
1 | async function getProcessedData(url) { |
注意,在上述示例中,return
语句中没有 await
操作符,因为 async function
的返回值将被隐式地传递给 Promise.resolve
。
await
await 只在异步函数里面才起作用。用于等待一个异步对象。它可以放在任何异步的,基于 promise 的函数之前。它会暂停代码在该行上,直到 promise 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了。
语法
1 | [return_value] = await expression; |
- expression: 一个 Promise 对象或者任何要等待的值。
返回值
返回 Promise 对象的处理结果。如果等待的不是 Promise 对象,则返回该值本身。
如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果。
1 | function testAwait (x) { |
正常情况下,await 命令后面是一个 Promise 对象,它也可以跟其他值,如字符串,布尔值,数值以及普通函数。
1 | function testAwait(){ |
await针对所跟不同表达式的处理方式:
- Promise 对象:await 会暂停执行,等待 Promise 对象 resolve,然后恢复 async 函数的执行并返回解析值。若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行
async function
。若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。 - 非 Promise 对象:直接返回对应的值。
例子:
如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果。
1 | function resolveAfter2Seconds(x) { |
如果该值不是一个 Promise,await 会把该值转换为已正常处理的Promise,然后等待其处理结果。
1 | async function f2() { |
如果 Promise 处理异常,则异常值被抛出。
1 | async function f3() { |
缺陷
- 您不能在非
async
函数内或代码的顶级上下文环境中使用await
运算符。这有时会导致需要创建额外的函数封包,这在某些情况下会略微令人沮丧。但大部分时间都值得。 - 浏览器对async / await的支持不如promises那样好。如果你想使用async / await但是担心旧的浏览器支持,你可以考虑使用BabelJS 库 - 这允许你使用最新的JavaScript编写应用程序,让Babel找出用户浏览器需要的更改。
资料来源:
- https://www.runoob.com/js/js-async.html
- https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous
- https://blog.51cto.com/u_14209124/2884330
- https://zhuanlan.zhihu.com/p/67452727
- https://segmentfault.com/a/1190000015806981
- https://segmentfault.com/a/1190000004322358
- https://cnodejs.org/topic/564dd2881ba2ef107f854e0b
- https://segmentfault.com/q/1010000009532089
- https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution?hl=zh-cn
- https://www.runoob.com/js/js-promise.html
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
- https://www.runoob.com/w3cnote/es6-async.html
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/await