事件循环和宏任务(Node环境和浏览器)

浏览器EventLoop

浏览器事件循环——概念:js是一门单线程非阻塞性的脚本语言,在同一时刻只有一个代码块在运行,但是对于一些异步请求或者setTimeout等任务,如果代码执行到这里,等待执行结束,阻塞情况下,浏览器就会“假死”,所以浏览器就是通过事件循环机制去实现异步任务,异步任务可以分为宏任务(setTimeout/setInterval
I/O、UI 渲染、postMessage、requestAnimationFrame)和微任务(Promise().then()),

  1. resolve执行之后才会执行then()方法,但是resolve不会阻塞后面的同步任务执行
  2. 一个宏任务执行结束之后,会立马执行微任务,当没有微任务可以执行的时候,才会去继续执行下一个宏任务
  3. requestAnimationFrame是一个宏任务,他的执行时机是下一次重绘之前去执行,具体什么时候执行由浏览器决定
  4. async 的await执行完后
    • 如果await后面是 promise对象会造成异步函数停止执行并且等待 promise 的解决,
    • 如果await后面是 正常的表达式则立即执行。
    • 对于一处于pending状态的Promise对象p,内部状态的resolve,会让p.then(fn)中的fn加入微任务队列
  5. 链式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

NodeEventLoop

  • 每一个阶段都会维护一个事件队列。可以把每一个圈想象成一个事件队列。这就和浏览器不一样了,浏览器最多两个队列(宏队列、微队列)。
  • 但是在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环境的事件循环区别

相同点:实现原理类似,都是基于事件驱动设计
不同点:

  1. 执行环境:浏览器的Event Loop运行在浏览器环境中,而Node.js 使用 libuv 库来实现事件循环,它的Event Loop运行在Node.js环境中。浏览器的Event Loop负责处理浏览器事件、用户交互和渲染等,而Node.js的Event Loop主要处理I/O操作和网络请求等。

  2. 宏任务和微任务:浏览器和Node.js都将任务分为宏任务(Macro Task)和微任务(Micro Task),但它们在微任务的处理上有所不同。在浏览器中,微任务包括Promise、MutationObserver和queueMicrotask等,而在Node.js中,微任务包括Promise和process.nextTick等。

  3. 触发时机:浏览器的Event Loop通常在每次完成宏任务后执行微任务队列,然后执行浏览器渲染,从而实现动画效果。而Node.js的Event Loop在每次完成一个宏任务后,会先执行微任务队列,然后继续执行下一个宏任务。

  4. Event Loop的运行机制:浏览器的Event Loop通常是单线程的,通过异步回调函数和事件触发来实现非阻塞的异步操作。而Node.js的Event Loop是基于Libuv库实现的,它利用了底层操作系统提供的多线程特性,使得Node.js能够处理更高的并发请求。

从浏览器线程和进程角度下的EventLoop

进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位)

浏览器主要进程:

  1. Browser进程(主进程):控制chrome的地址栏,书签栏,返回和前进按钮,同时还有浏览器的不可见部分,例如网络请求和文件访问
  2. 第三方插件进程:每种插件一个进程,插件运行时才会创建
  3. GPU进程:仅此一个,用于3D绘制等
  4. 浏览器渲染进程:负责界面渲染,脚本执行,事件处理等

浏览器渲染进程的主要线程:

  1. GUI渲染线程
    • 负责渲染浏览器界面(解析 HTML ,CSS,构建 DOM树 CSSOM树 和 Render树 ,布局和绘制等)。
    • GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行,当界面需要重绘或由于某种操作引发的重排时,该线程就会执行。
      【GUI 渲染线程与 JS 引擎线程是互斥的】,这也是造成 JS堵塞 的原因。(线程互斥原因:JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JS 引擎线程和 GUI 渲染线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。)
  2. JS引擎线程(V8引擎)
    • 一个Tab页中无论什么时候都只有一个JS线程在运行JS程序(因为JS是一门单线程的语言)
  3. 事件触发线程
    • 这属于浏览器而不是JS引擎,主要用来控制事件循环
    • 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中。
      当对应的事件符合触发条件被触发时,该线程会把是事件添加到待处理队列(宏任务)的队尾,等待JS引擎的处理。
  4. 定时触发器线程
    • setIntervalsetTimeout 所在线程
    • JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确。
      因此通过单独线程来计时并触发定时,计时完毕后,添加到事件队列(宏任务)中,等待JS引擎空闲后执行。
  5. 异步http请求线程
    • XMLHttpRequest 在连接后是通过浏览器新开的一个线程请求
    • 当检测到状态更新时,如果没有设置回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列(微任务)中,等待 JS 引擎执行
  6. Web Worker
    • 它允许一段 JavaScript 程序运行在主线程之外的另外一个线程中
    • 将一些任务分配给后者运行,等到 Worker 线程完成计算任务,再把结果返回给主线程
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值