重点来了,我们可以看到,上面提到了这么多的进程,那么,对于普通的前端操作来说,最终要的是什么呢?答案是渲染进程。因为页面的渲染,JS的执行,事件的触发,都在这个进程内进行的。接下来重点分析这个进程。
之前讲到过,进程一般是多线程的,忘了可以再复习下前面第一节,进程和线程,那么浏览器的渲染进程又包括哪些线程。
1.渲染进程包括哪些线程
- GUI渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
- JS引擎线程(单线程)
- 也称为JS内核,负责处理Javascript脚本程序。(例如常常听到的谷歌浏览器的V8引擎,新版火狐的JaegerMonkey引擎等)
- JS引擎线程负责解析Javascript脚本,运行代码。
- JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
- 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
- 事件触发线程
- 归属于渲染进程而不是JS引擎,用来控制事件轮询(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
- 当JS引擎执行代码块如鼠标点击、AJAX异步请求等,会将对应任务添加到事件触发线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理任务队列的队尾,等待JS引擎的处理
- 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
-
定时触发器线程
- 定时器setInterval与setTimeout所在线程
- 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果任务队列处于阻塞线程状态就会影响记计时的准确)
- 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
- 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
-
异步http请求线程
- 用于处理请求XMLHttpRequest,在连接后是通过浏览器新开一个线程请求。如ajax,是浏览器新开一个http线程
-
将检测到状态变更(如ajax返回结果)时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入js引擎线程的事件队列中。再由JavaScript引擎执行。
知道了这几个线程,那么通过这几个线程,js是怎么执行的呢?
2.渲染进程中的线程之间的关系
GUI渲染线程与JS引擎线程互斥
由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和GUI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起,
GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。
JS阻塞页面加载
从上述的互斥关系,可以推导出,JS如果执行时间过长就会阻塞页面。
譬如,假设JS引擎正在进行巨量的计算,所以JS引擎很可能很久很久后才能空闲,所以导致页面渲染加载阻塞。这就牵扯到script标签在html中的存放位置。具体可以看我另一篇文章 为什么script标签一般放在body下面
3.js引擎是单线程的
我们知道js是单线程的。也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
参考阮一峰大神的文章js事件轮询(Event Loop)
- JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
- 所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
- 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
4.js事件轮询
上面我们已经知道JS引擎是单线程,任务应该是按顺序执行的,那么怎么会有同步异步之说?
- 单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
- 如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
- JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
- 于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
理解了同步异步。其实其最本质原因就是基于js的事件轮询机制。
- 所有同步任务都在主线程(即js引擎线程)上执行,形成一个执行栈
- 而异步任务均由事件触发线程控制,其有一个任务队列。只要异步任务有了运行结果,就在"任务队列"之中放置回调事件。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。所以所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",按顺序结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步
- 只要主线程空了,就会去读取"任务队列",这个过程会不断重复。这就是JavaScript的运行机制。又称为Event Loop(事件循环或者轮询)。
5.定时器触发线程
上述事件循环机制的核心是:JS引擎线程和事件触发线程
js来控制主线程,事件触发来控制任务队列就如主线程。
为什么要单独的定时器线程?因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确,因此很有必要单独开一个线程用来计时。
什么时候会用到定时器线程?当使用setTimeout或setInterval时,它需要定时器线程计时,计时完成后就会将特定的事件推入事件触发线程的任务队列中。等待进入主线程执行。
譬如:
setTimeout(function(){
console.log('hello!');
}, 1000);
这段代码的作用是当1000毫秒计时完毕后(由定时器线程计时),将回调函数推入事件队列中,等待主线程执行
setTimeout(function(){
console.log('hello!');
}, 0);
console.log('begin');
//begin hello
这段代码的效果是表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。
注意:
- 虽然代码的本意是0毫秒后就推入事件队列,但是html5标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
- 就算不等待4ms,就算假设0毫秒就推入事件队列,也会先执行begin(因为只有主线程可执行栈内空了后才会主动读取事件队列)。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。同理setInterval则是每次都精确的隔一段时间推入一个事件(但是,事件的实际执行时间不一定就准确,还有可能是这个事件还没执行完毕,下一个事件就来了)
6.总结
这里我没有总结css渲染这块,由于内容较多,我会另开一篇文章来讲解。
写了这么多,还是要感谢大神们的文章。