概括
javaScript是单线程的,在执行代码时只能按顺序执行,为了解决代码执行时的阻塞,所以js是异步的,比如在遇到setTimeout时,不会定时器内容执行过后,再去执行之后的代码,而是先执行代码,等时间到后再去执行定时器。
基于这种异步的机制,javaScript有着一套自己执行代码的规则,来保证代码能够高效无阻塞的运行,这种规则就是事件循环。
node和浏览器都给js提供了运行的环境,但是二者的运行机制是稍有差异的。
浏览器:
执行代码时,会产生执行上下文,包括作用域、当前作用域中的变量、上层作用域、以及当前作用域中的this指向。由于js是单线程的,在执行前面的代码时,后面的代码等待执行,此时该部分代码(函数或者可直接执行的代码)被放到一个栈中,称为执行栈。上面js的运行只考虑了同步事件,当js执行过程中遇到了异步事件(或者定时事件),会把对应的这些事件挂起,js并将这个事件加入与当前执行栈不同的另一个队列,继续执行当前执行上下文中的同步代码,这个**存储异步事件的队列称为事件队列。**在一个执行环境中的执行栈清空之后,js此时会去查看事件队列是否为空,不为空,则继续执行这些事件。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。
事件队列分为微任务队列,宏任务队列,同一次事件循环中,微任务永远在宏任务之前执行。
process.nextTick(它指定的任务总是发生在所有异步任务之前,当前主线程的末尾。(nextTick虽然也会异步执行,但是不会给其他io事件执行的任何机会)
nodejs:
- timers: 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。
- I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。
- idle, prepare: 这个阶段仅在内部使用,可以不必理会。
- poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
- check: setImmediate()的回调会在这个阶段执行。
- close callbacks: 例如socket.on(‘close’, …)这种close事件的回调。
在 文件I/O、网络I/O 中,setImmediate()会先于setTimeout(fn,0),其他一般情况下,setTimeout(fn,0)会先于setImmediate()。因为在poll阶段后,马上就进入check队列,从而进行setImmediate的回调。后面循环了之后才到setTimeout()
但是把它们放到一个I/O回调里面,就一定是 setImmediate() 先执行,因为poll阶段后面就是check阶段。
差别:
宏任务里面嵌套了微任务,
浏览器:先执行宏任务,当遇到嵌套的微任务时,取执行微任务,再执行下一次宏任务。
nodejs:先把按照不同的阶段,把宏任务放入不同的队列,如timers,当
该队列执行完之后,再执行其嵌套的微任务。在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。
在以往的Node版本中,也就是11.0 之前, JS的执行栈的顺序是 执行同类型的所有宏任务 -> 在间隙时间执行微任务 ->event loop 完毕执行下一个event loop 而在最新版本的11.0之后, NodeJS为了向浏览器靠齐,对底部进行了修改,最新的执行栈顺序和浏览器的执行栈顺序已经是一样了 执行首个宏任务 -> 执行宏任务中的微任务 -> event loop执行完毕执行下一个eventloop