文章目录
浏览器EventLoop
浏览器事件循环——概念:js是一门单线程非阻塞性的脚本语言,在同一时刻只有一个代码块在运行,但是对于一些异步请求或者setTimeout等任务,如果代码执行到这里,等待执行结束,阻塞情况下,浏览器就会“假死”,所以浏览器就是通过事件循环机制去实现异步任务,异步任务可以分为宏任务(setTimeout/setInterval
I/O、UI 渲染、postMessage、requestAnimationFrame)和微任务(Promise().then()),
- resolve执行之后才会执行then()方法,但是resolve不会阻塞后面的同步任务执行
- 一个宏任务执行结束之后,会立马执行微任务,当没有微任务可以执行的时候,才会去继续执行下一个宏任务
- requestAnimationFrame是一个宏任务,他的执行时机是下一次重绘之前去执行,具体什么时候执行由浏览器决定
- async 的await执行完后
- 如果await后面是 promise对象会造成异步函数停止执行并且等待 promise 的解决,
- 如果await后面是 正常的表达式则立即执行。
- 对于一处于pending状态的Promise对象p,内部状态的resolve,会让p.then(fn)中的fn加入微任务队列
- 链式then方法,是按执行顺序加入微任务队列中
例如:
- 常见版本
async function async1() {
console.log('a');
await async2();
console.log('b');
}
async function async2() {
console.log('c')
}
console.log('d')
async1();
setTimeout(()=> {
console.log('e')
},0)
new Promise((resolve, reject) => {
console.log('f')
resolve()
}).then(()=> {
console.log('g')
})
结果:d a c f b g e
- 常见版本2
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
new Promise((resolve, reject) => {
console.log('promise测试 before');
resolve()
console.log('promise测试 after');
}).then(() => {
console.log('promise测试1');
}).then(() => {
console.log('promise测试2');
})
function fn1() {
console.log('fn1 function')
}
async function fn() {
console.log('fn start')
const res = await fn1()
console.log('fn end')
}
fn()
console.log('end');
start
promise测试before
promise测试 after
fn start
fn1 function
end
promise测试1
fn end
promise测试2
timeout
- await后面跟了带有返回Promise对象的函数
async function async1() {
await async2()
console.log('async1 end')
new Promise(resolve => {
console.log('await')
resolve()
})
.then(function() {
console.log('await then')
})
}
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=> {
console.log('async2 end1')
})
}
async1()
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end');
执行结果为:
同步任务:async2 end -> Promise -> script end
微任务:-> async2 end1 -> promise1 -> promise2 -> async1 end -> await -> await then
- await后面跟了没有返回值的函数
async function async1() {
await async2()
console.log('async1 end')
new Promise(resolve => {
console.log('await')
resolve()
})
.then(function() {
console.log('await then')
})
}
async function async2() {
console.log('async2 end')
}
async1()
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end');
执行结果为:
同步任务: async2 end -> Promise -> script end
微任务:-> async1 end -> await -> promise1 -> await then -> promise2
- await后跟了基本数据类型
async function async1() {
await 1
new Promise(resolve => {
console.log('await')
resolve()
})
.then(function() {
console.log('await then')
})
}
async1()
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end');
执行结果为:
同步任务:Promise -> script end
微任务: -> await -> promise1 -> await then -> promise2
Node
的EventLoop
- 每一个阶段都会维护一个事件队列。可以把每一个圈想象成一个事件队列。这就和浏览器不一样了,浏览器最多两个队列(宏队列、微队列)。
- 但是在node里边有六个队列到达一个队列后,检查队列内是否有任务(也就是看下是否有回调函数)需要执行。如果有,就依次执行,直到全部执行完毕、清空队列。如果没有任务,进入下一个队列去检查。
- 直到所有队列检查一遍,算一个轮询。
- 其中,timers、pending callback、idle prepare等执行完毕后,到达poll队列
每个阶段:
- timers阶段:处理setTimeout()和setInterval()等定时器事件。
- I/O callbacks阶段:处理几乎所有的异步I/O回调,例如网络I/O、文件I/O等。
- idle, prepare阶段:这是Node.js内部使用的,开发者很少会用到。
- poll阶段:等待新的I/O事件,处理已经完成的事件回调。
- check阶段:处理setImmediate()的回调函数。
- close callbacks阶段:处理一些关闭事件,例如socket关闭等。
- timer: 执行如setTimeout和setInterval等的回调函数
- Poll轮询阶段:这是一个至关重要的阶段,系统主要做两件事,一是回到timer阶段执行回调,二是执行I/O回调。会主动检测是否有新的I/O事件,若存在新的I/O事件,则执行其回调函数,适当的条件下,node将阻塞在这里。
- check:执行setImmediate的回调函数
setImmediate() 与 setTimeout(0) 的对比
- setImmediate和setTimeout的回调是异步的。
- setImmediate回调在check队列,setTimeout回调在timers队列(概念意义,实际在计时器线程,只是setTimeout在timers队列做检查调用而已。)。
- setImmediate函数调用后,回调函数会立即push到check队列,并在下次eventloop时被执行。
- setTimeout函数调用后,计时器线程增加一个定时器任务,下次eventloop时会在timers阶段里检查判断定时器任务是否到达时间,到了则执行回调函数。
- 综上,setImmediate的运算速度比setTimeout(0)的要快,因为setTimeout还需要开计时器线程,并增加计算的开销。二者的效果差不多。但是执行顺序不定。
浏览器和node环境的事件循环区别
相同点:实现原理类似,都是基于事件驱动设计
不同点:
-
执行环境:浏览器的Event Loop运行在浏览器环境中,而Node.js 使用 libuv 库来实现事件循环,它的Event Loop运行在Node.js环境中。浏览器的Event Loop负责处理浏览器事件、用户交互和渲染等,而Node.js的Event Loop主要处理I/O操作和网络请求等。
-
宏任务和微任务:浏览器和Node.js都将任务分为宏任务(Macro Task)和微任务(Micro Task),但它们在微任务的处理上有所不同。在浏览器中,微任务包括Promise、MutationObserver和queueMicrotask等,而在Node.js中,微任务包括Promise和process.nextTick等。
-
触发时机:浏览器的Event Loop通常在每次完成宏任务后执行微任务队列,然后执行浏览器渲染,从而实现动画效果。而Node.js的Event Loop在每次完成一个宏任务后,会先执行微任务队列,然后继续执行下一个宏任务。
-
Event Loop的运行机制:浏览器的Event Loop通常是单线程的,通过异步回调函数和事件触发来实现非阻塞的异步操作。而Node.js的Event Loop是基于Libuv库实现的,它利用了底层操作系统提供的多线程特性,使得Node.js能够处理更高的并发请求。
从浏览器线程和进程角度下的EventLoop
进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位)
浏览器主要进程:
- Browser进程(主进程):控制chrome的地址栏,书签栏,返回和前进按钮,同时还有浏览器的不可见部分,例如网络请求和文件访问
- 第三方插件进程:每种插件一个进程,插件运行时才会创建
- GPU进程:仅此一个,用于3D绘制等
- 浏览器渲染进程:负责界面渲染,脚本执行,事件处理等
浏览器渲染进程的主要线程:
- GUI渲染线程
- 负责渲染浏览器界面(解析 HTML ,CSS,构建 DOM树 CSSOM树 和 Render树 ,布局和绘制等)。
- GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行,当界面需要重绘或由于某种操作引发的重排时,该线程就会执行。
【GUI 渲染线程与 JS 引擎线程是互斥的】,这也是造成 JS堵塞 的原因。(线程互斥原因:JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JS 引擎线程和 GUI 渲染线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。)
- JS引擎线程(V8引擎)
- 一个Tab页中无论什么时候都只有一个JS线程在运行JS程序(因为JS是一门单线程的语言)
- 事件触发线程
- 这属于浏览器而不是JS引擎,主要用来控制事件循环
- 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中。
当对应的事件符合触发条件被触发时,该线程会把是事件添加到待处理队列(宏任务)的队尾,等待JS引擎的处理。
- 定时触发器线程
- setInterval 与 setTimeout 所在线程
- JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确。
因此通过单独线程来计时并触发定时,计时完毕后,添加到事件队列(宏任务)中,等待JS引擎空闲后执行。
- 异步http请求线程
- XMLHttpRequest 在连接后是通过浏览器新开的一个线程请求
- 当检测到状态更新时,如果没有设置回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列(微任务)中,等待 JS 引擎执行
- Web Worker
- 它允许一段 JavaScript 程序运行在主线程之外的另外一个线程中
- 将一些任务分配给后者运行,等到 Worker 线程完成计算任务,再把结果返回给主线程