浏览器的进程、线程、Web Worker及Event Loop
进程和线程
系统会为进程分配cpu和内存
每个进程至少包含一个线程
不同进程之间也可以通信,不过代价较大
浏览器是多进程的
通过Chrome的更多工具 -> 任务管理器 可以查看进程信息
通常每个网页的渲染进程(Renderer)和每种第三方插件占用一个进程,所有网页公用一个Browser主进程(前进、后退、下载等)和GPU绘图进程
其中CSS由下载线程进行下载,不会阻塞DOM树解析,但会阻塞render树渲染
网页的渲染进程(Render)
- GUI线程
负责绘制、回流、重绘。注意GUI线程不会和JS线程同时执行,执行JS时GUI会被暂时挂起 - JS引擎线程
一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序 - 事件触发线程
来自浏览器内核的事件及JS引擎中的异步任务会被添加进事件触发线程,当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎线程 的处理 - 定时触发器线程
- 异步http请求线程
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
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);
复合图层(硬件加速)
dom改变会触发当前复合层的回流重绘,默认情况下所有dom处于同一复合层。
复合图层会占用额外资源。
通过硬件加速可以创建新的复合层,以提高重绘效率。
-
硬件加速通常有如下方法:
- translate3d、translateZ
- opacity属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
- <video><iframe><canvas><webgl>等元素
- 其它,譬如以前的flash插件
-
硬件加速时请使用index
webkit CSS3中,如果这个元素添加了硬件加速,并且index层级比较低,
那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的),
会默认变为复合层渲染,如果处理不当会极大的影响性能
Event Loop
同步任务都在JS线程上执行,形成一个执行栈。
JS线程之外,事件触发线程管理着一个任务队列,异步任务完成后会将其回调加入任务队列。
一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
定时器线程会在倒计时完成时将任务加入任务队列,但任务的执行依然得等到JS线程空闲,因此JS通过计时器执行任务是不准确的
macrotask与microtask
task
->process.nextTick
->其他jobs
->渲染
->task
->...
-
macrotask(又称之为宏任务或task)
包括每次执行的主代码块
、setTimeout
、setInterval
、postMessage
、setImmediate
等
每一个task会从头到尾将这个任务执行完毕,不会执行其它
浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
队列由事件触发线程维护(会进入任务队列) -
microtask(又称为微任务或jobs)
process.nextTick
(高于其他微任务)、Promise.then catch finally
(注意我不是说 Promise)、MutationObserver
、被await
阻塞的语句等。
在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前,并在渲染阶段触发requestAnimationFrame
)
在处理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
- 把浏览器最小化显示等操作时,
setInterval
的回调函数依然会进入队列,等浏览器窗口再次打开时,一瞬间全部执行
部分浏览器会对setInterval
进行优化,如果当前事件队列中有setInterval
的回调,不会重复添加。 - 一般认为的最佳方案是:用
setTimeout
模拟setInterval
,或者特殊场合直接用requestAnimationFrame