vlambda博客
学习文章列表

js执行机制:event loop(多图理解)

是什么

我们都知道js是单线程运行,那么在单线程进程中具体如果执行js的脚本,你或许还不清晰,那么该文章你就得看下去了。

js脚本(任务)的执行遵循一个机制:event loop,中文称为“事件循环”吧。顾名思义,表示的是周而复始不断循环执行任务事件的一个过程机制

这个机制主要是为了实现如何在js单线程运行的大背景下实现异步,避免出现脚本阻塞的情况。

在说明这个机制之前,我们先了解一下基础的概念,方便之后的理解

任务的区分

同步与异步任务

我们知道任务可以划分为同步任务和异步任务,我这里不说具体标准的定义,通俗说一下其意思。

同步任务:能马上被执行的,按照一定顺序依次被执行的任务

异步任务:等到一定触发时机发生了,才会被执行对应的操作,并不是马上就执行

宏任务与微任务
  • macro-task(宏任务):

整体的script、setTimeout、setInterval、I/O、ui渲染、setImmediate(用法参考:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/setImmediate)

以上脚本操作产生的是宏任务,可能有些资料把宏任务称呼为 task

  • micro-task(微任务):

Promise的方法,如then等、process.nextTick(Node.js)、MutationObserve(用法参考:http://javascript.ruanyifeng.com/dom/mutationobserver.html)

注意,在微任务中, process.nextTick的触发顺序会比其他微任务要先执行

以上脚本操作产生的是微任务

任务执行流程

js是单线程运行的,只有一个进程在执行着脚本任务,即主线程,那什么任务才会进入主线程呢,这是由一个叫 call stack(调用栈/执行栈)的家伙来处理的。

从宏观上看, eventloop就是这么一个循环

从宏任务队列中找到最开始的任务,此刻是有微任务,如果有,则执行完毕后执行此刻所有的微任务,执行完重新回到宏任务队列中找最开始的任务执行(原先的最开始任务已经出队了,原本排第二的任务现变成最开始的任务了);若无,也是重新回到宏任务队列中找最开始的任务执行;然后按照刚才的顺序再循环一遍。

以上这个循环不断周而复始的过程就是 eventloop

那么,从什么时候就已经划分好宏任务和微任务呢,即宏任务和微任务的队列是如何产生呢?看下面这个图先大体感受一下

js执行机制:event loop(多图理解)

上面的图我画的较其他资料都要仔细点,是个详细版的事件循环流程图。

我们可以看到,在异步操作的触发时机发生时,就会把任务入队,入哪个队呢,就按照我上面说的,是什么样的脚本产生就归属哪类(如 setTimeout产生的是宏任务)

这里的 eventtable可以理解成是一个注册机构,用来注册异步操作的回调函数的。

因为我们说了,整体 script脚本也算是一个宏任务,所以进入页面,执行最初始的脚本时就算是开始了从宏任务开头的事件循环这么一个过程了。

如果上面的图你还是不太理解,或许你需要了解一下 call stack的一些原理。

call stack原理

这里有一份叫 main.js的代码:

 
   
   
 
  1. var firstFunction = function(){

  2. console.log("I'm first!");

  3. };

  4. var secondFunction = function(){

  5. firstFunction();

  6. console.log("I'm second!");

  7. };

  8. secondFunction();

以这份代码为例子举例说明。

  • 当第一次执行 main.js时,调用栈的初始初始状态是这样的(上也说了,script脚本算是一个宏任务):

js执行机制:event loop(多图理解)

  • 当调用 secondFunction时,此刻调用栈情况:

js执行机制:event loop(多图理解)

  • 在 secondFunction里还调用了 firstFunction,此刻调用栈:

js执行机制:event loop(多图理解)

  • 当执行完了 firstFunction:

js执行机制:event loop(多图理解)

  • 同样,执行完了 secondFunction之后,就算执行完了 main.js这个宏任务了,所以会有以下变化:


以上是调用栈的执行原理。。

请结合原理和上面详细版的事件循环好好理解当中的过程。

练习理解

以这份代码为例子,按照上述描述的过程,判断输出顺序

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

分析思路:

  1. 执行同步任务,输出了1

  2. 代码走到第一个 setTimeout,产生第一个宏任务

  3. 代码走到 process.nextTick,产生1个微任务

  4. 代码走到 newPromise,执行同步任务输出了7,并产生了一个微任务( then方法)

  5. 代码走到第二个 setTimeout,产生第二个宏任务

  6. 代码走完了,此刻是已经执行完了该 script代码,第一个宏任务完了。此刻有一个宏任务队列,由两个 setTimeout任务构成,有一个微任务队列,由 process.nextTick和 promise.then任务构成。

  7. 按照event loop流程,宏任务执行完就执行所有微任务, process.nextTick是较其他微任务优先的,先输出6,接着输出8

  8. 执行新的宏任务,在宏任务队列中执行最开始入队的宏任务,即第一个 setTimeout,执行里面的同步任务,输出2和4,并产生了两个微任务

  9. 新的宏任务执行完了(第一个 setTimeout),执行此刻的所有微任务,输出3、5

  10. 开始新的宏任务(第二个 setTimeout),执行里面的同步脚本,输出9、11。同样的,产生了两个微任务

  11. 执行微任务,输出10、12

所以最终的输出顺序是1-7-6-8-2-4-3-5-9-11-10-12

最后

我这里没有过多的用文字述说很多,因为其实那个详细版的那个流程图就已经把最关键的说出来了,大家好好体会那个图,结合最后的例子。

这里说的主要是js客户端的event loop ,关于node.js服务器端的event loop是不同的,这里就不在这里叙述说了。