秋招面试被问到这个问题,查了很多文章,比较零散,所以自己集百家之长,做个整理
什么是事件?
事件:事件就是由于某种外在或内在的信息状态发生的变化,从而导致出现了对应的反应。比如说用户点击了一个按钮,就是一个事件;HTML页面完成加载,也是一个事件。一个事件中会包含多个任务。
JavaScript引擎又称为JavaScript解释器,是JavaScript解释为机器码的工具,分别运行在浏览器和Node中。而根据上下文的不同,Event loop也有不同的实现:其中Node使用了libuv库来实现Event loop; 而在浏览器中,html规范定义了Event loop,具体的实现则交给不同的厂商去完成。
所以,浏览器的Event loop和Node的Event loop是两个概念,下面分别来看一下。
在JavaScript中,任务被分为MacroTask(宏任务)和MicroTask(微任务)两种。它们分别包含以下内容:
MacroTask: script(整体代码), setTimeout, setInterval, setImmediate(node独有), I/O, UI rendering
MicroTask: process.nextTick(node独有), Promises, MutationObserver
由于js是单线程语言,只有一个主线程来处理所以任务,首次script压入执行栈,同步代码立即执行,异步代码放入事件队列,宏任务放入宏任务队列,微任务放入微任务队列。当当前执行栈空了会立刻处理所有的微任务,然后再去宏任务队列中取出一个事件,再执行所有的微任务…
注意:浏览器中,一个事件循环里有很多个来自不同任务源的任务队列(task queues),每一个任务队列里的任务是严格按照先进先出的顺序执行的。但是,因为浏览器自己调度的关系,不同任务队列的任务的执行顺序是不确定的。
nodejs的event loop分为6个阶段,它们会按照顺序反复运行,分别如下:
如图所示是Node 11之前的事件循环机制:不同于浏览器的是,在每个阶段完成后,而不是MacroTask任务完成后,microTask队列就会被执行。
注意:这个图是node11.0之前的机制,例如timers里面有两个settimeout,则两个settimeout都执行完之后,再执行微任务,但是node11.0之后,node修改了执行机制,和浏览器一样,执行完一个宏任务之后立刻执行所有微任务,例如下面示例3.1所示。
关于阶段,我从文档中摘录了比较重要的一段话:
轮询
轮询 阶段有两个重要的功能:
计算应该阻塞和轮询 I/O 的时间。
然后,处理 轮询 队列里的事件。
当事件循环进入 轮询 阶段且 没有被调度的计时器时 ,将发生以下两种情况之一:
如果 轮询 队列 不是空的 ,事件循环将循环访问回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬性限制。
如果 轮询 队列 是空的 ,还有两件事发生:
如果脚本被 setImmediate() 调度,则事件循环将结束 轮询 阶段,并继续 检查 阶段以执行那些被调度的脚本。
如果脚本 未被 setImmediate()调度,则事件循环将等待回调被添加到队列中,然后立即执行。
一旦 轮询 队列为空,事件循环将检查 已达到时间阈值的计时器。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。
检查阶段
此阶段允许人员在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲状态,并且脚本使用 setImmediate() 后被排列在队列中,则事件循环可能继续到 检查 阶段而不是等待。
setImmediate() 实际上是一个在事件循环的单独阶段运行的特殊计时器。它使用一个 libuv API 来安排回调在 轮询 阶段完成后执行。
通常,在执行代码时,事件循环最终会命中轮询阶段,在那等待传入连接、请求等。但是,如果回调已使用 setImmediate()调度过,并且轮询阶段变为空闲状态,则它将结束此阶段,并继续到检查阶段而不是继续等待轮询事件。
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
//浏览器和Node输出均为:
timer1
promise1
timer2
promise2
setImmediate(() => {
console.log('timer1')
Promise.resolve().then(function () {
console.log('promise1')
})
})
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function () {
console.log('promise2')
})
}, 0)
//Node输出:
timer1 timer2
promise1 或者 promise2
timer2 timer1
promise2 promise1
按理说setTimeout(fn,0)应该比setImmediate(fn)快,应该只有第二种结果,为什么会出现两种结果呢?
这是因为Node 做不到0毫秒,最少也需要4毫秒。实际执行的时候,进入事件循环以后,有可能到了4毫秒,也可能还没到4毫秒,取决于系统当时的状况。如果没到4毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。
官网解释:if we run the following script which is not within an I/O cycle , the order in which the two timers are executed is non-deterministic, as it is bound by the performance of the process
但是,如果把settimeout和setimmediate放入I/O循环,那么setImmediate会比setTimeout更快,例如:
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
$ node timeout_vs_immediate.js
immediate
timeout
先上代码,我亲自试的,果然百闻不如一试
setTimeout(() => {
console.log('我是settimeout1')
var p = new Promise((resolve) => {
console.log('我是promise1')
resolve()
})
p.then(() => {
console.log('我是promise1成功的回调')
})
process.nextTick(() => {
console.log('我是process1')
})
}, 0)
setTimeout(() => {
console.log('我是settimeout2')
process.nextTick(() => {
console.log('我是process2')
})
var p = new Promise((resolve) => {
console.log('我是promise2')
resolve()
})
p.then(() => {
console.log('我是promise2成功的回调')
})
}, 0)
//node v12结果:
我是settimeout1
我是promise1
我是promise1成功的回调
我是settimeout2
我是promise2
我是promise2成功的回调
我是process1
我是process2
Node文档中说明,任何阶段都可以调用process.nextTick,它的回调将在事件循环继续之前解析(也就是执行完这一阶段所以宏任务,腰进入下一阶段之前执行),但它可能会造成一些糟糕的情况,因为它允许您通过递归 process.nextTick()调用来“饿死”您的 I/O,阻止事件循环到达poll阶段(process.nextTick递归调用的process.nextTick会立即放到微任务队列,并立即执行,也就是死循环,这点与setimmediate不同,递归调用的setimmediate会被放入下一次事件循环中)。
进程是cpu资源分配的最小单位(系统会给它分配内存)
线程是cpu调度的最小单位(一个进程中可以有多个线程)
js是一门单线程语言,JS可以操作DOM,如果JS同时有两个线程,一个删除dom,一个添加dom,会出问题,所以规定js是一门单线程的语言。
但是浏览器是多进程的
GUI渲染线程和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,当GUI 渲染线程在工作的时候,js线程会被挂起
也就是在数据变化后,DOM并不会马上更新,而是在本轮事件循环结束后才执行更新。如果如果想要根据更新后 DOM 状态去做某些操作,就可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。