事由起因,咋们先来了解下浏览器的进程与线程
- 首先,浏览器是多进程,如:
- 浏览器主进程
- 第三方插件进程
- GPU进程
- tab页进程
- 一个进程中,可以存在一个或多个线程
浏览器进程
- 在浏览器中每开启一个tab页,就开启了对应tab页进程,每个页面互不影响
- 一个页面相当于一个进程,一个进程有多个线程互相配合
浏览器tab页中的进程主要由以下线程构成
主线程(JS引擎线程)
- 浏览器存在很多线程(事件触发线程互相操作)
GUI渲染线程(常驻线程)
- 渲染浏览器界面,解析html,css,构建dom树,布局和绘制
- 该线程与JS线程互斥,当js引擎执行时,该引擎会被挂起,会保存在一个队列中等到JS引擎空闲时立即执行
JS引擎线程(常驻线程)(JS内核)
- 负责处理JavaScript脚本程序(例如V8引擎),解析JavaScript脚本,运行代码
- 一个页面中只有一个JS线程在运行JS程序
- 如果JS引擎运行时间过长,会造成阻塞页面加载,影响GUI渲染线程页面加载
- 当JS引擎修改DOM的时候,如果同时运行GUI引擎,会造成页面数据不一致
- 因此GUI更新会放到任务队列中,等JS引擎空闲时立即执行
浏览器事件线程(onclick)(常驻线程)
- 该事件归属于浏览器,用来控制事件循环
- JS引擎遇到事件时(不是首先同步处理的事件),DomEvent事件等,会将这些任务添加到该事件线程中
- 当事件符合触发条件被触发时(如点击,移动等操作),该线程会把事件添加到任务队列末尾,等待JS引擎的处理
- 由于JS时单线程,所以任务队列需要等待JS引擎依次处理
- 定时器触发线程(setTimeout和setInterval)
- 定时器不是由JS引擎计数的,因为JS是单线程,如果处于阻塞状态就会影响计时的准确
- 所以当计时完毕后,会添加到任务队列末尾,等待JS引擎执行
- 注意!!!
- 如果JS引擎处理其他任务超过定时器时间
- 如定时器5000ms,比如循环事件用了6000ms,就算定时器线程在5000ms的时候把执行函数放入到任务事件队列末尾,也必须等循环结束后才能调用,这就造成实际是6000ms的时候才触发定时器
异步http请求线程
- 当XMLHttpRequest连接后,浏览器会新开一个线程请求
- 当检测到状态变更时(onreadystateChange = function () {})
- 如果设有回调函数,该异步线程就产生状态变更事件,将回调函数放入事件队列末尾,再由JS引擎执行
事件循环(Event Loop)
Event loop事件轮询处理
- 先来了解下,js引擎是怎样的运行方式
栈
- 后进先出,先进后出
- 数据存储只能从顶部逐个存入,取出时也需从顶部逐个取出
堆
- 无序的key-value(键值对)存储方式
队列
- 先进先出
- 数据存储从队尾插入,从队头取出
事件执行
- JS引擎先处理同步代码(宏任务),微任务和定时器等异步代码放入对应线程中等待触发
- 当微任务和异步代码符合触发条件,将会把回调函数放入任务队列末尾等待JS引擎执行
- 当用户操作event事件触发时,事件触发线程会将对应事件函数放入任务队列末尾
任务优先级
graph LR
id1(队列任务优先级) ==> id2(宏任务) == 同步任务 -->id3(微任务) == 异步任务--> id4(异步定时器任务)
style id1 fill:#ccf,stroke-width:4px;
style id2 fill:#ccf,stroke-width:4px;
style id3 fill:#ccf,stroke-width:4px;
style id4 fill:#ccf,stroke-width:4px;
任务执行线路
graph TB
开始执行队列任务...
--> 宏任务== 微任务 ==>
GUI渲染引擎线程 == ......
--> 宏任务2 == 微任务 ==>
GUI渲染引擎线程2
-- Event Loop --> 开始执行队列任务...
宏任务(同步任务)和微任务(异步任务)
graph TB
A["执行队列任务"]
A== 进入执行栈 -->B[同步 or 异步任务]
B== 同步 -->C(立即执行) == 同步任务执行完成 --> 完成后出栈 == 执行异步任务 --> 继续执行队列任务;
B== 异步 -->D[定时器触发线程] == 注册对应的回调函数 --> http触发线程 == 异步函数放入队列末尾 --> 继续执行队列任务 == 执行完毕 --> id>循环执行队列任务] --> A
let oneTask = () => {
console.log('第一个宏任务');
queueMicrotask(() => console.log('第一个微任务'));
}
oneTask()
setTimeout(() => console.log('处在中间位置的定时器任务'));
Promise.resolve('promise任务').then((result) => console.log(result));
let twoTask = () => {
console.log('第二个宏任务');
queueMicrotask(() => console.log('第二个微任务'));
}
twoTask()
graph LR
宏任务 --> 微任务 --> 异步定时器任务
再次证明宏任务(同步任务)往往会优先放入事件队列中执行
- 普通函数:属于宏任务
- queueMicrotask:创建一个微任务
- promise:也属于微任务
- setTimeout:属于异步任务
异步任务
- 在这里,再介绍两个异步任务 window.requestAnimationFrame
- 此Api作用为请求动画帧
- 实现动画可以用Css3,canvas,此Api专门用于请求动画
- 屏幕刷新频率:屏幕每秒出现图像的次数。普通笔记本为60Hz
- 动画原理:计算机每1Hz用时16.7ms,由于人眼的视觉停留,所以看起来是流畅的移动
- setTimeout:由于是异步定时任务
- 只有当主线程任务执行完后才会执行队列中的任务,因此实际执行时间总是比设定时间要晚
- 异步定时任务固定时间间隔不一定与屏幕刷新时间相同,会引起丢帧
- requestAnimationFrame:只会根据计算机刷新频率执行回调任务,会定时render界面
let progress = 0;
//回调函数
console.time()
const render = () => {
progress += 1; //修改图像的位置
if (progress < 100) {
//在动画没有结束前,递归渲染
window.requestAnimationFrame(render);
} else {
console.timeEnd()
}
}
//第一帧渲染
window.requestAnimationFrame(render)
window.requestIdleCallback
- 此Api作用与浏览器空闲时段内调用的函数排队
- 优先级低于window.requestAnimationFrame
- 在空闲回调函数中调用requestIdleCallback(),以便在下一次通过事件循环之前调度另一个回调。
window.requestIdleCallback(() => console.log('空闲调用该函数'));
graph LR
宏任务 --> 微任务 --> requestAnimationFrame --> requestIdleCallback --> 异步定时任务
总结
- 一个tab浏览器页面可以理解为拥有一个宏任务队列、一个微任务队列
- 在script加载时,先执行同步代码(宏任务),当执行栈清空后,将从微任务队列中依次取出放入到执行栈执行
- 当执行栈执行完微任务后,等待下一个宏任务或微任务进入执行栈,继续执行任务,以此构成事件循环。