您当前的位置:首页 > 电脑百科 > 软件技术 > 软件技术

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

时间:2020-06-18 10:58:45  来源:  作者:
浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务

 

作者:趁你还年轻

转发链接:https://segmentfault.com/a/1190000022950333

前言

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

我来举几个常见的例子:

  • 我执行了一段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)
  • 浅谈单线程jsjs引擎图什么是单线程js?
  • 单线程js属于浏览器的哪个进程?
  • js为什么要设计成单线程的?
  • 事件循环与消息队列什么是事件循环?
  • 什么是消息队列?
  • 如何实现一个 EventEmitter(支持 on,once,off,emit)?
  • 宏任务和微任务哪些属于宏任务?
  • 哪些属于微任务?
  • 事件循环,消息队列与宏任务、微任务之间的关系是什么?
  • 为任务添加和执行流程示意图
  • 浏览器页面循环系统原理图消息队列和事件循环setTimeoutXMLHttpRequest宏任务
  • 参考资料

浅谈Chrome架构

浏览器可以是哪种架构?

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

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

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

 

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

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

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

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

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

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

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

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

 

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

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

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

  • 每一个Tab就是要给单独的进程
  • 由于每个Tab都有自己独立的Renderer进程,因此某一个Tab出问题不会影响其它Tab

缺点

  • Tab间内存不共享,不同进程内存包含相同内容

Chrome多进程架构实锤图

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

 

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

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

 

渲染进程主要包括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.主线程主要遍历布局树生成层树

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

 

2.栅格线程栅格化磁贴到GPU

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

 

3.合成线程将磁贴合成帧并通过IPC传递给Browser进程,显示在屏幕上

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

 

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

浅谈单线程js

js引擎图

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

 

什么是单线程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渲染型。

事件循环

什么是事件循环?

事件循环英文名叫做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与事件循环的桥梁。

队列数据结构图

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

 

入队出队图

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

 

在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);
    if (idx === -1) return;
    const listener = this.listeners[idx];

    if (listener.isOnce) {
      listener.callbacks[0](...args);
      this.listeners.splice(idx, 1);
    } else {
      listener.callbacks.forEach((cb) => {
        cb(...args);
      });
    }
  }
  once(event, func) {
    const callback = (listener) => listener.name === event;
    let idx = this.listeners.findIndex(callback);
    if (idx !== -1) return;
    this.listeners.push({
      name: event,
      callbacks: [func],
      isOnce: true,
    });
  }
  off(event, func) {
    if (this.listeners.length === 0) return;
    const callback = (listener) => listener.name === event;
    let idx = this.listeners.findIndex(callback);
    if (idx === -1) return;
    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");
// event.emit("onceEvent", "abc", "456");

// 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");
// // off销毁指定回调
// event.off("onEvent", onCallback);
// event.emit("onEvent", "abc", "123");

宏任务和微任务

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

哪些属于宏任务?

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

哪些属于微任务?

  • Promise
  • MutationObserver
  • process.nextTick
  • queueMicrotask

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

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

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

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

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

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

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

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

 


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

 

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

过程可以拆为以下几步:

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

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

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

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

消息队列和事件循环

线程的一次执行

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

 

在线程中引入事件循环

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

 

渲染进程线程之间发送任务

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

 


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

 

线程模型:队列 + 循环

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

 

跨进程发送消息

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

 

单个任务执行时间过久

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

 

setTimeout

长任务导致定时器被延后执行

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

 

循环嵌套调用 setTimeout

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

 

XMLHttpRequest

消息循环系统调用栈记录

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

 

XMLHttpRequest 工作流程图

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

 

HTTPS 混合内容警告

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

 

使用 XMLHttpRequest 混合资源失效

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

 

宏任务

宏任务延时无法保证

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

 

如果文中有不对的地方,欢迎指正和交流~



Tags:浏览器架构   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
作者:趁你还年轻转发链接:https://segmentfault.com/a/1190000022950333前言看到这些词仿佛比较让人摸不着头脑,其实在我们的日常开发中,早就和它们打过交道了。我来举几个常见...【详细内容】
2020-06-18  Tags: 浏览器架构  点击:(90)  评论:(0)  加入收藏
▌简易百科推荐
今天教大家通过windows的CMD终端查看WIFI密码 使用管理权限打开CMD 查看本机连接过WIFI名称netsh wlan show profiles 查询某一个WIFI的密码比如这里我想查询猪猪侠的...【详细内容】
2021-12-27  吉祥同学学安全    Tags:wifi密码   点击:(1)  评论:(0)  加入收藏
从本质上来讲,PE系统最广泛的用途只是用来安装系统。通过其内置的Ghost软件来调用第三方的GHO系统镜像来完成系统的安装。从表面来看,最终的系统是否纯净、是否安全,与Gho镜像...【详细内容】
2021-12-27  u盘装系统    Tags:pe工具   点击:(2)  评论:(0)  加入收藏
Safari是苹果在iPhone和iPad上的默认网络浏览器。虽然我们天天都在使用,但是,你是否深入研究了Safari的所有功能和设置?"无痕浏览"、"阅读器"视图和下载文件等标准选项只是其...【详细内容】
2021-12-16  趣玩公社    Tags:Safari   点击:(20)  评论:(0)  加入收藏
《开源精选》是我们分享Github、Gitee等开源社区中优质项目的栏目,包括技术、学习、实用与各种有趣的内容。本期推荐的是一个开源的 HTML5 视频播放器插件&mdash;&mdash;MuiP...【详细内容】
2021-12-16  GitHub精选    Tags:插件   点击:(22)  评论:(0)  加入收藏
给电脑安装软件,最让人担心的就是安装到“拖家带口”的,而这样的手法正是国产软件惯用的!全家桶自动安装、软文弹窗等等,这些让我们讨厌的却是软件的直接收入来源。 那么在你安...【详细内容】
2021-12-15  老毛桃winpe    Tags:软件   点击:(18)  评论:(0)  加入收藏
不知道你是否遇到过这么一种情况:在一个A4大小的图框画一个接近满框的图形,在打印的时候却发现图形很小根本不符合模型空间的实际情况。看似明显是比例问题,但又不知道究竟是什...【详细内容】
2021-12-14  设计师小仁君    Tags:CAD   点击:(20)  评论:(0)  加入收藏
哈喽大家好! 前几天一个朋友向我疯狂吐槽。 快到年底了,公司要统计年度数据。 需要把一月到十二月份,十二张表格的数据,全部汇总到一个表格内。 这样的数据汇报每年都会有,每到年...【详细内容】
2021-12-14  秋叶Excel    Tags:Excel   点击:(23)  评论:(0)  加入收藏
我发现最近不少小叶子的留言都和 Excel 相关,我寻思是时候出一期解决 Excel 疑难杂症的小合集了。于是毛毛在众多问题中,挑了三个被问次数最多的有关 Excel 的问题,今天就来给...【详细内容】
2021-12-14  叶PPT    Tags:Excel   点击:(28)  评论:(0)  加入收藏
大家好,我是Stark-C。油猴简介【油猴】一款免费的浏览器扩展和最为流行的用户脚本管理器,它是一个附加组件(扩展程序),让用户安装一些脚本使大部分HTML为主的网页改变得更方便易...【详细内容】
2021-12-13  什么值得买    Tags:油猴   点击:(43)  评论:(0)  加入收藏
年末,又到了各大厂商盘点年度最佳的时候了。不过让世超感到意外的是 Google 竟然给自己 Chrome 的插件,做了一个 2021 年精选集锦,挑选出了 13 款今年最热门的 Chrome 插件。...【详细内容】
2021-12-13  科技知迅    Tags:Chrome   点击:(19)  评论:(0)  加入收藏
相关文章
    无相关信息
最新更新
栏目热门
栏目头条