标签:Event Loop iplas
浏览器多线程机制介绍
浏览网页相信对于大家来说已经是一件习以为常的事情了,那么在开始今天的分享之前,先卖个关子。你们知道浏览器是怎么渲染页面的吗?它是多线程的吗?
在这里科普一下,浏览器确实是多线程的。请看下图,浏览器至少有以下现成组成:
浏览器页面渲染流程
浏览器从HTTP服务器获取html文档,到呈现页面给用户,会经过以下几个步骤:
1、解析文档构建DOM树
浏览器的解析内容可以分为三个部分
HTML/XHTML/SVG:解析这三种文件后,会生成DOM树(DOM Tree)
CSS:解析样式表,生成CSS规则树(CSS Rule Tree)
JavaScript:解析脚本,通过DOM API和CSSOM API操作DOM Tree和CSS Rule Tree,与用户进行交互。
2、构建渲染树
解析文档完成后,浏览器引擎会将 CSS Rule Tree 附着到DOM Tree 上,并根据DOM Tree 和 CSS Rule Tree构造 Rendering Tree(渲染树)。
3、布局与绘制渲染树
解析position, overflow, z-index等等属性,计算每一个渲染树节点的位置和大小,此过程被称为reflow。最后调用操作系统的Native GUI API完成绘制(repain)。
讲到这里,可能有些同学会有疑惑,在印象中,浏览器明明是单线程的呀,怎么又变成多线程了?我们要区分一下概念,单线程处理的是js引擎,不是浏览器。在任何时候,js引擎都是单线程的。且需要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起。
举个例子,编写一个简单的html页面如下:
Document
body render!
head中加载的脚本文件内容为:
alert('i am the script');
在浏览器中运行时,会先弹出提示框,此时,body元素并没有进行渲染。
关闭弹出框以后,才完成对body的渲染
我们经常在浏览网页时候,经常会遇到一个情况,显示空白加载不出东西来,或是在点击请求时候一直loading个不停。可能某些人没有遇到,反正小编是遇到了不少,这让小编很是郁闷。这是为什么呢?打开浏览器控制台一看才发现,原来是js运行报错了,导致页面渲染停止。这是一个很不友好的情况,这也是各种建议js放在尾部加载而不在头部加载的原因了,至少它可以先让页面渲染出来。
这个时候可能又有人问,我经常在js里面编写各种异步请求的代码,它如果是单线程的,又是怎么处理异步请求的呢?
不要急,不要慌,接下来才开始本次分享的重点讲解。
JavaScript的setTimeout与setInterval是两个很容易欺骗别人感情的方法,因为我们开始常常以为调用了就会按既定的方式执行, 我想不少人都深有同感, 例如
setTimeout( function(){ alert(’你好!’); } , 0);
setInterval( callbackFunction , 100);
执行这两个语句会发生什么呢???
相信不少人跟小编一样认为setTimeout中的问候方法会立即被执行,因为这并不是凭空而说,而是JavaScript API文档明确定义第二个参数意义为隔多少毫秒后,回调方法就会被执行. 这里设成0毫秒,理所当然就立即被执行了.
同理对setInterval的callbackFunction方法每间隔100毫秒就立即被执行深信不疑!
但随着JavaScript应用开发经验不断的增加和丰富,有一天你发现了一段怪异的代码而百思不得其解:
div.onclick = function(){
setTimeout( function(){document.getElementById(’inputField’).focus();}, 0);
};
既然是0毫秒后执行,那么还用setTimeout干什么, 此刻, 坚定的信念已开始动摇。
直到最后某一天 , 你不小心写了一段糟糕的代码:
setTimeout( function(){ while(true){} } , 100);
setTimeout( function(){ alert(’你好!’); } , 200);
setInterval( callbackFunction , 200);
第一行代码不出所料的进入了死循环,但很快你就会发现,第二、第三行代码并没有如意料中的执行下去,,alert问候未见出现,callbacKFunction也杳无音讯!小编心里着急啊,这是为什么呢?
真相只有一个
出现上面所有误区的最主要一个原因是:潜意识中认为,JavaScript引擎有多个线程在执行,JavaScript的定时器回调函数是异步执行的.
而事实上,JavaScript使用了障眼法,在多数时候骗过了我们的眼睛,这里背光得澄清一个事实:
JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序.
JavaScript引擎用单线程运行也是有意义的,单线程不必理会线程同步这些复杂的问题,问题得到简化。
那么单线程的JavaScript引擎是怎么配合浏览器内核处理这些定时器和响应浏览器事件的呢?
下面结合浏览器内核处理方式简单说明.
浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步.假如某一浏览器内核的实现至少有三个常驻线程:javascript引擎线程,界面渲染线程,浏览器事件触发线程,除些以外,也有一些执行完就终止的线程,如Http请求线程,这些异步线程都会产生不同的异步事件,下面通过一个图来阐明单线程的JavaScript引擎与另外那些线程是怎样互动通信的.虽然每个浏览器内核实现细节不同,但这其中的调用原理都是大同小异.
由图可看出,浏览器中的JavaScript引擎是基于事件驱动的,这里的事件可看作是浏览器派给它的各种任务,这些任务可以源自JavaScript引擎当前执行的代码块,如调用setTimeout添加一个任务,也可来自浏览器内核的其它线程,如界面元素鼠标点击事件,定时触发器时间到达通知,异步请求状态变更通知等.从代码角度看来任务实体就是各种回调函数,JavaScript引擎一直等待着任务队列中任务的到来.由于单线程关系,这些任务得进行排队,一个接着一个被引擎处理.
上图t1-t2..tn表示不同的时间点,tn下面对应的小方块代表该时间点的任务,假设现在是t1时刻,引擎运行在t1对应的任务方块代码内,在这个时间点内,我们来描述一下浏览器内核其它线程的状态.
t1时刻:
GUI渲染线程:
该线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行.本文虽然重点解释JavaScript定时机制,但这时有必要说说渲染线程,因为该线程与JavaScript引擎线程是互斥的,这容易理解,因为JavaScript脚本是可操纵DOM元素,在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致了.
在JavaScript引擎运行脚本期间,浏览器渲染线程都是处于挂起状态的,也就是说被”冻结”了。
所以,在脚本中执行对界面进行更新操作,如添加结点,删除结点或改变结点的外观等更新并不会立即体现出来,这些操作将保存在一个队列中,待JavaScript引擎空闲时才有机会渲染出来.
GUI事件触发线程:
JavaScript脚本的执行不影响html元素事件的触发,在t1时间段内,首先是用户点击了一个鼠标键,点击被浏览器事件触发线程捕捉后形成一个鼠标点击事件,由图可知,对于JavaScript引擎线程来说,这事件是由其它线程异步传到任务队列尾的,由于引擎正在处理t1时的任务,这个鼠标点击事件正在等待处理.
定时触发线程:
注意这里的浏览器模型定时计数器并不是由JavaScript引擎计数的,因为JavaScript引擎是单线程的,如果处于阻塞线程状态就计不了时,它必须依赖外部来计时并触发定时,所以队列中的定时事件也是异步事件.
由图可知,在这t1的时间段内,继鼠标点击事件触发后,先前已设置的setTimeout定时也到达了,此刻对JavaScript引擎来说,定时触发线程产生了一个异步定时事件并放到任务队列中, 该事件被排到点击事件回调之后,等待处理.
同理, 还是在t1时间段内,接下来某个setInterval定时器也被添加了,由于是间隔定时,在t1段内连续被触发了两次,这两个事件被排到队尾等待处理。
可见,假如时间段t1非常长,远大于setInterval的定时间隔,那么定时触发线程就会源源不断的产生异步定时事件并放到任务队列尾而不管它们是否已被处理,但一旦t1和最先的定时事件前面的任务已处理完,这些排列中的定时事件就依次不间断的被执行,这是因为,对于JavaScript引擎来说,在处理队列中的各任务处理方式都是一样的,只是处理的次序不同而已.
t1过后,也就是说当前处理的任务已返回,JavaScript引擎会检查任务队列,发现当前队列非空,就取出t2下面对应的任务执行,其它时间依此类推,由此看来:
如果队列非空,引擎就从队列头取出一个任务,直到该任务处理完,即返回后引擎接着运行下一个任务,在任务没返回前队列中的其它任务是没法被执行的.
讲到这里,稍微有点儿清晰了?
setTimeout、setInterval方法在执行的时候,触发生成了一个Event(事件)到任务队列中列中,Javascript引擎有序的执行。
深入了解
现在我们已经知道了Javascript引擎是基于事件驱动的单线程工作引擎,用户的各类操作行为(包括单击、双击、滚动等),setTimeout、setInterval定时任务以及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
为啥是这样的结果?看起来有点二让人懵逼。
这是为什么呢?
要理解这些你首先需要对事件循环机制处理宏任务和微任务的方式有了解。
如果是第一次接触信息量会有点大。深呼吸……
因为Javascript 在浏览器端运行是单线程的,称之为主线程(HTML5提供了web worker API可以让浏览器开一个线程运行比较复杂耗时的 javascript任务,但是这个线程仍受主线程的控制)。如果我们做一些“sleep”的操作比如说
var now = + new Date()
while (+new Date() <= now + 1000){
//这是一个耗时的操所
}
那么在这将近一秒内,线程就会被阻塞,无法继续执行下面的任务。
还有些操作比如说获取远程数据、I/O操作等,他们都很耗时,如果采用同步的方式,那么进程在执行这些操作时就会因为耗时而等待,就像上面那样,下面的任务也只能等待,这样效率并不高。
那浏览器是怎么做的呢?
Event loop: 为了协调事件,用户交互,脚本,渲染,网络等,用户代理必须使用事件循环。
每个线程都会有它自己的Event loop(事件循环),所以都能独立运行。然而所有同源窗口会共享一个event loop以同步通信。event loop会一直运行,来执行进入队列的宏任务。一个event loop有多种的宏任务源(译者注:event等等),这些宏任务源保证了在本任务源内的顺序。但是浏览器每次都会选择一个源中的一个宏任务去执行。这保证了浏览器给与一些宏任务(如用户输入)以更高的优先级。好的,跟着我继续……
为了更清晰的理解,需先了解宏任务、微任务的概念。
宏任务(task)
浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->...)。鼠标点击会触发一个事件回调,需要执行一个宏任务,然后解析HTMl。还有setTimeout,它的作用是等待给定的时间后为它的回调产生一个新的宏任务。这就是为什么打印‘setTimeout’在‘script end’之后。因为打印‘script end’是第一个宏任务里面的事情,而‘setTimeout’是另一个独立的任务里面打印的。
微任务(Microtasks )
微任务通常来说就是需要在当前 task 执行结束后立即执行的任务,比如对一系列动作做出反馈,又或者是需要异步的执行任务而又不需要分配一个新的 task,这样便可以减小一点性能的开销。只要执行栈中没有其他的js代码正在执行且每个宏任务执行完,微任务队列会立即执行。如果在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,之后也会被执行。微任务包括了mutation observe的回调还有接下来的例子promise的回调。一旦一个pormise有了结果,或者早已有了结果(有了结果是指这个promise到了fulfilled或rejected状态),他就会为它的回调产生一个微任务,这就保证了回调异步的执行即使这个promise早已有了结果。所以对一个已经有了结果的promise调用.then(yey, nay)会立即产生一个微任务。这就是为什么‘promise1’,'promise2'会打印在‘script end’之后,因为所有微任务执行的时候,当前执行栈的代码必须已经执行完毕。‘promise1’,'promise2'会打印在‘setTimeout’之前是因为所有微任务总会在下一个宏任务之前全部执行完毕。
上图直观的展示了宏任务与微任务的先后执行情况。
消化了这些后,对于script start, script end, promise1, promise2, setTimeout这样一个执行结果是否也就清晰明了了呢?
难度晋级
明白了宏任务与微任务后挑战一下吧。
请看下面代码:
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
// Let's listen for attribute changes on the
// outer element
//监听element属性变化
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});
// Here's a click listener…
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
这时如果我点击div.inner会打印什么?
有兴趣的同学可以自己尝试一下,结果是:
click
promise
mutate
click
promise
mutate
timeout
timeout。
demo出处:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/?utm_source=html5weekly
如果在前面例子的js末端添加一句
inner.click();
又会发生什么呢?
有兴趣的同学可以自己实验一下。也可以查看原文获取更加详细的解释
关于堆栈与微任务、宏任务的关系有兴趣的同学可以点击下面链接了解一下。
https://juejin.im/post/5b1deac06fb9a01e643e2a95
解答:ajax异步请求是否真的异步?
很多同学朋友搞不清楚,既然说JavaScript是单线程运行的,那么XMLHttpRequest在连接后是否真的异步?
其实请求确实是异步的,不过这请求是由浏览器新开一个线程请求(参见上图),当请求的状态变更时,如果先前已设置回调,这异步线程就产生状态变更事件放到JavaScript引擎的处理队列中等待处理,当任务被处理时,JavaScript引擎始终是单线程运行回调函数,具体点即还是单线程运行onreadystatechange所设置的函数.
解答前文中setTimeout( Function, 0)作用
http://www.cnblogs.com/winner/archive/2008/11/15/1334077.html
参考:http://www.cnblogs.com/hksac/p/6596105.html
https://www.cnblogs.com/woodyblog/p/6061671.html
http://www.cnblogs.com/winner/archive/2008/11/15/1334077.html
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/?utm_source=html5weekly