Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务 #228

Open
FrankKai opened this issue Jun 16, 2020 · 6 comments

Comments

@FrankKai
Copy link
Owner

FrankKai commented Jun 16, 2020

image

关键词:
多进程、单线程、事件循环、消息队列、宏任务、微任务

看到这些词仿佛比较让人摸不着头脑,其实在我们的日常开发中,早就和他们打过交道了。

我来举几个常见的例子:

  • 我执行了一段js,页面就卡了挺久才有响应
  • 我触发了一个按钮的click事件,click事件处理器做出了响应
  • 我用setTimeout(callback, 1000)给代码加了1s的延时,1秒里发生了很多事情,然后功能正常了
  • 我用setInterval(callback, 100)给代码加了100ms的时间轮训,直到期待的那个变量出现再执行后续的代码,并且结合setTimeout删除这个定时器
  • 我用Promise,async/await顺序执行了异步代码
  • 我用EventEmitter、new Vue()做事件广播订阅
  • 我用MutationObserver监听了DOM更新
  • 我手写了一个Event类做事件的广播订阅
  • 我用CustomEvent创建了自定义事件
  • 我·······

其实上面举的这些click, setTimeout, setInterval, Promise,async/await, EventEmitter, MutationObserver, Event类, CustomEvent多进程、单线程、事件循环、消息队列、宏任务、微任务或多或少的都有所联系。

而且也与浏览器的运行原理有一些关系,作为每天在浏览器里辛勤耕耘的前端工程师们,浏览器的运行原理(多进程、单线程、事件循环、消息队列、宏任务、微任务)可以说是必须要掌握的内容了,不仅对面试有用,对手上负责的开发工作也有很大的帮助。

  • 浅谈浏览器架构
    • 浏览器可以是哪种架构?
    • 如何理解Chrome的多进程架构?
    • 前端最核心的渲染进程包含哪些线程?
      • 主线程(Main thread)(下载资源、执行js、计算样式、进行布局、绘制合成)
      • 光栅线程(Raster thread)
      • 合成线程(Compositor thread)
      • 工作线程(Worker thread)
  • 浅谈单线程js
    • js引擎图
    • 什么是单线程js?
    • 单线程js属于浏览器的哪个进程?
    • js为什么要设计成单线程的?
  • 事件循环与消息队列
    • 什么是事件循环?
    • 什么是消息队列?
    • 如何实现一个 EventEmitter(支持 on,once,off,emit)?
  • 宏任务和微任务
    • 哪些属于宏任务?
    • 哪些属于微任务?
    • 事件循环,消息队列与宏任务、微任务之间的关系是什么?
    • 微任务添加和执行流程示意图
  • 浏览器页面循环系统原理图
    • 消息队列和事件循环
    • setTimeout
    • XMLHttpRequest
    • 宏任务
  • 参考资料
@FrankKai
Copy link
Owner Author

FrankKai commented Jun 16, 2020

浅谈Chrome架构

浏览器可以是哪种架构?

浏览器本质上也是一个软件,它运行于操作系统之上,一般来说会在特定的一个端口开启一个进程去运行这个软件,开启进程之后,计算机为这个进程分配CPU资源、运行时内存,磁盘空间以及网络资源等等,通常会为其指定一个PID来代表它。

先来看看我的机器上运行的微信和Chrome的进程详情

软件 CPU(%) 线程 PID 内存 端口
微信 0.1 46 587 555MB 124301
Chrome 7.9 48 481 603MB 1487

如果自己设计一个浏览器,浏览器可以是那种架构呢?

  • 单进程架构(线程间通信)
  • 多进程架构(进程间IPC通信)

如果浏览器单进程架构的话,需要在一个进程内做到网络、调度、UI、存储、GPU、设备、渲染、插件等等任务,通常来说可以为每个任务开启一个线程,形成单进程多线程的浏览器架构。

但是由于这些功能的日益复杂,例如将网络,存储,UI放在一个线程中的话,执行效率和性能越来越地下,不能再向下拆分出类似“线程”的子空间

因此,为了逐渐强化浏览器的功能,于是产生了多进程架构的浏览器,可以将网络、调度、UI、存储、GPU、设备、渲染、插件等等任务分配给多个单独的进程,在每一个单独的进程内,又可以拆分出多个子线程,极大程度地强化了浏览器。

如何理解Chrome的多进程架构?

Chrome作为浏览器届里的一哥,他也是多进程IPC架构的。
image

Chrome多进程架构主要包括以下4个进程:

  • Browser进程(负责地址栏、书签栏、前进后退、网络请求、文件访问等)
  • Renderer进程(负责一个Tab内所有和网页渲染有关的所有事情,是最核心的进程
  • GPU进程(负责GPU相关的任务)
  • Plugin进程(负责Chrome插件相关的任务)

Chrome 多进程架构的优缺点
优点

  • 每一个Tab就是要给单独的进程
  • 由于每个Tab都有自己独立的Renderer进程,因此某一个Tab出问题不会影响其它Tab
    缺点
  • Tab间内存不共享,不同进程内存包含相同内容

Chrome多进程架构实锤图
image

前端最核心的渲染(Renderer)进程包含哪些线程?

image

渲染进程主要包括4个线程:

  • 主线程(Main thread)(下载资源、执行js、计算样式、进行布局、绘制合成)
  • 光栅线程(Raster thread)
  • 合成线程(Compositor thread)
  • 工作线程(Worker thread)

渲染进程的主线程知识点:

  • 下载资源:主线程可以通过Browser进程的network线程下载图片,css,js等渲染DOM需要的资源文件
  • 执行JS:主线程在遇到<script>标签时,会下载并且执行js,执行js时,为了避免改变DOM的结构,解析HTML停滞,js执行完成后继续解析HTML。正是因为JS执行会阻塞UI渲染,而JS又是浏览器的一哥,因此浏览器常常被看做是单线程的。
  • 计算样式:主线程会基于CSS选择器或者浏览器默认样式去进行样式计算,最终生成Computed Style
  • 进行布局:主线程计算好样式以后,可以确定元素的位置信息以及盒模型信息,对元素进行布局
  • 进行绘制:主线程根据先后顺序以及层级关系对元素进行渲染,通常会生成多个图层
  • 最终合成:主线程将渲染后的多个frame(帧)合成,类似flash的帧动画和PS的图层

渲染进程的主线程细节可以查阅Chrome官方的博客:Inside look at modern web browser (part 3)Rendering Performance

渲染进程的合成线程知识点:

  • 浏览器滚动时,合成线程会创建一个新的合成帧发送给GPU
  • 合成线程工作与主线程无关,不用等待样式计算或者JS的执行,因此合成线程相关的动画比涉及到主线程重新计算样式和js的动画更加流畅

下面来看下主线程、合成线程和光栅线程一起作用的过程
1.主线程主要遍历布局树生成层树
image
2.栅格线程栅格化磁贴到GPU
image
3.合成线程将磁贴合成帧并通过IPC传递给Browser进程,显示在屏幕上
image

图片引自Chrome官方博客:Inside look at modern web browser (part 3)

@FrankKai
Copy link
Owner Author

FrankKai commented Jun 16, 2020

浅谈单线程js

js引擎图

应用程序(实现) 方言和最后版本 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

什么是单线程js?

如果仔细阅读过第一部分“谈谈浏览器架构”的话,这个答案其实已经非常显而易见了。
在”前端最核心的渲染进程包含哪些线程?“这里我们提到了主线程(Main thread)(下载资源、执行js、计算样式、进行布局、绘制合成,注意其中的执行js,这里其实已经明确告诉了我们Chrome中JavaScript运行的位置。

那么Chrome中JavaScript运行的位置在哪里呢?

渲染进程(Renderer Process)中的主线程(Main Thread)

单线程js属于浏览器的哪个进程?

单线程的js -> 主线程(Main Thread)-> 渲染进程(Renderer Process)

js为什么要设计成单线程的?

其实更为严谨的表述是:“浏览器中的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,结果会分为以下情况:

  • 以主线程解析HTML的渲染结果为准
  • 以JS同时在操作DOM或者CSSOM的渲染结果为准

考虑到最终页面的渲染效果的一致性,所以js在浏览器中的实现,被设计成为了JS执行阻塞UI渲染型。

@FrankKai
Copy link
Owner Author

FrankKai commented Jun 16, 2020

事件循环

什么是事件循环?

事件循环英文名叫做Event Loop,是一个在前端届老生常谈的话题。
我也简单说一下我对事件循环的认识:

事件循环可以拆为“事件”+“循环”。
先来聊聊“事件”:

如果你有一定的前端开发经验,对于下面的“事件”一定不陌生:

  • click、mouseover等等交互事件
  • 事件冒泡、事件捕获、事件委托等等
  • addEventListener、removeEventListener()
  • CustomEvent(自定义事件实现自定义交互)
  • EventEmitter、EventBus(on,emit,once,off,这种东西经常出面试题)
  • 第三方库的事件系统

有事件,就有事件处理器:在事件处理器中,我们会应对这个事件做一些特殊操作。

那么浏览器怎么知道有事件发生了呢?怎么知道用户对某个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与事件循环的桥梁。

队列数据结构图

image

入队出队图

image

在js中,如何发现出队列FIFO的特性?

下面这个结构大家都熟悉,瞬间体现出队列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操作和浏览器事件循环系统的任务队列

如何实现一个 EventEmitter(支持 on,once,off,emit)?

/**
 * 说明:简单实现一个事件订阅机制,具有监听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");

@FrankKai FrankKai changed the title 如何理解宏任务、微任务? 如何理解事件循环、宏任务和微任务? Jun 16, 2020
@FrankKai FrankKai changed the title 如何理解事件循环、宏任务和微任务? 浅谈浏览器架构、单线程js引擎、事件循环、宏任务和微任务? Jun 16, 2020
@FrankKai FrankKai changed the title 浅谈浏览器架构、单线程js引擎、事件循环、宏任务和微任务? 浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务 Jun 16, 2020
@FrankKai
Copy link
Owner Author

FrankKai commented Jun 16, 2020

宏任务和微任务

  • 哪些属于宏任务?
  • 哪些属于微任务?
  • 事件循环,消息队列与宏任务、微任务之间的关系是什么?
  • 微任务添加和执行流程示意图

哪些属于宏任务?

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • I/O
  • UI渲染

哪些属于微任务?

  • Promise
  • MutationObserver
  • process.nextTick
  • queueMicrotask

事件循环,消息队列与宏任务、微任务之间的关系是什么?

  • 宏任务入队消息队列,可以将消息队列理解为宏任务队列
  • 每个宏任务内有一个微任务队列,执行过程中微任务入队当前宏任务的微任务队列
  • 宏任务微任务队列为空时才会执行下一个宏任务
  • 事件循环捕获队列出队的宏任务和微任务并执行

事件循环会不断地处理消息队列出队的任务,而宏任务指的就是入队到消息队列中的任务,每个宏任务都有一个微任务队列,宏任务在执行过程中,如果此时产生微任务,那么会将产生的微任务入队到当前的微任务队列中,在当前宏任务的主要任务完成后,会依次出队并执行微任务队列中的任务,直到当前微任务队列为空才会进行下一个宏任务。

微任务添加和执行流程示意图

假设在执行解析HTML这个宏任务的过程中,产生了Promise和MutationObserver这两个微任务。

// parse HTML···
Promise.resolve();
removeChild();

微任务队列会如何表现呢?

image

image

图片引自:极客时间的《浏览器工作原理与实践》

过程可以拆为以下几步:

  1. 主线程执行JS Promise.resolve(); removeChild();
  2. parseHTML宏任务暂停
  3. Promise和MutationObserver微任务入队到parseHTML宏任务的微任务队列
  4. 微任务1 Promise.resolve()执行
  5. 微任务2 removeChild();执行
  6. 微任务队列为空,parseHTML宏任务继续执行
  7. parseHTML宏任务完成,执行下一个宏任务

@FrankKai
Copy link
Owner Author

FrankKai commented Jun 16, 2020

浏览器页面循环系统原理图

以下所有图均来自极客时间《《浏览器工作原理与实践》- 浏览器中的页面循环系统》,可以帮助理解消息队列,事件循环,宏任务和微任务。

  • 消息队列和事件循环
  • setTimeout
  • XMLHttpRequest
  • 宏任务

消息队列和事件循环

线程的一次执行
image
在线程中引入事件循环
image
渲染进程线程之间发送任务
image

image
线程模型:队列 + 循环
image
跨进程发送消息
image
单个任务执行时间过久
image

setTimeout

长任务导致定时器被延后执行
image
循环嵌套调用 setTimeout
image

XMLHttpRequest

消息循环系统调用栈记录
image
XMLHttpRequest 工作流程图
image
HTTPS 混合内容警告
image
使用 XMLHttpRequest 混合资源失效
image

宏任务

宏任务延时无法保证
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant