🗒️宏观任务和微观任务
整理的笔记,来源详见文末
Last updated
整理的笔记,来源详见文末
Last updated
JavaScript 引擎是常驻内存的,它等着宿主环境(浏览器或 Node)将 JavaScript 代码或者函数传递给它,进而将其执行。
在 ES3 及更早的版本中,JavaScript 本身还没有异步执行代码的能力,这就意味着,每当宿主环境传递给 JavaScript 引擎一段代码的时候,引擎就会直接顺次执行。这个任务也就是宿主发起的任务。
在 ES5 之后,JavaScript 引入了 Promise,这样就可以不依赖宿主了,JavaScript 引擎本身就可以发起任务。
在 JSC 引擎的理论体系中,将宿主发起的任务称为宏观任务,将 JavaScript 引擎发起的任务称为微观任务。在 Node 的理论体系中,将宿主发起的任务称为事件,因为在操作系统中,通常等待的行为都是一个事件循环。在底层的 C/C++ 代码中,这个事件循环是跑在独立的线程中的。伪代码如下:
整个事件循环做的事情基本上就是反复地“等待-执行”,这里每次执行的过程都是一个宏观任务。
在宏观任务中,JavaScript 的 Promise 还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此每个宏观任务中又包含了一个微观任务队列:
有了宏观任务和微观任务的机制,我们就可以实现宿主级和 JavaScript 引擎级的任务了。比如 Promise 会永远在队列的尾部添加微观任务,而 setTimeout 等宿主 API 会添加宏观任务。
如何分析异步执行的顺序:
首先我们分析有多少个宏任务
在每个宏任务中,分析有多少个微任务
根据调用次序,确定宏任务中的微任务执行次序
根据宏任务的触发规则和调用次序,确定宏任务的执行次序
确定整个顺序
以上代码的输出顺序是:a b c。
d 必定发生在 c 之后,因为 Promise 产生的是 JavaScript 引擎内部的微任务,而 setTimeout 是浏览器 API,它产生宏任务。
即使耗时一秒的 c1 执行完毕,再排队列微任务 c2,它仍然先于 d 执行了。这很好地解释了微任务优先的原理。
利用 Promise 把 setTimeout 封装成可以用于异步的函数
setTimeout 把整个代码分割成了 2 个宏观任务,这里不论是 5 秒还是 0 秒,都是一样的。
第一个宏观任务中,包含了先后同步执行的 console.log(“a”); 和 console.log(“b”);。
setTimeout 后,第二个宏观任务执行调用了 resolve,然后 then 中的代码异步得到执行,所以调用了 console.log(“c”),最终输出的顺序才是: a b c。
Promise 是 JavaScript 中的一个定义,但在实际编写代码时,它似乎并没有比回调的方式书写更简单。但是从 ES6 开始,我们有了 async/await,用它和 Promise 配合能够有效地改善代码结构。
async/await 提供了用 for、if 等代码结构来编写异步的方式,它的运行时基础是 Promise。async 函数必定返回 Promise,我们把所有返回 Promise 的函数都可以认为是异步函数。async 函数的强大之处在于可以嵌套,我们定义了一批原子操作,然后可以利用它来组合出新的 async 函数。
此外,generator/iterator 也常常和异步放在一起讲,但必须说明的是 generator/iterator 并不是异步代码,它们只是在缺少 async/await 的时候,一些框架(比如著名的 co)使用这样的特性来模拟 async/await。所以在有了 async/await 之后,用 generator/iterator 来模拟异步的方法应该被废弃掉。