Chrome浏览器的多进程架构
- 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU 进程:其实, GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
- 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
渲染进程(浏览器内核)
-
GUI渲染线程
- 负责浏览器页面HTML的渲染和绘制
- 如果页面需要重排或者回流,该线程会执行
- 与js引擎线程互斥,在js引擎线程执行时,该线程会停止执行
-
JS引擎线程
- 负责解析和实行JavaScript脚本,是主线程
- 一个tab只有一个JS引擎线程
- 与GUI渲染线程互斥,如果js解析时间比较长,GUI渲染线程处于等待的状态,会造成页面卡顿
-
事件触发线程
- 当js引擎线程执行到绑定的事件时,该线程会把绑定事件添加到该线程
- 等到被绑定的事件触发时,该线程会把触发事件后的回调函数添加到任务队列,等待JS引擎线程处理
- 被触发的事件可以是鼠标点击、ajax异步等
- 由于js单线程,故所有的事件都得排队等JS引擎线程处理
-
定时触发器线程
- 定时器setTimeout、setInterval所在的线程
- js是单线程的,如果计时器在主线程计时,会阻碍线程,因此需要别的线程专门处理计时
- 当JS引擎线程执行到setTimeout、setInterval关键词时,会把定时器任务添加到该线程,等计时完毕后,通知事件触发线程
-
异步HTTP请求线程
- 一个处理ajax的线程
- 请求完成时,如果有回调函数,通知事件触发线程
浏览器线程间的关系:
GUI渲染线程和 JS引擎线程是互斥的。当JS引擎线程执行时,GUI渲染线程被挂起,GUI的更新会保存到队列中,等JS引擎线程线程空闲时执行。
若GUI渲染线程和 JS引擎线程同时执行,假设JS引擎线程更改了dom结构,GUI渲染线程进行了渲染,那么 GUI渲染线程获取到的dom结构可能和JS引擎线程操作dom后的结构不一致。为了防止这种现象,js被设计成单线程的,即GUI渲染线程和 JS引擎线程不可同时执行。
事件触发线程和定时触发器线程、异步HTTP请求线程的共同点在于在于他们都使用了回调函数。当触发回调函数时,浏览器会将回调函数放到事件队列里,等JS引擎线程空闲时执行。
那么GUI线程和JS引擎线程是如何协调工作的的呢?
答案就是event loop
宿主环境:
JS引擎并不提供event loop,它是宿主环境为了集合渲染和JS执行,也为了处理JS执行时的高优先级任务而设计的机制。宿主环境有浏览器、node、跨端引擎等,不同的宿主环境有一些区别。
浏览器中的event loop
Call stack
js引擎是单线程的,这意味着js一次只能做一件事情,因为它只有一个调用堆栈(执行栈),调用堆栈是一种帮助JavaScript跟踪脚本调用的函数的机制。每次脚本或函数调用一个函数时,它都会被添加到调用堆栈的顶部。每次函数退出时,js引擎都会将其从调用堆栈中移除。堆栈处理每个函数调用的顺序遵循LIFO原则(后进先出)。
Web APIs
试问当在js代码里写了一个耗时有点长的http请求时会发生什么?
这将使后面的程序无法运行,造成阻塞,影响交互,直到从服务器获得响应信息。
为了解决这个问题,浏览器提供了可以在JavaScript代码中调用的api。然而,执行是由浏览器本身去调度的,这就是它不会阻塞调用堆栈的原因。webAPI包括AJAX请求、操作DOM、setTimeout/setInterval等,但也可以做其他事情,比如地理跟踪、访问本地存储、服务工作者等等。
Callback Queue
Web API允许JS在API执行完毕后将回调函数添加到Callback Queue中,等待Call stack(调用堆栈)为空时再来执行回调队列中的函数。
const a = () => console.log('a');
const b = () => setTimeout(() => console.log('b'), 100);
const c = () => console.log('c');
a();
b();
c();
调用setTimeout会触发web API的执行,从而将回调添加到回调队列中。然后事件循环从队列中获取回调,并在堆栈为空时将其添加到堆栈中。与调用堆栈不同,回调队列遵循FIFO顺序(First In, First Out)。
总结起来就是:
- 同步代码,一行一行放在Call Stack执行
- 遇到异步先记录下来,等待时机(定时,网络请求)
- 时机一到立马推入Callback Queue中
- 如Call Stack为空(同步代码执行完毕),Event Loop开始工作
- 轮询查找Callback Quque,有则移动到Call Stack中执行
- 然后继续轮询查找
从线程的角度看浏览器的event loop,用一张图来解释:
- 不管是
setTimeout/setInterval和XHR/fetch代码,在这些代码执行时, 本身是同步任务,而其中的回调函数才是异步任务。
- 当代码执行到
setTimeout/setInterval时,实际上是JS引擎线程通知定时触发器线程,间隔一个时间后,会触发一个回调事件, 而定时触发器线程在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列中。
- 当代码执行到
XHR/fetch时,实际上是JS引擎线程通知异步http请求线程,发送一个网络请求,并制定请求完成后的回调事件, 而异步http请求线程在接收到这个消息后,会在请求成功后,将回调事件放入到由事件触发线程所管理的事件队列中。
- 当同步任务执行完,
JS引擎线程会询问事件触发线程,在事件队列中是否有待执行的回调函数,如果有就会加入到执行栈中交给JS引擎线程执行。
宏任务与微任务
而异步任务又分为宏任务和微任务,可以理解为微任务为“高优先级”的异步任务
-
宏任务:
macrotask,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从回调队列中获取一个回调并放到执行栈中执行)。
浏览器为了能够使得JS内部macrotask与DOM任务能够有序的执行,会在一个macrotask执行结束后,在下一个macrotask 执行开始前,对页面进行重新渲染.
宏任务包括:setTimeout、setInterval、AJAX、DOM事件等
-
微任务:
microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。
微任务包括:Promise.then、async/await
-
浏览器中event loop 在js线程上运行的一个清晰的流程如下图所示:
JS执行与DOM渲染
JS执行与DOM渲染的顺序:
微任务执行时机要比宏任务要早。,宏任务在DOM渲染后触发,而微任务是DOM渲染前触发的。执行顺序为:同步代码(主线程)---> 微任务--> DOM渲染---> 宏任务。
浏览器里面执行一个 JS 任务就是一个 event loop,每个 loop 结束会检查下是否需要渲染,通过这种每次 loop 结束都 check 的方式来综合渲染、JS 执行等,以此来协调渲染线程和JS线程工作。
event loop 实现了 task 和 高优先级任务处理机制 microtask,而且每次 loop 结束会 check 是否要渲染,渲染之前会有 requestAnimationFrames 生命周期。 每一帧的计算和渲染是有固定频率的,如果 JS 执行时间过长,超过了一帧的刷新时间,那么就会导致渲染延迟,甚至掉帧。
什么情况会导致帧刷新拖延甚至帧数据被覆盖(丢帧)呢?
每个 loop 在 check 渲染之前的每一个阶段都有可能,也就是 task、microtask、requestAnimationFrame、requestIdleCallback 都有可能导致阻塞了 check,这样等到了 check 的时候发现要渲染了,再去渲染的时候就晚了。
因此浏览器提供 了requestIdeCallback API 在每帧间隔来执行某些计算
requestIdleCallback 会在每次 check 结束发现距离下一帧的刷新还有时间,就执行一下。如果时间不够,就下一帧再说。如果每一帧都没时间呢,那也不行。所以并提供了 timeout 的参数可以指定最长的等待时间,如果一直没时间执行这个逻辑,那么就算拖延了帧渲染也要执行。
例子
例子一:
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:grey';
页面背景会在瞬间变成灰色,以上代码属于同一次宏任务,所以全部执行完才触发页面渲染,渲染时GUI线程会将所有UI改动优化合并,所以视觉效果上,只会看到页面变成灰色。
例子二:
document.body.style = 'background:blue';
setTimeout(function(){
document.body.style = 'background:black'
},1000)
页面先显示成蓝色背景,然后瞬间变成了黑色背景,这是因为以上代码属于两次宏任务,第一次宏任务执行的代码是将背景变成蓝色,然后触发渲染,将页面变成蓝色,再触发第二次宏任务将背景变成黑色。
例子三:
document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
console.log(2);
document.body.style = 'background:black'
});
console.log(3);
页面的背景色直接变成黑色,没有经过蓝色的阶段,是因为,在宏任务中将背景设置为蓝色,但在进行渲染前执行了微任务, 在微任务中将背景变成了黑色,然后才执行的渲染。