此文首发于 lijing0906.github.iojavascript
本想写写Promise
的,可是查阅相关博客的时候发现浏览器进程、JS事件循环机制、宏任务和微任务须要提早学习一下,因而有了这篇博客。 参考连接css
区分进程和线程
用个形象的比喻:html
- 工厂之间相互独立
- 线程是工厂中的工人,多个工人协做完成任务
- 工厂内有一个或多个工人
- 工人之间共享空间
引伸为计算机线程进程:前端
- 进程是一个工厂,工厂有本身独立的资源 -> 系统分配的内存(独立的一块内存)
- 工厂之间相互独立 -> 进程之间相互独立
- 线程是工厂中的工人,多个工人协做完成任务 -> 多个线程在进程中相互协做完成任务
- 工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成
- 工人之间共享空间 -> 同一进程下各个线程之间共享程序的内存空间(如代码段、数据集、堆等)
浏览器是多进程的
- Browser进程,浏览器的主进程(负责协调,主控),只有一个,做用有:
- 负责浏览器界面的显示,与用户交互。如前进、后退等
- 负责各页面的管理,建立和销毁其余进程
- 将Renderer进程获得的内存中的bitmap绘制到用户界面上
- 网络资源的管理,下载等
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才建立
- GPU进程:最多一个,用于3D绘制等
- 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每一个Tab页面一个进程,互不影响,主要用于页面渲染,脚本执行,事件处理等 **强调:**浏览器中打开一个网页至关于新起了一个进程(进程内有本身的多线程),也有可能多个合并成一个,经过Chrome的
更多工具 -> 任务管理器
能够查看
浏览器多进程的优点
- 避免单个page crash影响整个浏览器
- 避免第三方插件crash影响整个浏览器
- 多进程充分利用多核优点
- 方便使用沙盒模型隔离插件等进程,提升浏览器稳定性 简单理解:若是浏览器是单进程,那么某个Tab页或者某个插件崩溃了,就影响整个浏览器 固然,内存等资源消耗也会更大,有点空间换时间的意思。
重点来了,浏览器内核(渲染进程)
对于前端来讲,页面的渲染、JS的执行、事件的循环都在这个进程中进行。 浏览器的渲染进程是多线程的。 浏览器的渲染进程包括哪些线程:java
- GUI渲染进程
- 负责渲染浏览器界面,解析HTML、CSS,构建DOM树和RenderObject树,布局和绘制
- 当界面须要重绘(Repaint)或因为某种操做引起回流(reflow)时,该线程就会执行
- GUI渲染进程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(至关于被冻结了),GUI更新会被保存在一个队列中,等到JS引擎空闲时当即被执行。
- JS引擎线程
- 也称JS内核,负责处理JS脚本程序。例如V8引擎
- JS引擎一直等待着任务队列中任务的到来,而后加以处理,一个Tab页(Renderer进程)中不管何时都只有一个JS引擎线程在运行JS程序
- GUI渲染进程与JS引擎线程是互斥的,因此若是JS执行的时间过长,页面渲染就不连贯。
- 事件触发线程
- 归属于浏览器而不是JS引擎,用来控制事件循环(能够理解为:JS引擎本身都忙不过来,须要浏览器另开线程协助)
- 当JS引擎执行代码块和setTimeout时(也可来自浏览器内核的其余线程,如鼠标点击、ajax异步请求等),会将对应事件任务添加到事件线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的尾部,等待JS引擎的处理
- 因为JS是单线程关系,因此这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
- 定时触发器线程
- 传说中的
setInterval
和setTimeout
所在的线程
- 浏览器定时计数器并非由JS引擎计数的,由于JS引擎是单线程的,若是处于阻塞线程状态就会影响计时的准确性
- 所以经过定时触发器线程来计时并触发定时,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行
- W3C在HTML标准中规定,要求setTimeout中低于4ms的时间间隔算4ms
- 异步http请求线程
- XMLHttpRequest在链接后是经过浏览器新开一个线程请求
- 在检测到状态变动时,若是设置有回调函数,异步线程就产生状态变动事件,将这个回调再放入事件队列中,再由JS引擎执行
Browser进程和浏览器内核(Renderer进程)如何通讯
- Browser进程收到用户请求,首先须要获取页面内容(好比经过网络下载资源),随后将该任务经过RendererHost接口传递给Renderer进程
- Renderer进程的RendererHost接口收到消息,简单解释后,交给渲染线程,而后开始渲染
- 渲染线程接收到请求,加载网页并渲染网页,这其中可能须要Browser进程获取资源和须要GPU进程来帮助渲染
- 固然可能会有JS引擎线程操做DOM(这样可能会形成回流并重绘)
- 最后Renderer进程将结果传递给Browser进程
- Browser进程接收到结果并将结果绘制出来
梳理浏览器渲染流程
简化前期工做:node
浏览器输入url,浏览器Browser主进程接管,开一个下载线程 而后进行http请求(略去DNS查询,IP寻址等等操做),而后等待响应,获取内容 获得内容就将内容经过RendererHost接口转交给Renderer进程 浏览器渲染流程开始git
浏览器内核拿到内容后,渲染大概能够划分红如下几个步骤:github
- 解析html建立dom树
- 解析css构建render树(将css解析成树形结构,而后结合DOM合并成render树)
- 布局render树(layout/reflow),负责各元素尺寸、位置的计算
- 绘制render树(paint),绘制页面像素信息
- 浏览器将各层的信息发送给GPU,GPU会将各层合成(composite)显示在页面上 全部详细步骤已略去,渲染完毕后就是load事件了,以后就是本身的JS逻辑处理了
从Event Loop谈JS的运行机制
理解一个概念:面试
- JS分为同步任务和异步任务
- 同步任务都在主线程上执行,造成一个执行栈
- 主线程以外,事件触发线程管理着一个
任务队列
,只要异步任务有了运行结果,就在任务队列
之中放置一个事件。
- 一旦
执行栈
中的全部同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列
,将可运行的异步任务添加到可执行栈中,开始执行。
看到这里,应该就能够理解了:为何有时候setTimeout推入的事件不能准时执行?由于可能在它推入到事件列表时,主线程还不空闲,正在执行其它代码,因此天然有偏差。
事件循环机制进一步补充
上图大体描述就是:
- 主线程运行时会产生执行栈,
- 栈中的代码调用某些api时,它们会在事件队列中添加各类事件(当知足触发条件后,如ajax请求完毕)
- 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
- 如此循环
- 注意,老是要等待栈中的代码执行完毕后才会去读取事件队列中的事件
事件循环进阶:macrotask与microtask
先看一道面试题:ajax
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
复制代码
打印顺序:
'script start'
'script end'
'promise1'
'promise2'
'setTimeout'
复制代码
为何呢?由于Promise里有了一个一个新的概念:microtask
。 或者,进一步,JS中分为两种任务类型:macrotask
和microtask
,在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部分。 再根据线程来理解下:
- macrotask中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护
- microtask中的全部微任务都是添加到微任务队列(Job Queues)中,等待当前macrotask执行完毕后执行,而这个队列由JS引擎线程维护 (这点由本身理解+推测得出,由于它是在主线程下无缝执行的) 因此,总结下运行机制:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程当中若是遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,当即执行当前微任务队列中的全部微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,而后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)