浏览器多线程到事件循环机制

浏览器多线程到事件循环机制

进程与线程

进程

进程是CPU分配资源的最小单位,它是一个可以自己独立运行且拥有自己资源空间的任务程序;包括程序以及程序所使用的内存及系统资源

当我们启动一个程序时就会创建一个进程来执行任务代码,同时会为该进程分配内存空间,该应用的状态都保存再该内存空间里;应用关闭时,应用空间就会被回收;进程可以启动更多的进程来执行任务,由于进程之间分配的内存空间是独立的;如果连个进程之间需要传递某些数据 ;则需要通过进程间的通信管道 IPC ;很多进程是多进程,是为了避免一个进程卡死,影响整个应用程序

进程可以将任务划分为多个小任务,启动多个线程并行执行不同的任务;同一个进程中的线程是可以直接通信,共享数据的

线程

线程是CPU调度的最小单位,它就是程序中的一个执行流;也可以理解为一个进程代码的不同执行路径

一个进程中只有一个执行流就是单线程,程序按照顺序执行,前面的处理好才执行后面的;一个进程中有多个执行流就是多线程,多个线程并行执行各自的任务

JS为什么是单线程

单线程就是同一时间只能做一件事情;JS是单线程是因为JS的主要作用是用户的交互,dom的操作;如果它是多线程,一个线程修改dom的内容;另一个线程也改了dom内容,那么到底怎么显示呢;为了避免这种复杂的问题,JS就是单线程(多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高。而且,如果同时操作 DOM ,在多线程不加锁的情况下,最终会导致 DOM 渲染的结果不可预期)

为了提高效率,新的标准(html5提出的web worker标准)允许JS创建多个线程,但是子线程完全收主线程控制,且不得操作DOM,所以它本质上还是单线程

谷歌浏览器的进程模型

  • process-per-site-instance

​ 谷歌浏览器默认模型;访问不同站点或者同一站点的不同Tab标签页都会单独创建一个渲染进程;比如如果跨域使用了iframe,那么iframe的渲染进程是一个单独的进程

​ 它是最安全的,因为每个站点互补影响;但是它进程多,占用的内存空间也多

  • process-per-site

    同一站点使用同一个渲染进程

  • process-per-tab

    每个标签页所有站点使用同一个渲染进程

  • single process

    浏览器 JS 引擎和渲染引擎共用一个进程;不推荐,因为不安全

浏览器从关闭状态进行启动,然后新开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个进程;后续再新开标签页,浏览器、网络进程、GPU进程是共享的,不会重新启动,如果2个页面属于同一站点的话,并且从a页面中打开的b页面,那么他们也会共用一个渲染进程,否则新开一个渲染进程。

最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。

如果我们打开多个Tab标签页,其中一个Tab标签页崩溃了,影响整个浏览器,那体验肯定是不行的,所以不可能是单进程;每个进程有多个线程都会占用资源,所以我们打开多个标签页可能会卡,谷歌浏览器就有标签页限制

浏览器多进程

在这里插入图片描述

浏览器(Broswer)进程

浏览器的主进程,该进程只有一个,主要负责与其他进程之间的协调、主控的作用

负责浏览器的页面展示、交互;(前进后退、书签等)

负责页面的子进程管理,创建和销毁其他进程

网络资源的管理,下载等

  • UI 线程:只有一个,浏览器主进程,负责处理选项卡页面之外的内容,用于控制用户可见的 UI 部分

    (比如地址栏,书签,后退、前进按钮)

  • 网络线程:发送请求,接收数据

  • 存储线程:控制对文件的访问

第三方插件进程

使用插件是才创建,一个插件对应一个进程

GPU进程

只有一个,处理图像,3d 绘制,提高性能

其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程;

它是使用浏览器的硬件加速技术实现的。GPU进程主要负责处理和管理GPU相关的操作和资源,同时确保GPU的安全和稳定性, 负责3D作图和使用GPU加速的网页效果的运行

渲染(Render)进程

Render渲染进程的核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页;(谷歌浏览器排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中);

默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下

内核分为渲染引擎和 JS 引擎,由于js引擎越来越独立,内核就倾向于只指渲染引擎

浏览器内核:内核分为渲染引擎和 JS 引擎

内核浏览器说明
TridentIE
GeckoFirefox
WebkitSafari苹果
BlinkChrome/Opera/Edge
网络进程

主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,后面才独立成为一个单独的进程

Render进程及它主要的线程

render渲染进程是多线程

GUI渲染线程

主要负责页面的渲染,解析html、css,生成DOM树、CSS规则树,构建Render树,页面的布局绘制

JS引擎线程程

负责解析JavaScript脚本,运行代码;JS是单线程

JS引擎线程与GUI渲染线程互斥

因为JS引擎可以修改DOM树,那么如果JS引擎在执行修改了DOM结构的同时,GUI线程也在渲染页面,那么这样就会导致渲染线程获取的DOM的元素信息可能与JS引擎操作DOM后的结果不一致。

当JS引擎执行的时候,GUI线程需要被冻结,但是GUI的渲染会被保存在一个队列当中,等待JS引擎空闲的时候执行渲染

如果JS引擎正在进行CPU密集型计算,那么JS引擎将会阻塞,长时间不空闲,导致渲染进程一直不能执行渲染,页面就会看起来卡顿卡顿的,渲染不连贯。所以,要尽量避免JS执行时间过长。

事件触发线程

属于浏览器而不是JS引擎

用来控制事件循环,管理事件队列

当js执行碰到事件绑定和异步操作会走事件触发线程,将对应的事件添加到对用的线程中;当事件触发或异步有了结果将它们的回调事件添加到事件队列等待JS引擎事件线程处理

因为JS是单线程,所以事件队列中的事件都要等待JS引擎线程处理

定时触发线程

浏览器定计数器不是在 JS 引擎线程中计数的(JS引擎是单线程,如果处于阻塞线程状态就计不了时,JS引擎线程与GUI渲染线程互斥,GUI进程执行的时候就阻塞了JS引擎线程,就记不了时了)

计时完成后,会添加到事件触发线程的事件队列中

W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms

异步http请求线程

当执行到一个http异步请求时,就把异步请求事件添加到异步请求线程,等收到响应(准确来说应该是http状态变化),再把回调函数添加到事件队列,等待js引擎线程来执行

事件循环机制Event Loop

JS分为同步任务和异步任务,同步任务在主线程也就是JS引擎线程上执行

除了主线程之外,我们的事件触发线程中有一个任务队列,只要异步事件有了结果就会在任务队列总添加它的回调事件;任务队列中由两个队列,一个宏任务队列,一个时微任务队列

1》首先我们的同步任务会进入主执行栈,异步任务交给事件触发线程,异步任务有了结果才会被添加到事件队列中

2》当我们主执行栈中的代码执行之后,会先去微任务队列读取任务,然后执行

3》执行完所有的微任务后又会到微任务队列中读取微任务(因为刚才可能产生了新的微任务),直到没有读取到微任务,然后GUI渲染线程会进行一次渲染(会阻塞js执行)

4》渲染后会去宏任务队列读取宏任务,然后执行

5》宏任务执行完毕会去微任务队列读取任务,有就执行,然后和之前一样,读取微任务直到没有微任务,进行渲染

6》重复上面的事情,宏任务-微任务-渲染

在这里插入图片描述

每次循环都会检查任务队列中是否有任务存在,如果有,就去取出任务执行,执行完后进入下一次循环,如果没有,进入休眠状态

当事件队列中有新的事件添加进来了;主线程如果是休眠状态,则会将其唤醒继续循环拿去任务

宏任务

所有微任务执行完后,下一个宏任务执行前,GUI渲染线程判断是否渲染一次页面

常见宏任务:

setTimeout

setInterval

requestAnimationFrame(浏览器)

微任务

常见微任务

Promise.then()

Promise.catch()

Promise.finally()

process.nextTick (node)

Object.observe

this.$nextTick()

Vue异步执行DOM更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作上非常重要。然后,在下一个的事件循环“tick”中,Vue刷新队列并执行实际 (已去重的) 工作。

nextTick会创建一个微任务(或宏任务),将其推入微任务队列中,vue中一个事件循环中所有dom更新也是一个微任务,dom更新再这个微任务之前进入微任务队列中,是先更新dom再执行this.$next中的代码,所以可以再里面获取到更新后的dom

异步处理更新队列的逻辑:在下一个的事件循环“tick”中,去刷新队列,依次尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境都不支持,则会采用 setTimeout(fn, 0) 代替;所以this.$nextTick(vue中) 可能是一个微任务也可能时宏任务

我们再来梳理一遍上面从数据变更到 dom 更新之前的整个流程

  • 修改响应式数据
  • 触发 Object.defineProperty 中的 set
  • 发布通知
  • 触发 Watcher 中的 update 方法,
  • update 方法中把 Watcher 缓冲到一个队列
  • 刷新队列的方法(其实就是更新 dom 的方法)传到 nextTick 方法中
  • nextTick 方法中把传进来的 callback 都放在一个数组 callbacks 中,然后放在异步队列中去执行

然后这时你调用了 $nextTick 方法,传进来一个获取最新 dom 的回调,这个回调也会推到那个数组 callbacks 中,此时遍历 callbacks 并执行所有回调的动作已经放到了异步队列中,到这(假设你后面没有其他的代码了)所有的同步代码就执行完了,然后开始执行异步队列中的任务,更新 dom 的方法是最先被推进去的,所以就先执行,你传进来的获取最新 dom 的回调是最后传进来的所以最后执行,显而易见,当执行到你的回调的时候,前面更新 dom 的动作都已经完成了,所以现在你的回调就能获取到最新的 dom 了。

如何理解JS中的异步

js是一门单线程语言,有i那位它运行在浏览器的渲染主线程中,而渲染主线程只有一个

渲染主线程有很多任务要出李,如果使用同步的方式,就极有可能导致主线程阻塞,导致其他任务无法执行,这样主线程的事件被浪费掉了,也买你也无法及时更新,体验也差

所以浏览器采用异步的方式来避免这种情况;当有些任务发送是,主线程会交给其他线程处理,自身立即结束任务的执行其他线程完成是,将实现称帝的回调包装程任务,加到消息队列末尾,等待主线程调度执行

这种异步模式下,浏览器不会阻塞,最大限度的保证单线程的流畅运行