同步与异步

异步:代码执行的顺序并不是按照从上到下的顺序一次性一次执行,而是在不同的时间段执行,一部分代码在“未来执行”。

单线程与多线程

JavaScript 是单线程的,怎么还存在异步,那些耗时操作到底交给谁去执行了?
JavaScript 其实就是一门语言,说是单线程还是多线程得结合具体运行环境。 JavaScript 的运行通常是在浏览器中进行的,具体由 JavaScript 引擎去解析和运行。

浏览器的内核是多线程的,一个浏览器通常由以下几个常驻的线程:

  • 渲染引擎线程:负责页面的渲染
  • JS引擎线程:负责 JavaScript 的解析和执行
  • 定时触发器线程:处理定时事件,比如 setTimeout ,setInterval
  • 事件触发线程:处理 DOM 事件
  • 异步 HTTP 请求线程:处理 HTTP 请求

注意:渲染线程和 JS 引擎线程是不能同时进行的。渲染线程在执行的时候, JS 引擎线程会被挂起。因为 JS 可以操作 DOM ,若在渲染中 JS 处理了 DOM ,浏览器可能就不知所措了。
_
虽然 JavaScript 是单线程的,可是浏览器内部不是单线程的。一些 I/O 操作、定时器的计时和事件监听(click、keydown……)等都是由浏览器提供的其它线程来完成的。

事件循环

JS 引擎用来执行栈中的同步任务,当所有同步任务执行完毕后,栈被清空,然后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。
JS 引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫事件循环
image.png
image.png
事件,用户交互,脚本,渲染,网络这些都是我们所熟悉的东西,它们都是由 event loop 协调的。触发一个 click 事件,进行一次 ajax 请求,背后都有 event loop 在运作。
注意:node 的 event loop 和浏览器端的 event loop 从规范上就存在差异,我们下面介绍的都是浏览器端的规范。

任务队列

一个事件循环中有一个或者多个 Task 队列。
当用户代理安排一个任务,必须将任务增加到事件循环的一个Task 队列中。
每一个 Task 都来源于指定的任务源,比如可以为鼠标、键盘事件提供一个 Task 事件,其它事件又是一个单独的队列。

宏任务macro task

也被称为 task。
总结来说任务的来源有:

  • srcipt
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI Rendering

    微任务micro task

    微任务在最新的标准中也被称为 jobs 。每一个 event loop 都只有一个 micro task 队列。一个 micro task 会被 push 进 micro task 队列而非 task 队列。
    通常任务 micro task 任务源有:

  • process.nextTick

  • Promise
  • Object.observe(已废弃)
  • MutaitonObserver(HTML5新特性)

    事件循环的执行过程(重点)

  1. 从 script (整体代码)开始第一次循环,之后全局上下文进入函数调用栈,直到调用栈清空(只剩全局)。
  2. 执行所有的 micro task 。
  3. 执行完 micro task 队列里的任务,有可能会渲染更新。
  4. 执行一个最老的 macro task。
  5. 到第二步,一致这样循环下去。

渲染更新会在 event loop 中的 tasks 和 microtasks 完成后进行,但并不是每轮 event loop 都会更新渲染,这取决于是否修改了 DOM 和浏览器觉得是否有必要在此时立即将新状态呈现给用户。如果在一帧的时间内(时间不确定,因为浏览器每秒的帧数总在波动,16.7 ms 只是估算,并不准确)修改了多出 DOM,浏览器可能将变动积攒起来,只进行一次绘制,这是合理的。

检验你的学习成果吧

下面我们通过一连串的例子来看看我们是否真的掌握了
例子一:

  1. console.log('script start');
  2. setTimeout(function () {
  3. console.log('setTimeout');
  4. }, 0);
  5. Promise.resolve()
  6. .then(function () {
  7. console.log('promise1');
  8. })
  9. .then(function () {
  10. console.log('promise2');
  11. });
  12. console.log('script end');

答案一:

  1. script start
  2. script end
  3. promise1
  4. promise2
  5. setTimeout

例子二:

  1. console.log('1');
  2. setTimeout(function() {
  3. console.log('2');
  4. process.nextTick(function() {
  5. console.log('3');
  6. })
  7. new Promise(function(resolve) {
  8. console.log('4');
  9. resolve();
  10. }).then(function() {
  11. console.log('5')
  12. })
  13. })
  14. process.nextTick(function() {
  15. console.log('6');
  16. })
  17. new Promise(function(resolve) {
  18. console.log('7');
  19. resolve();
  20. }).then(function() {
  21. console.log('8')
  22. })
  23. setTimeout(function() {
  24. console.log('9');
  25. process.nextTick(function() {
  26. console.log('10');
  27. })
  28. new Promise(function(resolve) {
  29. console.log('11');
  30. resolve();
  31. }).then(function() {
  32. console.log('12')
  33. })
  34. })

答案二:在浏览器端是这样,在node环境就是其它的结果了

  1. 1 7 6 8
  2. 2 4 3 5
  3. 9 11 10 12

解析:
第一轮事件循环:

  1. 整体 script 作为第一个宏任务进入主线程,遇到 console.log ,输出 1
  2. 遇到 setTimeout ,其回调函数被分发到宏任务 Event Queue 中,我们暂且记为 setTimeout1
  3. 遇到 process.nextTick() ,其回调函数被分发到微任务 Event Queue 中,我们记为 process1
  4. 遇到 Promisenew Promise 直接执行,输出 7then 被分发到微任务 Event Queue 中,我们记为 then1
  5. 又遇到 setTimeout ,其回调函数被分发到宏任务 Event Queue 中,我们记为 setTimeout2 中。

当第一轮事件循环宏任务结束时输出了 17,接着执行 process1 和 then1 微任务,输出 68

接着第二轮事件循环setTimeout1 宏任务开始:

  1. 首先输出 2
  2. 遇到 process.nextTick() ,将其分发到微任务 Event Queue 中,记为 process2
  3. 遇到 Promisenew Promise 立即执行输出 4then 被分发到微任务 Event Queue 中,记为 then2

第二轮事件循环宏任务结束,输出了 24 。接下来执行process2then2 这两个微任务,输出 35

接着第三轮事件循环setTimeout2 宏任务开始:

  1. 首先输出 9
  2. 遇到 process.nextTick() ,将其分发到微任务 Event Queue 中,记为 process3
  3. 遇到 Promisenew Promise 立即执行输出 11then 被分发到微任务 Event Queue 中,记为 then3
    第二轮事件循环宏任务结束,输出了 911 。接下来执行 process3then3 这两个微任务,输出 1012

demo演示
JavaScript 运行机制详解:再谈Event Loop——阮一峰
从event loop规范探究javaScript异步及浏览器更新渲染时机 —— 杨敬卓写得很详细
Tasks, microtasks, queues and schedules有关于代码的执行过程可以看
深入理解JavaScript的执行机制(同步和异步)