美文网首页前端H5开发前端开发
从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

作者: 海之深处爱之港湾 | 来源:发表于2019-04-18 16:09 被阅读1次

    一、区分进程和线程

    形象比喻

    * 进程是一个工厂,工厂有它独立的资源;
    
    * 工厂之间相互独立;
    
    * 线程是工厂里边的工人,多个工人协同完成任务;
    
    * 工厂内有一个或多个工人;
    
    * 工人之间共享空间
    

    完善概念

    * 工厂的资源 -> 系统分配的内存(独立的一块内存)
    
    * 工厂之间的相互独立 -> 进程之间相互独立
    
    * 多个工人协作完成任务 -> 多个线程在进程中协作完成任务
    
    * 工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成
    
    * 工人之间共享空间 ->同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
    
    • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
    • 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

    tips

    • 不同进程之间也可以通信,不过代价较大
    • 现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)

    二、浏览器是多进程的

    • 浏览器是多进程的
    • 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
    • 简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。

    三、浏览器都包含哪些进程?

    • Browser进程:浏览器的主进程(负责协调、主控),只有一个。作用有:
      • 负责浏览器界面显示,与用户交互。如前进,后退等

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

      • 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上

      • 网络资源的管理,下载等

    • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
    • GPU进程:最多一个,用于3D绘制等
    • 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为
      • 页面渲染,脚本执行,事件处理等

    强化记忆:在浏览器中打开一个网页相当于新起了一个进程(进程内有自己的多线程)

    四、浏览器多进程的优势

    相比于单进程浏览器,多进程有如下优点:

    * 避免单个page crash影响整个浏览器
    
    * 避免第三方插件crash影响整个浏览器
    
    * 多进程充分利用多核优势
    
    * 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性
    

    简单点理解:如果浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差;同理如果是单进程,插件崩溃了也会影响整个浏览器;而且多进程还有其它的诸多优势。。。

    五、浏览器内核(渲染进程)

    浏览器的渲染进程是多线程的

    那些线程?

    • GUI渲染线程

      • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。

      • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行

      • 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

    • JS引擎线程

      • 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)

      • JS引擎线程负责解析Javascript脚本,运行代码。

      • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序

      • 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

    • 事件触发线程

      • 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)

      • 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中

      • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理

      • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)

    • 定时触发器线程

      • 传说中的setInterval与setTimeout所在线程
      • 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
      • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
      • 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
    • 异步http请求线程

      • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求

      • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

    六、Browser进程和浏览器内核(Renderer进程)的通信过程

    • Browser进程收到用户请求,首先需要获取页面内容(譬如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程
    • Renderer进程的Renderer接口收到消息,简单解释后,交给渲染线程,然后开始渲染
      • 渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源* 和需要GPU进程来帮助渲染

      • 当然可能会有JS线程操作DOM(这样可能会造成回流并重绘)

      • 最后Render进程将结果传递给Browser进程

    • Browser进程接收到结果并将结果绘制出来

    七、简单梳理下浏览器渲染流程

    * 浏览器输入url,浏览器主进程接管,开一个下载线程,
    然后进行 http请求(略去DNS查询,IP寻址等等操作),然后等待响应,获取内容,
    随后将内容通过RendererHost接口转交给Renderer进程
    
    * 浏览器渲染流程开始
    

    浏览器器内核拿到内容后,渲染大概可以划分成以下几个步骤:

    * 解析html建立dom树
    
    * 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
    
    * 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
    
    * 绘制render树(paint),绘制页面像素信息
    
    * 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。
    

    八、load事件与DOMContentLoaded事件的先后

    load事件与DOMContentLoaded事件的先后?

    • 当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片。 (譬如如果有async加载的脚本就不一定完成)
    • 当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了。 (渲染完毕了)

    所以,顺序是:DOMContentLoaded -> load

    九、css加载是否会阻塞dom树渲染?

    这里说的是头部引入css的情况

    首先,我们都知道:css是由单独的下载线程异步下载的。

    然后再说下几个现象:

    • css加载不会阻塞DOM树解析(异步加载时DOM照常构建)
    • 但会阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息)

    这可能也是浏览器的一种优化机制。

    因为你加载css的时候,可能会修改下面DOM节点的样式, 如果css加载不阻塞render树渲染的话,那么当css加载完之后, render树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。 所以干脆就先把DOM树的结构先解析完,把可以做的工作做完,然后等你css加载完之后, 在根据最终的样式来渲染render树,这种做法性能方面确实会比较好一点。

    十、事件循环进阶:macrotask与microtask

    JS中分为两种任务类型:macrotaskmicrotask,在ECMAScript中,microtask称为jobs,macrotask可称为task

    它们的定义?区别?简单点可以按如下理解:

    • macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
      • 每一个task会从头到尾将这个任务执行完毕,不会执行其它

      • 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->...)

    • microtask(又称为微任务),可以理解是在当前 task执行结束后立即执行的任务
      • 也就是说,在当前task任务后,下一个task之前,在渲染之前

      • 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染

      • 也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)

    分别很么样的场景会形成macrotask和microtask呢?

    • macrotask:主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个macrotask)
    • microtask:Promise,process.nextTick等

    补充:在node环境下,process.nextTick的优先级高于Promise,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分。

    另外,setImmediate则是规定:在下一次Event Loop(宏任务)时触发(所以它是属于优先级较高的宏任务), (Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面), 所以setImmediate如果嵌套的话,是需要经过多个Loop才能完成的, 而不会像process.nextTick一样没完没了。

    再根据线程来理解下:

    * macrotask中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护
    
    * microtask中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前macrotask执行完毕后执行,而这个队列由JS引擎线程维护 (这点由自己理解+推测得出,因为它是在主线程下无缝执行的)
    

    所以,总结下运行机制:

    * 执行一个宏任务(栈中没有就从事件队列中获取)
    
    * 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
    
    * 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
    
    * 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
    
    * 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
    

    原文链接:http://www.dailichun.com/2018/01/21/js_singlethread_eventloop.html?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

    相关文章

      网友评论

        本文标题:从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

        本文链接:https://www.haomeiwen.com/subject/xsetgqtx.html