添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

为什么会有异步

JavaScript 是单线程按照代码顺序一行行执行的, 按理说常规的代码应该是同步执行的:遇到函数调用形如 functionName(params) 时, 将函数放入 Call Stack 调用栈中执行, 执行完后从调用栈中 pop 出并返回结果, 再继续执行下一行代码。

但平时我们用 JavaScript 所写的很多功能并不是 JavaScript, 而是 JS 调用外部接口来实现的, 如 Web Browser APIs(而浏览器是用 c++ 实现的相应功能)。这些功能的执行并不是在 JS 的函数调用栈中, 而是在浏览器中, 并不会阻塞 JS 继续执行后面的代码, 这就形成了异步。

在这样的背景下, 我们在执行诸如数据请求这类比较耗时的操作时, 和远程建立连接并取回数据的活实际是浏览器在做, 而不是 JS, 所以任务并不会放到 JS 的调用栈中, JS 仍然不受影响继续往下执行, 从而不会造成用户在等待数据返回期间页面完全不能操作的情况。

异步的含义

异步代码不按照看到的顺序执行, 当异步执行的函数返回给 JS 时, JS 才处理它。

区分浏览器和 JS 的世界

平时我们使用 JS 做的很大一部分都不是 JS 的功能, 而是浏览器的功能。例如, setTimeout 是浏览器的定时器功能, “setTimeout” 是 JS 和浏览器交互的 API。JS 同样没有发送网络请求的能力, 这也是浏览器提供的功能。

下面这些常用功能容易被误认为是 JS 的功能, 但实际上是浏览器的功能,JS 只是调用了浏览器的 API。

console - console
sockets
Network request - xhr、fetch
HTML DOM - Document
Timer - setTimeout
localStorage - localStorage
IndexedDB

PS. 虽然这些功能很常用, 但知道了这件事, 我才明白了为什么说 node.js 是可以脱离浏览器环境执行的语言。

Callback Queue 和 event loop

Callback Queue

示例和说明1

先通过下面一段定时器代码来说明一下异步执行的过程。

function printHello(){ console.log("Hello") }

setTimeout(printHello,1000)

console.log("Me first!")

第一行在 global memory 中定义了一个名为 printHello 的函数。

执行到第二行时, JS 做的是调用一下浏览器里的定时器功能, 调用后 不需要等到 1000ms 结束 , JS 已经完成了呼叫浏览器帮忙计时的任务, 直接去往下一行了。这里浏览器里的 printHello 是一个引用, 而不是复制了一份代码。
与此同时, 浏览器的定时器开始计时, 看是否到了 1000ms, 这和 JS 代码执行是分开独立的。

JS继续执行第三行, 在 console 里打印 “Me first!”, 此时常规的 JS 代码已经执行完成, 时间可能过去了 1ms 还不到。
但是浏览器仍在在后台“滴滴答答”地执行计时。

到了 1000ms 时, 计时结束, printHello 调头回到 JS 的执行环境, 并被放回 JS 的 Callback Queue 回调队列 中, 这个队列并不做执行的工作, 和 Call Stack 也是相互独立的。
由于此时 global 中的代码也都执行完了(也就是所有 JS 代码都按顺序执行完了), printHello 从 Callback Queue 出队, 进入 JS 函数调用栈, 执行。

示例和说明2

继续看一个例子。现在把上面的定时器时间改为 0ms, 并增加一个常规的 JS 函数, 并假设这个函数需要 1s 的执行时间。

function printHello(){ console.log("Hello") }
function blockFor1Sec(){ // 假设这个函数执行需要 1000ms }

setTimeout(printHello,0)

blockFor1Sec()

console.log("Me first!")

打印出来的顺序如下, 虽然定时器设置的是 0s, 仍在最后打印出来
print_async_exp.png

这是因为即使计时时间是 0ms, 0ms结束时, printHello 进入的是 Callback Queue, JS 会优先执行完 Call Stack 中的函数, 再将 Callback Queue 中的函数挪到 Call Stack 中执行。

下面来看具体的执行过程, 从函数定义后调用 setTimeout 开始
0ms 调用 setTimeout, 浏览器计时, 计时结束后 printHello 先进入 Callback Queue等待。
image.png

1ms JS 执行调用栈最底下的 global(), 执行下一行, 调用 blockFor1Sec。
step2.png

1001ms 执行 blockFor1Sec 用了 1s, 这时候执行 global() 的代码, 到下一行调用 console.log(‘Me First!’)。printHello 仍然在回调队列中等待。
image.png

1002ms global 中所有的代码都执行完了, printHello 从回调队列中出队, 压入调用栈。
3step3.png

规则总结

将函数从 Callback Queue 中移入调用栈中的时机是所有的同步代码都执行完毕后。应用这个规则, 就可以确定函数执行的顺序。

event loop

那么 JS 是如何知道可以执行 callback 中的内容的呢, 这就涉及了关键的 event loop 机制。event loop 在每一行代码执行前, 都会不停检查:

1.Call Stack 中的函数是否全部执行完了, 也包括调用栈最底下全局环境下的代码是否执行完了。

2.Callback Queue 中是否有待执行函数。

3.当 Call Stack 中所有的代码都执行完, 就会去 Callback Queue 中把函数移入 Call Stack 中执行。

Promises 和 Microtask Queue

Promise 对象

以 fetch 为例, 执行 fetch 会同时做两件事, 一是在 JS 中返回一个 Promise 对象, 另一方面, 在浏览器中, 会触发浏览器来进行网络请求。

其返回的 Promise 对象的属性如下, 这些属性都不能使用点语法取得:

1.value: 默认值是 null, 当浏览器请求的数据返回了, global 代码执行完成后, 会立刻自动更新 value 的值。

2.onFulfilled: 默认是空数组, .then() 中要执行的函数会 push 到这个数组中。当网络请求返回了数据, value 的值更新后, 数组中的函数会进入 Microtask Queue 微任务队列, 等待执行。

3.onRejection: 默认是一个空数组。.catch() 中要执行的函数会 push 到这个数组中。

会返回 Promise 对象的内置函数可以在 MDN 上查到。

Microtask Queue

Promise 对象 .then() 方法中的回调函数在返回值更新后会进入微任务队列, 它和前面说的回调队列是独立的, 并且里面的函数执行优先级高于回调队列。微任务队列中的函数同样是先进先出。

执行过程示例

示例代码

function display(data){console.log(data)}
function printHello(){console.log("Hello")}
function blockFor300ms(){ // 假设这个函数执行需要 300ms }

setTimeout(printHello, 0)

const futureData = fetch('https://twitter.com/will/tweets/1')
futureData.then(display)

blockFor300ms()
console.log("Me first!")

// 输出顺序
// Me first!
// data 的值
// Hello

执行过程

从3个函数定义后开始, 上述代码执行过程如下:

0ms 触发浏览器的定时器功能计时 0 ms, 0ms 计时完成, printHello 进入 Callback 队列
4pms0.png

1ms 执行 fetch(), 返回一个 Promise 对象给 futureData 标签, 同时告诉浏览器进行网络请求(包括 主机名、地址、方法这些信息)

执行 fetchData.then(display), 将 display 方法存入 Promise 对象 onFulfilled 属性对应的数组中

2ms 执行 blockFor300ms, 这个程序执行要 300ms, 这是同步代码所以 JS 会在 300ms 后再执行下一行
5pms1.png

270ms 假设此时异步请求的数据返回了, Promise 对象 futureData 的 value 属性值会更新为请求回来的数据。同时自动将 Promise 对象 onFulfilled 数组中的函数, 注册进 Microtask Queue。
6pms2.png

302ms blockFor300ms 执行完了, global 中还有代码, 所以继续执行下一行 console.log(“Me first”), 会直接在 console 输出内容。

303ms 整个过程中 eventloop 一直在进行检测, 此时调用栈中没有函数在执行, global 中的代码也执行完了; Microtask Queue 和 Callback Queue 中都有待执行的函数, 优先将 Microtask Queue 中的函数“移入”调用栈。在 console 打印出 data 的值。
7pms3.png

304ms 调用栈 和 Microtask Queue 都空了, 将 Callback Queue 中的 printHello “移入”调用栈中执行。在 console 面板中输出 “Hello”。
8pms3.png

总结

分类

1.常规的同步代码函数调用都会直接进入 Call Stack 调用栈中, 以最高优先级调用

2.老的回调 Api, 如 setTimeout, 其回调函数会进入 Callback Queue 中

3.所有通过 then 方法附属于某个 Promise 对象的函数, 当 Promise 对象 value 属性的值自动更新时, 进入 Microtask Queue 中

执行顺序

0.优先执行完 Call Stack 中的函数, 和 global 中的代码, 直到所有代码都执行完

1.其次将 Microtask Queue 中的函数移入 Call Stack 中执行

2.最后将 Callback Queue 中的函数移入 Call Stack 中执行

学习资料

视频: JavaScript: The Hard Parts, v2

  • 本文标题: 异步和Promise执行过程图解
  • 本文作者: Super喵喵玄
  • 本文链接: http://miaomiaoxuan.cn/2020/01/29/异步和Promise执行过程图解/
  • 发布时间: 2020-01-29
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
    Your browser is out-of-date!

    Update your browser to view this website correctly. Update my browser now

  •