Javascript中的执行机制——Event Loop
众所周知,Javascript是单线程语言, 这就意味着,所有的任务都必须按照顺序执行,只有等前面的一个任务执行完毕了,下一个任务才能执行。如果前面一个任务耗时很长,后一个任务就得一直等着,因此,为了实现主线程的不阻塞,就有了Event Loop。
1、javascript事件循环
首先,我们先了解一下同步任务和异步任务,同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
为了更好的了解执行机制,看下图
以上图说明主线程在执行的时候产生堆(内存分配)和堆栈(执行上下文),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,此时的流程如下图:
接着往下执行,执行环境栈中会创建一个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) }) })