Javascript中的执行机制——Event Loop

众所周知,Javascript是单线程语言, 这就意味着,所有的任务都必须按照顺序执行,只有等前面的一个任务执行完毕了,下一个任务才能执行。如果前面一个任务耗时很长,后一个任务就得一直等着,因此,为了实现主线程的不阻塞,就有了Event Loop。

1、javascript事件循环

首先,我们先了解一下同步任务和异步任务,同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

为了更好的了解执行机制,看下图

Javascript中的执行机制——Event Loop

以上图说明主线程在执行的时候产生堆(内存分配)和堆栈(执行上下文),JavaScript是单线程的,意味着当执行环境的堆栈中的一个任务(task)在执行的时候,其它的任务都要处于等待状态。当主进程执行到异步操作的时候就会将异步操作对应的task放到event table,指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行,因为这个过程是不断重复的,所以称为Event Loop(事件循环),接下来,我们用几个例子进行分析
eg1:

console.log(1); 
setTimeout(function () { 
    console.log(2); 
})
 console.log(3);
 //执行结果:1、3、2

我们来分析一下这段代码,首先,根据执行上下文可知,执行环境栈中就有了一个task——console.log(1),输出1。接着往下执行,因为setTimeout是异步函数,所以将setTimeout进入event table,注册了一个回调函数在event queue,我们暂且称为fun1,此时的流程如下图:

Javascript中的执行机制——Event Loop

接着往下执行,执行环境栈中会创建一个console.log(3)的task,并执行它,输出3,此时,执行环境已经没有任务了,则去Event Queue读取对应的函数,fun1被发现,进入主线程输出2,整个过程已经完成,所以输出的结果是1、3、2。
eg2:

setTimeout(function () {
      console.log(1)
   }, 3)
   setTimeout(function () {
      console.log(2)
    })
输出2,1

我们再来简单的分析一下这个列子,我们暂且称第一个setTimeout为Time1,第二个为Time2。由于两个都是异步函数,按照执行顺序,先将Time放到event Table,接着将Time移到event Table,因为Time在event Table指定要3秒后才执行,所以Time2先于Time1到注册回调函数到event queue,所以输出的结果是2,1。

2、macro-task(宏任务)、micro-task(微任务)

MacroTask: script(整体代码), setTimeout, setInterval, setImmediate(node独有), I/O, UI rendering
MicroTask: process.nextTick(node独有), Promises, Object.observe(废弃), MutationObserver

任务又分为宏任务和微任务两种,在同一个上下文中,总的执行顺序为“同步代码—>microTask—>macroTask”,根据上面event loop的流程图,我们用列子来做进一步的了解:
eg1:

setTimeout(function () {
       console.log(1);
   },0);
   console.log(2);
   process.nextTick(() => {
       console.log(3);
   });
   new Promise(function (resolve, rejected) {
       console.log(4);
       resolve()
   }).then(res=>{
       console.log(5);
   })
   setImmediate(function () {
       console.log(6)
   })
   console.log('end');
    //输出2、4、end、3、5、1、6

本例参考《JavaScript中的执行机制》,里面有详细的解释,大家可以参考下。

3、优先级

我们将上面的例子稍微改一下,将process.nextTick移到promise的后面,看下面的代码:

setTimeout(function () {
       console.log(1);
   },0);
   console.log(2);
   new Promise(function (resolve, rejected) {
       console.log(4);
       resolve()
   }).then(res=>{
       console.log(5);
   })
 process.nextTick(() => {
       console.log(3);
   });
   setImmediate(function () {
       console.log(6)
   })
   console.log('end');

按照前面的分析,先执行同步代码,先输出“2,4,end”;然后是微任务promise输出5,process.nextTick输出3;最后的宏任务输出1,6。所以结果为2,4,end,5,3,1,6,然后事实并非如此,结果还是输出2、4、end、3、5、1、6,这是因为process.nextTick注册的函数优先级高于Promise**。
关于Event Loop的其他特殊情况,大家可参考文章一篇文章教会你Event loop——浏览器和Node和Event Loop的规范和实现,里面有更详细的介绍。

4、补充

console.log(1)
    
    setTimeout(() => {
        console.log(2)
        new Promise(resolve => {
            console.log(4)
            resolve()
        }).then(() => {
            console.log(5)
        })
        setTimeout(() => {
            console.log(999)
        
        })
    })
    
    new Promise(resolve => {
        console.log(7)
        resolve()
    }).then(() => {
        console.log(8)
    })
    
    setTimeout(() => {
        console.log(9)
        new Promise(resolve => {
            console.log(11)
            resolve()
        }).then(() => {
            console.log(12)
        })
    })

相关推荐