1.JavaScript是单线程语言
既然JavaScript是单线程语言,那么就会存在一个问题,所有的代码都是一句一句的来执行。

一、概念

1.事件循环(Event Loop)
JavaScript 执行机制 - 图1

什么是Event Loop?
javascript有一个主线程 main thread [θred], 和调用栈 call-stack 也称之为执行栈。所有的任务都会放到调用栈中等待主线程来执行。
1.任务队列(task queue)
每一个语句就是一个任务, task Queue就是承载的任务队列。JavaScript的Event Loop 就是会不断的过来找这个queue,问有没有task可以运行
2.同步任务(SyncTask)、异步任务(AsyncTask)
同步任务说白了就是主线程来执行的时候立即就能执行的代码
异步任务就是你先去执行别的 task,等我这 xxx 完之后再往 Task Queue 里面塞一个 task 的同步任务来等待被执行

  1. setTimeout(()=>{
  2. console.log(2)
  3. });
  4. console.log(1);

例如: setTimeout就是一个异步任务,主线程去执行的时候遇到setTimeout发现是一个异步任务。就先注册一个任务的回调,然后接着执行下面的语句console.log(1),等上面的异步任务等待时间到了之后,在执行console.log(2)。
JavaScript 执行机制 - 图2
1.主线程自上而下执行所有代码
2.同步任务直接进入到主线程被执行,异步任务则进入到Event Table 并注册相应的回调函数
3.异步任务完成后,Event Table 会将这个函数移入Event Queue
4.主线程任务执行完了以后,会从Event Queue中读取任务,进入到主线程去执行
5.循环如上
上述动作不断循环,就是我们所说的事件循环(Event Loop)

  1. let data = [];
  2. $.ajax({
  3. url:www.javascript.com,
  4. data:data,
  5. success:() => {
  6. console.log('发送成功!');
  7. }
  8. })
  9. console.log('代码执行结束');
  10. 1.ajax进入Event Table,注册回调函数success
  11. 2.执行console.log('代码执行结束')
  12. 3.ajax事件完成,回调函数success进入Event Queue
  13. 4.主线程从Event Queue读取回调函数success并执行

3.宏任务(MacroTask)、微任务(MicroTask)
JavaScript 的任务不仅仅分为同步任务和异步任务,同时从另一个维度,也分为了宏任务(MacroTask)和微任务(MicroTask)。
先说说 MacroTask,所有的同步任务代码都是MacroTask(这么说其实不是很严谨,下面解释),setTimeout、setInterval、I/O、UI Rendering 等都是宏任务。
MicroTask,为什么说上述不严谨我却还是强调所有的同步任务都是 MacroTask 呢,因为我们仅仅需要记住几个 MicroTask 即可,排除法!别的都是 MacroTask。MicroTask 包括:Process.nextTick、Promise.then catch finally(注意我不是说 Promise)、MutationObserver。
一次事件循环回来后,开始去执行Task Queue中的Task;但是这里的Task有有优先级。所以优先执行MicroTask Queue 中的task,执行完后在执行MacroTask Queue中的task

  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. })

第一轮事件循环流程分析如下:

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

  • 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。

  • 我们发现了process1then1两个微任务。
  • 执行process1,输出6。
  • 执行then1,输出8。

好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:

  • 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。 | 宏任务Event Queue | 微任务Event Queue | | :—-: | :—-: | | setTimeout2 | process2 | | | then2 |

  • 第二轮事件循环宏任务结束,我们发现有process2then2两个微任务可以执行。

  • 输出3。
  • 输出5。
  • 第二轮事件循环结束,第二轮输出2,4,3,5。
  • 第三轮事件循环开始,此时只剩setTimeout2了,执行。
  • 直接输出9。
  • process.nextTick()分发到微任务Event Queue中。记为process3
  • 直接执行new Promise,输出11。
  • then分发到微任务Event Queue中,记为then3。 | 宏任务Event Queue | 微任务Event Queue | | :—-: | :—-: | | | process3 | | | then3 |

  • 第三轮事件循环宏任务执行结束,执行两个微任务process3then3

  • 输出10。
  • 输出12。
  • 第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

链接知识点:
https://juejin.im/post/59e85eebf265da430d571f89