从浏览器到事件循环
大家好!这篇是我重新开始写文的第2篇文章。此次主要是总结以下我之前关于事件循环的笔记,希望大家看到这篇文章有所不一样的收获吧。少说废话,开始正文。
浏览器进程与渲染线程
浏览器进程
首先从浏览器进程开始吧。我们都知道,浏览器的进程分为五个。其中有三个进程每个浏览器只会存在一个,而剩余的两个则是可以多个(下面进程后为进程的数量)。
- 浏览器主进程(1):页面显示,用户交互,子进程管理,存储功能等
- GPU 进程(1):初始目的用于3D CSS渲染,之后用于UI界面绘制
- 网络进程(1):网络资源加载
- 插件进程(n):负责插件运行,一个插件启动一个
- 渲染进程(n):将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,每一个页面都有一个渲染进程
这里我们需要着重关注渲染进程,这个进程关系到浏览器的JavaScript执行和浏览器渲染。
渲染进程
渲染进程分五个线程
- GUI渲染线程(互斥)
- 负责页面绘制
- 回流或者重绘时会触发
- JS引擎线程(互斥)
- 负责JavaScript脚本解析和执行
- GUI渲染线程与JS引擎线程互斥,JS引擎线程优先
- 事件触发线程
- 控制事件循环,将事件放到对应的处理线程中
- DOM事件
- 定时器触发
- 异步http请求
- ...
- 事件触发时,将注册的事件回调放至事件队列对尾,等待执行
- 控制事件循环,将事件放到对应的处理线程中
- 定时器触发线程
- 控制setInterval与setTimeout的时间计数
- 由于单线程,阻塞线程状态就会影响计时的准确
- 当计数到达设定值时,通知事件触发线程
- 异步http请求线程
- 处理XMLHttpRequest请求
- 当状态变更时,通知事件触发线程
了解了这个我们可以知道以下几点
- JS引擎线程和GUI渲染线程互斥,导致JavaScript执行会阻塞渲染
- 定时器触发线程和异步http请求线程负责事件回调的触发,并把触发的回调事件通过事件触发线程推送至事件队列等待事件循环处理
事件循环
说到事件循环,我非常推荐大家观看一个关于它的讲座视频,这个视频通俗易懂的讲到了事件循环过程,和RAF与普通回调事件触发的区别。非常大家看后再看后续的文章复习!!!
首先由于线程单一,导致JavaScript只能在同一时间做一件事(当然后续特性webworker支持新建线程处理JavaScript)。从而当遭遇异步事件时,浏览器都是会将其交由对应线程处理。而相关事件分为宏任务和微任务
宏任务和微任务
这个知识点,我相信大家都知道吧。以下我只列出,不详细说明。当然Node我只是列出。后续不会介绍Node的事件循环(绝不是我我没看)
宏任务(Macrotask)
- 浏览器中的
- I/O 操作
- setTimeout
- setInterval
- RAF(但是有争议,我觉得不是,原因见文章后续)
- Node中的
- I/O 操作
- setTimeout
- setInterval
- setImmediate
微任务(Microtask)
- 浏览器中的
- Promise
- MutationObserver
- Node中的
- Promise
- process.nextTick
浏览器事件循环
终于来到核心的地方了,接下来就是浏览器的事件循环了
事件循环结构
首先介绍浏览器事件循环的结构
- 执行栈(Call Stack)
- 负责JavaScript的执行
- 遵循先进后出
- 存放具有限制,过多会爆栈
- 事件队列(Task Queue)
- 负责宏任务回调事件处理
- 微任务队列(Microtask Queue)
- 负责微任务队列的回调处理
- 渲染队列(Animation callback)
- 负责requestAnimationFrame的回调处理
队列的执行过程
- 事件队列(Task Queue)
- 一次只执行一个事件
- 当有新事件加入时并不会立即执行
- 微任务队列(Microtask Queue)
- 一直执行,直到队列清空
- 当执行过程中有新事件加入时,也会直接执行
- 由于微任务处理是同步的,过多的任务会阻塞渲染
- 渲染队列(Animation callback)
- requestAnimationFrame的回调处理,本次渲染前执行(有的会在之后执行)
- 一直执行,直到当前帧队列清空
- 当有新事件加入时,只会将当前帧的处理完,执行过程中新加入的下次帧时执行
从而此处再提出之前的问题RAF是宏任务吗?,我觉得不是有以下几个原因
- 宏任务与RAF任务有明显的差异
- 执行时机不同,RAF在浏览器重新渲染前
- RAF任务队列被执行时,会将其此刻队列中所有任务都执行完
- 所以RAF任务不属于宏任务,而由于微任务的特殊性(单独的任务队列),它显然更不是微任务
当然各位有异议可以评论区指出。
事件循环的执行过程
既然已经知道了队列和执行栈,此时执行过程就大致总结下吧
- 取事件队列中最老的Task,无Task则直接到第六步
- 将Task设为事件循环中正在运行的Task,并推入执行栈
- 执行Task
- Task执行完毕,将事件循环中正在运行的Task设置为null
- 事件队列移除Task
- 清空微任务队列
- 页面UI渲染,此时RAF会进行处理
- 不是每次循环都会渲染
- 多次变动不会立即响应,会积攒变动以60HZ频率更新
- 任务队列中没有Task,并且符合条件时中止步骤,否则返回第一步
来个小练习
最后给大家一个视频中的小练习吧
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('p1'))
console.log('l1')
})
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('p2'))
console.log('l2')
})
// 鼠标点击
// l1 p1 l2 p2
// button.click()触发
// l1 l2 p1 p2
造成button.click触发的差异,主要是当第一个注册的事件执行完成时,执行栈中还存在button.click()这一事件。从而微任务由于栈不空则无法执行。
总结
拖拖拉拉这第二篇总算水出来了,主要还是总结没什么亮眼的部分。水文这件事还是要坚持,我安排写作的方式还是要调节下(毕竟笔记的存货不多了)。有错误希望大家指出来,毕竟人无完人,共同进步吧。最后,谢谢大家的阅读!!!