细说JavaScript的EventLoop事件循环机制

601 阅读4分钟

1.从js单线程谈起

众所周知,javascript诞生之初的目的是为了在浏览器上实现用户的交互,以及操作DOM。因此js必须是单线程的,否则它将带来复杂的同步问题。所以单线程是js这一门语言的核心特性。

2.任务队列

但是如果只有单线程的话就会导致线程堵塞,如果前一个任务一直执行没有结束, 它就会一直占用线程,导致后面的任务无法继续执行。例如一些IO操作和Ajax等 网络操作需要等待很长的时间才能返回数据,在等待的过程中cpu的大多数时间并 没有进行运算,而是在以空闲的状态等待数据返回。这就导致cpu资源的大量浪费。 js的作者也意识到了这一点,于是产生了异步任务(asynchronous)和同步任务 (synchronous)的概念。js的运行机制如下:

1.所有同步任务都在主线程上执行,形成一个执行栈
2.除了主线程之外还有一个“任务队列”,异步任务执行完之后会,
  会在任务队列中添加一个事件
3.等主线程执行栈中的任务执行完之后,系统再去读取“任务队列”
  中的事件,此时异步任务进入执行栈执行。
4.主线程会不断重复上述三个过程

3.事件循环(Event Loop)

上述主线程从“任务队列”中读取事件,这个不断循环的过程就叫事件循环(Event Loop)。 在事件循环的过程中每一次的循环过程称为Tick。而每次Tick的过程则是本文所要讨论的重点。 在这里先说两个概念:宏任务(Macro Task)和微任务(Micro Task)

    1.宏任务(Macro Task)主要包含:script( 整体代码)、setTimeout、
    setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
    2.微任务主要包含:Promise、MutaionObserver、process.nextTick(Node环境)

tick详细过程如下:
1.执行栈先执行宏任务的代码,在执行过程中若有宏任务则将宏任务添加到宏任务队列,
  若出现微任务则将微任务添加到微任务队列。
2.等宏任务执行完毕后,执行栈开始执行微任务,执行过程中若有宏任务则将其添加到
  宏任务队列,若出现微任务则将其添加到微任务队列。
3.当前微任务执行完毕后,检查微任务队列是否有其它微任务,如果存在其他微任务则
  继续执行其他微任务直到微任务队列清空。
4.本次tick结束,进入下次循环重复上述三个步骤。

4.例子

console.log('script start')
setTimeout(() => {
    console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
    console.log('promise1')
}).then(() => {
    console.log('promise2')
})

console.log('script end')

运行结果

scriptstart  scriptend  promise1  promise2  setTimeout

运行过程

1.开始执行第一个宏任务Run script:
    宏任务栈(Macrotasks): Run script
    微任务栈(Microtasks):
    执行栈(JS stack): script
    输出(log): scriptstart
2.添加steTimeout callback到宏任务栈:
    Macrotasks: Runscript,setTimeoutCallback
    Microtasks:
    JS stack: script
    log: scriptstart
3.添加Promise then到微任务栈:
    Macrotasks:Run script,setTimeouCallback
    Microtasks:Promise then
    JS stack:script
    log: scriptstart,scriptend
4.第一个宏任务执行完毕,执行栈清空:
    Macrotasks:Run script,setTimeouCallback
    Microtasks:Promise then
    JS stack: 
    log:scriptstart,scriptend
5.开始执行微任务,执行过程中遇见第二个微任务Promise then
  将其添加到微任务栈:
    Macrotasks:Run script, setTimeoutCallback
    Microtasks:Promise then, Promise then
    JS stack:PromiseCallback
    log:scriptstart,scriptend,promise1
6.第一个微任务执行完毕,检测微任务栈是否清空,
  若未清空继续执行:
    Macrotasks:Runscript,setTimeoutCallback
    Microtasks:Promise then
    JS stack:PromiseCallback
    log: scriptstart,scriptend,promise1,promise2
7.微任务栈清空,本次事件循环结束,进入下次事件循环,
  开始执行宏任务setTimeouCallback:
    Macrotasks:setTimeoutCallback
    Microtasks:
    JS stack:setTimeouCallback
    log:scriptstart,scriptend,promise1,promise2,setTimeout

总结

1.由上述过程可以看出js是优先处理微任务的,而定义的宏任务则是会在下一次Tick才执行。所以建议优先级较高的代码使用 微任务。

2.js是一门单线程语言所有的异步事件都要放入事件队列里,等待执行栈安装事件循环来读取执行。