同步与异步

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

单线程与多线程

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. 遇到 Promise ,new Promise 直接执行,输出 7 。 then 被分发到微任务 Event Queue 中,我们记为 then1 。
  5. 又遇到 setTimeout ,其回调函数被分发到宏任务 Event Queue 中,我们记为 setTimeout2 中。

当第一轮事件循环宏任务结束时输出了 1 和 7,接着执行 process1 和 then1 微任务,输出 6 和 8 。

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

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

第二轮事件循环宏任务结束,输出了 2 和 4 。接下来执行process2 和 then2 这两个微任务,输出 3 和 5 。

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

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

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