关键词:多进程、单线程、事件循环、消息队列、宏任务、微任务
css
看到这些词仿佛比较让人摸不着头脑,其实在咱们的平常开发中,早就和他们打过交道了。前端
我来举几个常见的例子:git
其实上面举的这些click, setTimeout, setInterval, Promise,async/await, EventEmitter, MutationObserver, Event类, CustomEvent
与多进程、单线程、事件循环、消息队列、宏任务、微任务
或多或少的都有所联系。github
并且也与浏览器的运行原理有一些关系,做为天天在浏览器里辛勤耕耘的前端工程师们,浏览器的运行原理(多进程、单线程、事件循环、消息队列、宏任务、微任务)能够说是必需要掌握的内容了,不只对面试有用,对手上负责的开发工做也有很大的帮助。web
浅谈浏览器架构面试
前端最核心的渲染进程包含哪些线程?segmentfault
浅谈单线程js浏览器
事件循环与消息队列微信
宏任务和微任务网络
浏览器页面循环系统原理图
浏览器本质上也是一个软件,它运行于操做系统之上,通常来讲会在特定的一个端口开启一个进程去运行这个软件,开启进程以后,计算机为这个进程分配CPU资源、运行时内存,磁盘空间以及网络资源等等,一般会为其指定一个PID来表明它。
先来看看个人机器上运行的微信和Chrome的进程详情:
软件 | CPU(%) | 线程 | PID | 内存 | 端口 |
---|---|---|---|---|---|
微信 | 0.1 | 46 | 587 | 555MB | 124301 |
Chrome | 7.9 | 48 | 481 | 603MB | 1487 |
若是本身设计一个浏览器,浏览器能够是那种架构呢?
若是浏览器单进程架构的话,须要在一个进程内作到网络、调度、UI、存储、GPU、设备、渲染、插件等等任务,一般来讲能够为每一个任务开启一个线程,造成单进程多线程的浏览器架构。
可是因为这些功能的日益复杂,例如将网络,存储,UI放在一个线程中的话,执行效率和性能愈来愈地下,不能再向下拆分出相似“线程”的子空间。
所以,为了逐渐强化浏览器的功能,因而产生了多进程架构的浏览器,能够将网络、调度、UI、存储、GPU、设备、渲染、插件等等任务分配给多个单独的进程,在每个单独的进程内,又能够拆分出多个子线程,极大程度地强化了浏览器。
Chrome做为浏览器届里的一哥,他也是多进程IPC架构的。
Chrome多进程架构主要包括如下4个进程:
Chrome 多进程架构的优缺点
优势
缺点
Chrome多进程架构实锤图
渲染进程主要包括4个线程:
渲染进程的主线程知识点:
<script>
标签时,会下载而且执行js,执行js时,为了不改变DOM的结构,解析HTML停滞,js执行完成后继续解析HTML。正是由于JS执行会阻塞UI渲染,而JS又是浏览器的一哥,所以浏览器经常被看作是单线程的。 渲染进程的主线程细节能够查阅Chrome官方的博客:Inside look at modern web browser (part 3)和Rendering Performance
渲染进程的合成线程知识点:
下面来看下主线程、合成线程和光栅线程一块儿做用的过程
1.主线程主要遍历布局树生成层树
2.栅格线程栅格化磁贴到GPU
3.合成线程将磁贴合成帧并经过IPC传递给Browser进程,显示在屏幕上
图片引自Chrome官方博客:Inside look at modern web browser (part 3)
应用程序(实现) | 方言和最后版本 | ECMAScript版本 |
---|---|---|
Google Chrome,V8引擎 | JavaScript | ECMA-262,版本6 |
Mozilla Firefox,Gecko排版引擎,SpiderMonkey和Rhino | JavaScript 1.8.5 | ECMA-262,版本6 |
Safari,Nitro引擎 | JavaScript | ECMA-262,版本6 |
Microsoft Edge,Chakra引擎 | JavaScript | EMCA-262,版本6 |
Opera,Carakan引擎(改用V8以前) | 一些JavaScript 1.5特性及一些JScript扩展[12] | ECMA-262,版本5.1 |
KHTML排版引擎,KDE项目的Konqueror | JavaScript 1.5 | ECMA-262,版本3 |
Adobe Acrobat | JavaScript 1.5 | ECMA-262,版本3 |
OpenLaszlo | JavaScript 1.4 | ECMA-262,版本3 |
Max/MSP | JavaScript 1.5 | ECMA-262,版本3 |
ANT Galio 3 | JavaScript 1.5附带RMAI扩展 | ECMA-262,版本3 |
若是仔细阅读过第一部分“谈谈浏览器架构”的话,这个答案其实已经很是显而易见了。
在”前端最核心的渲染进程包含哪些线程?“这里咱们提到了主线程(Main thread)(下载资源、执行js、计算样式、进行布局、绘制合成,注意其中的执行js,这里其实已经明确告诉了咱们Chrome中JavaScript运行的位置。
那么Chrome中JavaScript运行的位置在哪里呢?
渲染进程(Renderer Process)中的主线程(Main Thread)
单线程的js -> 主线程(Main Thread)-> 渲染进程(Renderer Process)
其实更为严谨的表述是:“浏览器中的js执行和UI渲染是在一个线程中顺序发生的。”
这是由于在渲染进程的主线程在解析HTML生成DOM树的过程当中,若是此时执行JS,主线程会主动暂停解析HTML,先去执行JS,等JS解析完成后,再继续解析HTML。
那么为何要“主线程会主动暂停解析HTML,先去执行JS,再继续解析HTML呢”?
这是主线程在解析HTML生成DOM树的过程当中会执行style,layout,render以及composite的操做,而JS能够操做DOM,CSSOM,会影响到主线程在解析HTML的最终渲染结果,最终页面的渲染结果将变得不可预见。
若是主线程一边解析HTML进行渲染,JS同时在操做DOM或者CSSOM,结果会分为如下状况:
考虑到最终页面的渲染效果的一致性,因此js在浏览器中的实现,被设计成为了JS执行阻塞UI渲染型。
事件循环英文名叫作Event Loop,是一个在前端届老生常谈的话题。
我也简单说一下我对事件循环的认识:
事件循环能够拆为“事件”+“循环”。
先来聊聊“事件”:
若是你有必定的前端开发经验,对于下面的“事件”必定不陌生:
有事件,就有事件处理器:在事件处理器中,咱们会应对这个事件作一些特殊操做。
那么浏览器怎么知道有事件发生了呢?怎么知道用户对某个button作了一次click呢?
若是咱们的主线程只是静态的,没有循环的话,能够用js伪代码将其表述为:
function mainThread() { console.log("Hello World!"); console.log("Hello JavaScript!"); } mainThread();
执行完一次mainThread()以后,这段代码就无效了,mainThread并非一种激活状态,对于I/O事件是没有办法捕获到的。
所以对事件加入了“循环”,将渲染进程的主线程变为激活状态,能够用js伪代码表述以下:
// click event function clickTrigger() { return "我点击按钮了" } // 能够是while循环 function mainThread(){ while(true){ if(clickTrigger()) { console.log(“通知click事件监听器”) } clickTrigger = null; } } mainThread();
也能够是for循环
for(;;){ if(clickTrigger()) { console.log(“通知click事件监听器”) } clickTrigger = null; }
在事件监听器中作出响应:
button.addEventListener('click', ()=>{ console.log("多亏了事件循环,我(浏览器)才能知道用户作了什么操做"); })
消息队列能够拆为“消息”+“队列”。
消息能够理解为用户I/O;队列就是先进先出的数据结构。
而消息队列,则是用于链接用户I/O与事件循环的桥梁。
下面这个结构你们都熟悉,瞬间体现出队列FIFO的特性。
// 定义一个队列 let queue = [1,2,3]; // 入队 queue.push(4); // queue[1,2,3,4] // 出队 queue.shift(); // 1 queue [2,3,4]
假设用户作出了"click button1","click button3","click button 2"的操做。
事件队列定义为:
const taskQueue = ["click button1","click button3","click button 2"]; while(taskQueue.length>0){ taskQueue.shift(); // 任务依次出队 }
任务依次出队:
"click button1"
"click button3"
"click button 2"
此时因为mainThread有事件循环,它会被浏览器渲染进程的主线程事件循环系统捕获,并在对应的事件处理器作出响应。
button1.addEventListener('click', ()=>{ console.log("click button1"); }) button2.addEventListener('click', ()=>{ console.log("click button 2"); }) button3.addEventListener('click', ()=>{ console.log("click button3") })
依次打印:"click button1","click button3","click button 2"。
所以,能够将消息队列理解为链接用户I/O操做和浏览器事件循环系统的任务队列。
/** * 说明:简单实现一个事件订阅机制,具备监听on和触发emit方法 * 示例: * on(event, func){ ... } * emit(event, ...args){ ... } * once(event, func){ ... } * off(event, func){ ... } * const event = new EventEmitter(); * event.on('someEvent', (...args) => { * console.log('some_event triggered', ...args); * }); * event.emit('someEvent', 'abc', '123'); * event.once('someEvent', (...args) => { * console.log('some_event triggered', ...args); * }); * event.off('someEvent', callbackPointer); // callbackPointer为回调指针,不能是匿名函数 */ class EventEmitter { constructor() { this.listeners = []; } on(event, func) { const callback = () => (listener) => listener.name === event; const idx = this.listeners.findIndex(callback); if (idx === -1) { this.listeners.push({ name: event, callbacks: [func], }); } else { this.listeners[idx].callbacks.push(func); } } emit(event, ...args) { if (this.listeners.length === 0) return; const callback = () => (listener) => listener.name === event; const idx = this.listeners.findIndex(callback); this.listeners[idx].callbacks.forEach((cb) => { cb(...args); }); } once(event, func) { const callback = () => (listener) => listener.name === event; let idx = this.listeners.findIndex(callback); if (idx === -1) { this.listeners.push({ name: event, callbacks: [func], }); } } off(event, func) { if (this.listeners.length === 0) return; const callback = () => (listener) => listener.name === event; let idx = this.listeners.findIndex(callback); if (idx !== -1) { let callbacks = this.listeners[idx].callbacks; for (let i = 0; i < callbacks.length; i++) { if (callbacks[i] === func) { callbacks.splice(i, 1); break; } } } } } // let event = new EventEmitter(); // let onceCallback = (...args) => { // console.log("once_event triggered", ...args); // }; // let onceCallback1 = (...args) => { // console.log("once_event 1 triggered", ...args); // }; // // once仅监听一次 // event.once("onceEvent", onceCallback); // event.once("onceEvent", onceCallback1); // event.emit("onceEvent", "abc", "123"); // // off销毁指定回调 // let onCallback = (...args) => { // console.log("on_event triggered", ...args); // }; // let onCallback1 = (...args) => { // console.log("on_event 1 triggered", ...args); // }; // event.on("onEvent", onCallback); // event.on("onEvent", onCallback1); // event.emit("onEvent", "abc", "123"); // event.off("onEvent", onCallback); // event.emit("onEvent", "abc", "123");
事件循环会不断地处理消息队列出队的任务,而宏任务指的就是入队到消息队列中的任务,每一个宏任务都有一个微任务队列,宏任务在执行过程当中,若是此时产生微任务,那么会将产生的微任务入队到当前的微任务队列中,在当前宏任务的主要任务完成后,会依次出队并执行微任务队列中的任务,直到当前微任务队列为空才会进行下一个宏任务。
假设在执行解析HTML这个宏任务的过程当中,产生了Promise和MutationObserver这两个微任务。
// parse HTML··· Promise.resolve(); removeChild();
微任务队列会如何表现呢?
图片引自:极客时间的《浏览器工做原理与实践》
过程能够拆为如下几步:
Promise.resolve(); removeChild();
如下全部图均来自极客时间《《浏览器工做原理与实践》- 浏览器中的页面循环系统》,能够帮助理解消息队列,事件循环,宏任务和微任务。
线程的一次执行
在线程中引入事件循环
渲染进程线程之间发送任务
线程模型:队列 + 循环
跨进程发送消息
单个任务执行时间太久
长任务致使定时器被延后执行
循环嵌套调用 setTimeout
消息循环系统调用栈记录
XMLHttpRequest 工做流程图
HTTPS 混合内容警告
使用 XMLHttpRequest 混合资源失效
宏任务延时没法保证
若是文中有不对的地方,欢迎指正和交流~
期待和你们交流,共同进步,欢迎你们加入我建立的与前端开发密切相关的技术讨论小组:
- 微信公众号: 生活在浏览器里的咱们 / excellent_developers
- Github博客: 趁你还年轻233的我的博客
- SegmentFault专栏:趁你还年轻,作个优秀的前端工程师
- Leetcode讨论微信群:Z2Fva2FpMjAxMDA4MDE=(加我微信拉你进群)
努力成为优秀前端工程师!