JavaScript | 2020-07-27 09:37:30 1372次 4次
一、浏览器事件循环
浏览器中的 js 执行是单线程,但是比如我们发送的一个 ajax 请求为什么可以异步执行?因为浏览器中的事件循环机制,可以一边执行同步任务,一边处理异步任务。
同步任务进入主线程,异步的进入 Event Table 并注册回调函数,异步逻辑执行完将回调函数移入 Event Queue 队列。
主线程内的任务执行完毕为空(会持续不断的检查主线程执行栈是否为空)就会去 Event Queue 读取对应的函数,放进主线程执行。
这个不断重复的过程就被称为 Event Loop (事件循环)。
继续盗图:
大致的了解什么是事件循环,并且知道异步会被进入一个异步事件注册回调,但是 js 中还有微任务的概念。
macro-task(宏任务):setTimeout、setInterval、setImmediate、全部代码、 I/O 操作、UI 渲染等
micro-task(微任务): process.nextTick、Promise、MutationObserver(html5 新特性) 等
那么这个过程中微任务和宏任务的运行和事件循环有什么关系呢,继续盗图:
这张图里面有一个重要的点(隐含信息),通过一段代码来看下或许能够理解到:
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人赞