V8运行时环境中的事件循环机制
事件循环(event loop)机制的背景
由于JavaScript的单线程编程语言的机制, 所以在同一时间内只会执行一个任务 那如果JS允许多线程操作DOM的话, 会让多个线程同时修改DOM会导致不可预测的状态冲突(竟态条件)
所以,JS 就引入了异步编程模型, 事件循环机制就是该模型的核心
浏览器中是怎么运行事件循环机制的?
在Goodle浏览器中, V8引擎提供的JavaScript运行时环境中, 包含以下关键模块:
调用栈 : V8引擎内部
Web API 线程池、任务队列、事件循环:浏览器(Chrome 渲染引擎)
V8运行时
V8引擎/Nodejs 等JS 运行时环境,会负责
- 解析、编译、执行 JS 代码
- 维护调用栈,执行上下文(main()). 内存管理
V8引擎/Nodejs 等JS 运行时环境,本身不具备网络请求、定时器、DOM 操作的能力 这些都不是 JS 语法的一部分,而是浏览器提供的 Web API。 也就是说,JS代码唯一真正执行的地方其实是这些V8引擎和Nodejs提供的运行时环境中!!!
在V8引擎中,所有的JS代码都要通过调用栈(Callback Stack) 进行任务的出栈进栈的操作
调用栈的关键作用在于:
- 所有同步代码必须进栈才能执行
- 同一时间只能执行一个任务(JS 单线程的根源)
- 栈空 = JS 引擎空闲
- 栈不空 → 永远不会执行任何异步回调
浏览器中的 Web API 线程池
Web API 线程池会在 Chrome 浏览器渲染进程中,由 Blink 引擎管理
Web API 包含很多线程:
- 网络线程(AJAX/fetch)
- 定时器线程(setTimeout/setInterval)
- DOM 事件线程(click/scroll)
- 文件 I/O 线程
- 渲染、合成、栅格线程…
由此可见它的核心职责就是:
- 处理所有异步、耗时操作
- 多线程并行执行,不阻塞 JS 主线程
- 任务完成后,把回调函数丢进任务队列
Web API线程池的存在让JS单线程不会被阻塞 用户发送的AJAX,定时器,点击事件, 全是浏览器在进行处理, 不是JS在进行处理
浏览器中的任务队列(Task Queue)
任务队列在浏览器内部由Blink 渲染引擎维护
任务队列分为两种:
- 宏任务队列(Macrotask Queue) setTimeout、AJAX、DOM 事件、I/O、fetch
- 微任务队列(Microtask Queue) Promise.then、queueMicrotask、MutationObserver
他的核心职责就是:
- 存放异步回调函数,排队等待执行
- 先进先出 FIFO
关键作用就是缓存异步结果的回调函数, 不让它们打断同步代码
JS异步模型的主角----事件循环(Event Loop)
同样的, 事件循环机制依旧是由浏览器(Blink)引擎进行调度
事件循环(Event Loop)的核心职责就是: 不断监听调用栈是否为空
执行流程为:
- 看调用栈是否空
- 如果不空:什么都不做,等待
- 如果空: 先把微任务队列全部清空 再取一个宏任务放到调用栈执行
- 无限循环重复操作
它的作用不必多说,伟大,无需多言
连接 V8 调用栈 和 浏览器异步系统 的桥梁。 没有事件循环(Event Loop), 异步回调永远都不会执行
事件循环中的实例
判断输出顺序:
console.log(1);setTimeout(() => console.log(2), 0);Promise.resolve().then(() => console.log(3));console.log(4);输出: 1 4 3 2 (从上到下)
详细分析一下这个过程:
- 在调用栈中推入执行上下文(main()),并依次执行同步代码:
console.log(1);
-
当调用栈执行宏任务定时器等 Web API 时, 浏览器将它们放入到Web API线程池中,等待操作产生回调函数:
setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3));
-
产生的回调会根据任务类型(宏任务/微任务)缓存到各自的任务队列当中
-
关键一步, event loop会监听调用栈是否有未执行的同步代码,随后就会将任务队列的回调推入到调用栈中, 先清空微队列任务,后处理宏任务队列
当调用栈的所有代码都执行完毕后, 事件循环就会再去执行
事件循环机制带来的思考
首先, 同步代码会带来阻塞,异步代码不会进行阻塞 同步函数执行完才能执行下一句
异步函数中通常接入一个回调作为参数, 在调用异步函数后,会立即继续执行下一行 并且回调函数仅在异步操纵完成且调用栈为空的时候调用.
例如
还有一个场景就很好玩:
一般浏览器会在60帧每秒去渲染页面中 根据事件循环机制, 浏览器也不会在调用栈还有同步代码的时候去渲染你的页面
例如我们将它放入异步函数的回调中: delay()
为什么? 因为渲染调用的本身几乎就是一个回调过程 渲染相较于回调的区别就在于, 渲染的优先级比回调更高
假如我们在栈中执行了一个该死的循环delay()函数的话, 这个js代码在对堆栈中会执行,那么就会阻塞事件循环 就会导致页面ui渲染很慢很烂
总之, 我们应该避免去写这种 shity code 去阻塞我们的执行程序
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时
.webp)
.webp)
.webp)
.webp)
.webp)
.webp)
.webp)
.webp)
.webp)
.webp)
.webp)
.webp)
