【js进阶5】事件循环

JavaScript | 2020-07-27 09:37:30 919次 4次

一、浏览器事件循环

浏览器中的 js 执行是单线程,但是比如我们发送的一个 ajax 请求为什么可以异步执行?因为浏览器中的事件循环机制,可以一边执行同步任务,一边处理异步任务。

同步任务进入主线程,异步的进入 Event Table 并注册回调函数,异步逻辑执行完将回调函数移入 Event Queue 队列。

主线程内的任务执行完毕为空(会持续不断的检查主线程执行栈是否为空)就会去 Event Queue 读取对应的函数,放进主线程执行。

这个不断重复的过程就被称为 Event Loop (事件循环)。

继续盗图:

1645bc78ff90482b.png

大致的了解什么是事件循环,并且知道异步会被进入一个异步事件注册回调,但是 js 中还有微任务的概念。

macro-task(宏任务):setTimeoutsetIntervalsetImmediate、全部代码、 I/O 操作、UI 渲染等

micro-task(微任务):  process.nextTickPromiseMutationObserver(html5 新特性) 等

那么这个过程中微任务和宏任务的运行和事件循环有什么关系呢,继续盗图:

15fdcea13361a1ec.png

这张图里面有一个重要的点(隐含信息),通过一段代码来看下或许能够理解到:

let timer1 = setTimeout(()=>{
    console.log('1')
    Promise.resolve().then(()=>{
        console.log('1-1')
        Promise.resolve().then(()=>{
            console.log('1-1-1')
        })
    })
    Promise.resolve().then(()=>{
	console.log('1-2')
	let timer3 = setTimeout(()=>{
            console.log('1-2-1')
             Promise.resolve().then(()=>{
                console.log('1-2-2')
            })
        }, 0)
    })
}, 0)

let timer2 = setTimeout(()=>{
    console.log('2')
    Promise.resolve().then(()=>{
        console.log('2-1')
    })
}, 0)

Promise.resolve().then(()=>{
    console.log('3')
    Promise.resolve().then(()=>{
        console.log('3-1')
    })
})

//结果 3、3-1、1、1-1、 1-2、1-1-1、2、2-1、1-2-1、1-2-2

在第一个定时器中将所有的微任务执行完才会进行第二个 timer2 的执行,同时 timer1 中又注册了一个 timer3 宏任务,最后再会被执行。所以我们可以得出一个结论:宏任务队列可以有多个,微任务队列只有一个。所以上图中标注的是有可执行的微任务并且执行所有

补充:js 或者 node 中的定时器并不是严格的到点就执行,只是到点会把任务放进 Event Queue,具体执不执行这个回调要看主线程有没有空闲(没有正在处理的任务了),比如通过耗时的 while 循环等操作,会影响定时器回调的延迟执行,所以不要相信定时器。


二、Nodejs 事件循环

Nodejs 中的事件循环要比浏览器复杂,原理也不一样。

Nodejs 是基于 V8 引擎构建的,模型与浏览器类似。js 是单线程的,但是从严格意义上来讲 Nodejs 并不是单线程架构,因为他有 I/O 线程,定时器线程等等,只不过这些都是由更底层的 libuv 处理,libuv 将执行结果放入到队列中等待执行,这个过程就是 node 中的事件循环。

┌─>  timers       // 这个阶段执行通过 setTimeout 和 setInterval 设置的回调函数,并且是由 poll 阶段控制的
│       |
│     I/O callbacks // 处理一些上一轮循环中的少数未执行的 I/O 回调
│       |
│     idle, prepare // node 内部 libuv 调用
│       |
│      poll         // 获取新的 I/O 事件,适当的条件下 node 将阻塞在这里
│       |
│      check        // 此阶段调用 setImmadiate 设置的回调         
│       |
└─ close callbacks // 一些关闭回调,比如 socket.on('close',...)

1.timer

timers 阶段会执行 setTimeout setInterval 回调,并且是由 poll 阶段控制的

2.poll

进入该阶段时如果没有设定 timer

    1)如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制

    2)如果 poll 队列为空,会有两件事发生

        如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调

        如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去

进入该阶段时有设定 timer

    1如果 poll 队列为空

        判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调


对于  setTimeout setImmediate 的执行先后顺序,在异步 i/o callback 内部调用时,总是先执行 setImmediate,再执行 setTimeout:

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})
// immediate
// timeout

process.nextTick:

这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行:

setTimeout(() => {
 console.log('timer1')
 Promise.resolve().then(function() {
   console.log('promise1')
 })}, 0)

process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

其中对于宏任务微任务的处理也不一样:

浏览器环境:microtask 的任务队列是每个 macrotask 执行完之后执行

Node 环境:microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务

注意在高版本的 node 中这种差异没有了,向浏览器看齐。

4人赞

分享到: