一些计算机概念
CPU
中央处理器,负责运算和控制。
- 核
物理线程。多核CPU才能支持线程并行,否则只能并发。 - 逻辑CPU
通过超线程技术,单个核可以支持多个逻辑处理器(通常为1~2),每个逻辑处理器可以并行执行一个线程。 - vCPU
虚拟处理器,即虚拟机内可以并行的线程数。等于CPU数×核数×超线程数。
GPU
图形处理器,更擅长利用多核心同时处理单一的任务,在图像处理方面有优势
进程和线程
- 操作系统(OS)会为进程分配cpu和内存,进程是最小的资源分配单位
- 每个进程至少包含一个线程,线程是资源调度的基本单位
- 同一进程的线程之间共享进程中的资源(数据、内存等)。
不同进程之间数据通常相互隔离,如果需要通信,则需要使用IPC(Inter-Process Communication)技术 - 当一个进程关闭之后,操作系统会回收进程所占用的内存(包括因操作不当导致的内存泄漏)
串行 并行 并发 协程
- 串行 多个任务,执行时一个执行完再执行另一个。
- 并行 多个任务同时执行。
- 并发 一个CPU同时只能执行一个进程,其多个核心可以分别执行一个线程。系统不停切换线程,看起来像同时运行
- 协程 进程和线程占用内存大,且需通过CPU调度切换,切换过程耗时长。而协程并不增加线程数,切换代价小。
1、单CPU中进程只能是并发,多CPU中进程可以并行。
2、单CPU单核中线程只能并发,单CPU多核中线程可以并行。
浏览器是多进程的
通过Chrome的更多工具 -> 任务管理器 可以查看进程信息
通常每个网页的渲染进程(Renderer)和每种第三方插件占用一个进程,所有网页公用一个Browser主进程(前进、后退、下载等)和GPU绘图进程
网页的渲染进程(Render)
1. GUI渲染线程
又称 CRP关键渲染路径 Critical Rendering Path
- 解析HTML并生成DOM树+下载解析CSS并生成CSSOM树
-
link
标签当媒体查询不符合条件时会变成异步加载,不会阻塞渲染
-
-
DOM
和CSSOM
都解析完毕后,构建RenderObject
渲染树
(其中display:none
的元素不会进入渲染树,而visibility:hidden
会进入) - Layout(布局)
根据RenderObject渲染树和设备视口(viewport)大小计算出各DOM节点的位置、大小的像素值。 - Paint(绘制)
在多个层上分别进行DOM 元素的绘制 - 渲染层合并 (Composite)
之前步骤都在CPU中完成后,浏览器主进程将默认的图层和复合图层交给 GPU ,将各个图层合成(composite),最后显示出页面
- GUI线程同时解析DOM和下载并解析CSS(两者独立并行、互不阻塞),当遇到JS时被阻塞,转入JS线程下载并执行脚本。脚本执行完毕后回到GUI线程。
-
DOM
和CSSOM
都解析完毕后才会进行渲染,现代浏览器在GUI被JS阻塞时会将已有的GUI部分先显示,称为First Paint
- 整个HTML文档(包括JS)解析完毕后触发
DOMContentLoaded
事件,然后等媒体资源(图片,音频,视频,iframe等)都加载完毕后再触发onload
事件。
document.addEventListener('DOMContentLoaded', function () {});
window.onload = function(){}
2. JS引擎线程
一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
JS是单线程的,因为其设计用于进行用户操作和DOM交互,避免多线程在操作同一DOM时出现冲突,并需要引入锁等复杂概念
GUI 渲染线程与 JS 引擎线程是互斥的。执行JS线程时GUI线程会被暂时挂起,执行GUI线程时JS线程会被暂时挂起。因为如果 JS线程和 GUI线程同时运行,那么渲染线程前后获得的元素数据就可能不一致了
script标签属性作用
-
defer
异步加载,并在所有元素解析完成之后,DOMContentLoaded
事件触发前执行。 -
async
加载和执行都变成异步。
如果script标签是由JavaScript代码创建的,标签的async属性会默认为true
3. 事件触发线程
来自浏览器内核的事件及JS引擎中的异步任务会被添加进事件触发线程,当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎线程的处理
4. 定时触发器线程
5. 异步http请求线程
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
6. Web Worker 线程 (IE11以上支持)
JS引擎线程向浏览器申请开一个子线程(仅能通过.postMessage()
与JS线程交互,而且不能操作DOM)用于处理复杂JS,防止阻塞页面。
逻辑完成后应在主线程中调用.terminate()
或Worker中调用close()
方法关闭Worker以释放资源,否则会一直占用
postMessage传递对象时仅传值不传址(先将通信内容串行化,然后把串行化后的字符串发给 Worker,再还原。)
- 主线程中
- 引用的Web Worker的脚本文件必须和主线程同源
- 通过
.postMessage()
发送消息,通过.onmessage()
接收消息 - 通过
.onerror(function (event) {})
或者.addEventListener('error', function (event) {})
可监听Worker中的错误
var worker = new Worker('test.js');
worker.postMessage('Hello World');
worker.postMessage({ method: 'echo', args: ['Work'] });
worker.onmessage = function (event) {
console.log('Received message ' + event.data);
worker.terminate();
}
- Worker线程中
- Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用
document
、window
、parent
等(没有alert
和confirm
方法),仅有navigator
对象和location
对象。 - 全局对象可用
self
或this
表示,也可直接省略(同主线程中的window
) - 通过
addEventListener('message',function(e){})
或onmessage()
监听主线程推送的消息,或通过postMessage()
推送消息 - 通过
importScripts()
加载其他js脚本 - Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
- Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用
// 写法零
self.addEventListener('message', function (e) {
self.postMessage('You said: ' + e.data);
}, false);
// 写法一
this.addEventListener('message', function (e) {
this.postMessage('You said: ' + e.data);
}, false);
// 写法二
addEventListener('message', function (e) {
postMessage('You said: ' + e.data);
}, false);
Event Loop
同步任务都在JS线程上执行,形成一个执行栈。
JS线程之外,事件触发线程管理着一个任务队列,异步任务完成后会将其回调加入任务队列。
一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
定时器线程会在倒计时完成时将任务加入任务队列,但任务的执行依然得等到JS线程空闲,因此JS通过计时器执行任务是不准确的
macrotask与microtask
macrotask(又称之为宏任务或task)
包括每次执行的 主代码块脚本执行、渲染事件(如解析/绘制DOM)、setTimeout
、setInterval
、postMessage
、setImmediate
、用户交互事件(如鼠标点击)、I/O相关(如XMLHttpRequest
是网络I/O)等
每一个task会从头到尾将这个任务执行完毕,不会被打断
浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
队列由事件触发线程维护(会进入任务队列)-
microtask(又称为微任务或jobs)
process.nextTick
(高于其他微任务)、Promise.then catch finally
(注意不是 Promise主代码块)、MutationObserver
、被await
阻塞的语句等。
在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕。在处理microtask期间,如果有新添加的microtasks,也会被添加到当前微任务队列的末尾
队列由JS引擎线程维护(不进入任务队列,有自己专门的微任务队列)- Promise的polyfill,一般都是通过setTimeout模拟的,所以是macrotask形式
setTimeout(function(){
console.log(1)
},0);
new Promise(function(resolve){
console.log(2)
for( var i=100000 ; i>0 ; i-- ){
i==1 && resolve()
}
console.log(3)
}).then(function(){
console.log(4)
});
console.log(5);
// 2 3 5 4 1
setTimeout和setInterval
- setTimeout 过指定时间将回调函数加入队列
- setInterval 每过指定时间将回调函数加入队列
- 把浏览器最小化显示等操作时,
setInterval
的回调函数依然会进入队列,等浏览器窗口再次打开时,一瞬间全部执行
部分浏览器会对setInterval
进行优化,如果当前事件队列中有setInterval
的回调,不会重复添加。 - 一般认为的最佳方案是:用
setTimeout
模拟setInterval
以保证两次回调之间的最小时间差,或者特殊场合直接用requestAnimationFrame
- 把浏览器最小化显示等操作时,
- V8中用 32 个 bit 来存储延时值,其最大能存放的数字是 2147483647,因此如果设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,定时器会被立即执行。
requestAnimationFrame和requestIdleCallback
- requestAnimationFrame 在每次屏幕被刷新前加入调用队列
- requestIdleCallback 在每次屏幕刷新并且空闲时加入调用队列(在
requestAnimationFrame
之后加入)
如requestIdleCallback(fn,{timeout:1000})
即在下次刷新且空闲时加入调用队列,且最迟在1s后加入
注意requestAnimationFrame
和requestIdleCallback
不是微任务,在某种程度上可以理解为宏任务