从浏览器环境到JavaScript执行流程的一次简单梳理

参考文章

www.ruanyifeng.com/blog/2014/1…

www.zhihu.com/question/64…

www.alloyteam.com/2016/05/jav…

nodejs.org/zh-cn/docs/…

juejin.im/post/59e85e…

juejin.im/post/5be5a0…

juejin.im/post/5a6547…

浏览器环境的多进程环境

首先明确一下两个概念

  • 进程是CUP资源分配的最小单位 (每个进程之间相互独立,各自拥有一块运行资源)
  • 线程是CPU资源调度的最小单位 (一个进程可以包含多个线程,多个线程协作完成任务,共享一个进程中的资源) 现代浏览器是一个及其庞大的大型软件,在某种程度上甚至不亚于一个操作系统,它由多媒体支持、图形显示、GPU渲染、进程管理、内存管理、沙箱机制、存储系统、网络管理等大大小小数百个组件组成。如果这么大的一个软件是单进程的,那么其中一个组件出现问题,整个浏览器就无法运作,及其影响用户体验,所以浏览器在实现上是多进程的。

浏览器拥有的多个进程

  • 一个 Browser 进程
    • 浏览器的主进程,负责浏览器界面的显示与用户交互。
    • 负责创建和销毁其他进程
    • 网络资源管理
  • 多个 Renderer 进程
    • 每个tab页一个进程,浏览器有自己的优化策略,如多个空白tab页的时候会将其合并
    • 每个iframe页面单独一个renderer进程
    • 每个renderer进程是一个独立的沙箱,相互之间隔离不受影响
  • 一个 GPU 进程
  • 多个 NPAPI Render 进程多个 Pepper Plugin 进程
    • 每种类型的插件对应一个进程,仅当使用该插件时才创建

浏览器多线程的渲染进程(Renderer进程)

代码写的怎么样,页面性能如何的直观感觉是页面生成的快不快,这个与浏览器的渲染进程息息相关。下面先梳理一下

Renderer进程有哪些主要的线程

  • GUI渲染线程
    • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
    • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
    • 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  • JS引擎线程
    • 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
    • JS引擎线程负责解析Javascript脚本,运行代码。
    • 一个Renderer进程中只有一个JS引擎线程
    • GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  • 事件触发线程
    • 归属于浏览器而不是JS引擎,用来控制事件循环(点击,鼠标移动这些都是浏览器事件)
    • 事件触发之后会加入到事件队列等待执行
  • 定时触发器线程
    • setIntervalsetTimeout所在的线程
    • 浏览器定时计数器并不是由JavaScript引擎计数的,是交给浏览器计时
    • setTimeout(fn,ms) 指定某个任务在主线程最早可得的空闲时间执行,ms秒之后将fn函数加入到队列中
    • W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。所以setTimeout的延时设置为0也不可能瞬发
  • 异步http请求线程
    • 在XMLHttpRequest连接后是通过浏览器新开一个线程请求
    • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

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

由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。 因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起,GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。

网页解析的流程

主流程

页面的解析工作是在 Renderer 进程中进行的,Renderer 进程通过在主线程中持有的 Blink 实例边接收边解析 HTML 内容。每次从网络缓冲区中读取 8KB 以内的数据。浏览器自上而下逐行解析 HTML 内容,经过词法分析、语法分析,构建 DOM 树。当遇到外部 CSS 链接时,主线程调使用网络请求板块异步获取资源,不阻塞而继续构建 DOM 树。当 CSS 下载完毕后,主线程在合适的时机解析 CSS 内容,经过词法分析、语法分析,构建 CSSOM 树。浏览器结合 DOM 树和 CSSOM 树构建 Render 树,并计算布局属性,每个 Node 的几何属性和在坐标系中的位置,最后进行绘制展现在屏幕上。当遇到外部 JS 链接时,主线程调使用网络请求板块异步获取资源,因为 JS 可能会修改 DOM 树和 CSSOM 树而造成回流和重绘,此时 DOM 树的构建是处于阻塞状态的。但主线程并不会挂起,浏览器会用一个轻量级的扫描器去发现后续需要下载的外部资源,提前发起网络请求,而脚本内部的资源不会识别,比方 document.write。当 JS 下载完毕后,浏览器调使用 V8 引擎在 Script Streamer 线程中解析、编译 JS 内容,并在主线程中执行。

渲染流程

当 DOM 树构建完毕后,还需经过好几次转换,它们有多种中间表示。首先计算布局、绘图样式,转换为 RenderObject 树(也叫 Render 树)。再转换为 RenderLayer 树,当 RenderObject 拥有同一个坐标系(比方 canvas、absolute)时,它们会合并为一个 RenderLayer,这一步由 CPU 负责合成。接着转换为 GraphicsLayer 树,当 RenderLayer 满足合成层条件(比方 transform,熟知的硬件加速)时,会有自己的 GraphicsLayer,否则与父节点合并,这一步同样由 CPU 负责合成。最后,每个 GraphicsLayer 都有一个 GraphicsContext 对象,负责将层绘制成位图作为纹理上传给 GPU,由 GPU 负责合成多个纹理,最终显示在屏幕上。 另外,为了提升渲染性能效率,浏览器会有专使用的 Compositor 线程来负责层合成,同时负责解决部分交互事件(比方滚动、触摸),直接响应 UI 升级而不阻塞主线程。主线程把 RenderLayer 树同步给 Compositor 线程,由它开启多个 Rasterizer 线程,进行光栅化解决,在可视区域以瓦片为单位把顶点数据转换为片元,最后交付给 GPU 进行最终合成渲染。

JavaScript的事件循环

首先要将JavaScript分成同步任务和异步任务,其次是引入宏任务(macro-task)和微任务(micro-task)

同步任务和异步任务

下面用一副导图来描述一下同步任务与异步任务的执行

  • 首先会判断一个任务是同步任务还是异步任务,同步任务进入主线程,异步的进入Event Table并注册函数
    • 同步任务:页面骨架和页面渲染等这些主要的且资源耗时小的任务
    • 异步任务:图片、音乐等加载资源耗时长的或需要等待条件触发的任务
  • 当资源下载完毕或者指定的事件完成之后Event Table将对应的函数移入Event Queue中等待执行
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

宏任务和微任务

  • 宏任务(macrotask):
    • 可以理解为执行的整个代码块就是一个宏任务(每次执行栈中的代码)
    • 每一个宏任务都是连贯执行,中间不会中断去执行其他任务
    • 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->...
    • 主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个macrotask)
  • 微任务(microtask):
    • 一个宏任务执行完毕下一个宏任务执行之前执行这一个宏任务中产生的微任务(某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前))
    • 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染
    • Promise,process.nextTick, .then等

一道读程序题

console.log(1); 
setTimeout(function(){
    console.log(2);
},1000);

console.log(3); 

var p = new Promise(function(resolve,reject){ 
    setTimeout(function(){
        console.log(4);
    },500);
    resolve(); 
}).then(function(){
    console.log(5);
});

console.log(6);

p.then(function(){
    console.log(7);
});
console.log(8)

// 1 3 6 8 5 7 4 2
复制代码

第一次宏任务循环: 执行的整段代码看做一个宏任务

  • 遇到 console.log(1) 打印1
  • setTimeout()交给定时器处理线程处理,1秒延时后将任务加入宏任务队列 记为 s1
  • 遇到console.log(3) 打印3
  • 遇到Promise,执行里面的回调函数
  • setTimeout()交给定时器处理线程处理,0.5秒延时后将任务加入宏任务队列 记为s2
  • then中的回调函数加入这次的微任务队列 then1
  • 跳出Promise后,执行console.log(6),打印6
  • 执行p.then,把回调函数加入微任务队列 then2
  • 执行console.log(8)打印 8 此时打印的是 1 3 6 8 宏任务队列:s1,s2 微任务队列:then1,then2 第一次宏任务执行结束之后执行这次产生的微任务,依次执行then1,then2,依次打印 58

第二次宏任务开始 这里需要注意的是s1s2触发计时事件,是在Event Table中注册,等待计时完毕之后把回调函数加入Event Queue中,所以 s20.5秒比s11秒要先完成,这样在Event Queue中先放入s2,再放入s1 第二次宏任务执行s2,打印4,没有微任务 第三次宏任务执行s1,打印2,没有微任务

一些定时事件、点击事件、网络加载都是交给浏览器的API处理的,这些地方会创建宏任务,而且并不是直接加入到队列中,而是有一个EventTable来记录这些事件,等到条件满足之后再将回调的函数注册到Event Queue中,每次宏任务从 Event Queue中获取下一个宏任务

你可能感兴趣的:(javascript,ui,操作系统)