这两天看了 Event Loop 相关的技术文章,写了一个测试代码,发现测试结果并不稳定,可能会因为一些变量而导致不同的结果。不知道是浏览器实现的问题还是测试代码的问题。
参考技术文章
为了后面方便引用,给参考文章加上编号
JavaScript 运行机制详解:再谈Event Loop - $RYF
并发模型与Event Loop - MDN - $MDN
首先贴代码:
<!DOCTYPE html>
<html lang="zh">
<head>
<title>Event Loop Test</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<script>
setTimeout(function() {
console.log('setTimeout延迟完成,执行回调:',Date.now());
}, 10);
console.log('setTimeout方法调用完成:',Date.now());
var req = new XMLHttpRequest();
req.open('GET', 'http://cdn.bootcss.com/jquery/1.11.3/jquery.min.js');
req.onload = function () {
console.log('XHR请求完成,执行回调:',Date.now());
};
req.send();
console.log('XHR请求发出:',Date.now());
console.time('同步任务延迟完成时间:');
var arr = [],max = 3000000;
for (var i = 1; arr.push(i++) < max;);
var mapArr = [];
arr.map(function (val) {
mapArr.push(val);
})
console.timeEnd('同步任务延迟完成时间:');
console.log('第一个<script>标签,同步任务结束时间:',Date.now());
</script>
<script>
console.log('另外一个<script>标签:',Date.now());
</script>
</body>
</html>
测试环境
Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3041.0 Safari/537.36
运行了很多次,把比较典型的三种结果贴出来:
测试结果一: XHR加载jQuery用时503ms
setTimeout方法调用完成: 1489654050312
javascript-temp.html:28 XHR请求发出: 1489654050833
javascript-temp.html:37 同步任务延迟完成时间:: 2043.388916015625ms
javascript-temp.html:38 第一个<script>标签,同步任务结束时间: 1489654052877
javascript-temp.html:25 XHR请求完成,执行回调: 1489654052881
javascript-temp.html:42 另外一个<script>标签: 1489654052882
javascript-temp.html:18 setTimeout延迟完成,执行回调: 1489654054058
测试结果二: XHR加载jQuery用时1.36s
setTimeout方法调用完成: 1489664224372
javascript-temp.html:27 XHR请求发出: 1489664224375
javascript-temp.html:36 同步任务延迟完成时间:: 1480.636962890625ms
javascript-temp.html:37 第一个<script>标签,同步任务结束时间: 1489664225856
javascript-temp.html:17 setTimeout延迟完成,执行回调: 1489664225856
javascript-temp.html:24 XHR请求完成,执行回调: 1489664225861
javascript-temp.html:41 另外一个<script>标签: 1489664225862
测试结果三: XHR加载jQuery用时3.94s
setTimeout方法调用完成: 1489653980367
javascript-temp.html:28 XHR请求发出: 1489653980382
javascript-temp.html:37 同步任务延迟完成时间:: 1856.004150390625ms
javascript-temp.html:38 第一个<script>标签,同步任务结束时间: 1489653982239
javascript-temp.html:42 另外一个<script>标签: 1489653982243
javascript-temp.html:18 setTimeout延迟完成,执行回调: 1489653982330
javascript-temp.html:25 XHR请求完成,执行回调: 1489653984326
我的具体问题:
-
第二个
<script>
标签中的同步代码的执行结果为什么排在异步回调之后?$RYF 文章中第四章讲到:
执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。
-
setTimeout()
的回调函数为什么会在主进程(另外一个<script>
标签中的同步代码)和异步任务(XHR
请求 jQuery 文件)之前调用?$RYF 文章中第五章讲到:
setTimeout(fn,0)
的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。$MDN 中的 事件循环 -> 添加消息 段落讲到:
调用 setTimeout 函数会在一个时间段过去后在队列中添加一个消息。这个时间段作为函数的第二个参数被传入。如果队列中没有其它消息,消息会被马上处理。
难道测试结果二和三种,
setTimeout()
的回调函数出现在XHR
的回调函数之前是因为当时消息队列中为空吗?
-
往消息队列中添加消息是在事件触发的时候嘛?
例如,通过
XHR
请求 jQuery 资源,待 jQuery 文件加载完成会触发“加载完成事件”,这时候会在消息队列中添加一个消息,等待主进程读取并调用对应的回调函数。
为什么 jQuery 资源加载时间的变化,会引起另外一个
<script>
标签中的同步代码、XHR
的回调以及setTimeout()
方法的回调,这三段代码执行先后的变化?
如有描述错误地方,请指正。。
--------------------2017/3/17分割线--------------------------
为各位补充一篇我今天找到的关于浏览器工作原理的文章:
浏览器的工作原理:新式网络浏览器幕后揭秘
如果连接无法打开,可以访问这个备用的:
印象笔记
说下我的理解吧,先明确几个基本概念
js引擎
只负责实现ecmascript
标准,按照标准执行代码,它不关心也不知道event loop
的存在,你给它什么代码它就执行什么event loop
是js运行环境
内部使用的机制,运行环境也就是Node
以及各种各样的浏览器
js引擎
执行代码时会有一个执行栈
,执行栈中的代码执行完毕后,浏览器(JS运行环境)
才会从event queue
中取出事件,如果该事件有对应的callback
,则再次交给js引擎
执行。所有的事件在产生后都会被浏览器放到
event queue
中,事件可以来源于鼠标键盘、网络IO、定时器等。event queue
一般是由多个不同优先级
的队列组成,分别对应不同类型的事件,具体的细节由实现者自己决定。现在,根据你描述的问题,我尝试还原一下整个过程。
浏览器取得
HTML
文件后,做的第一件事就是解析HTML
(它内部有另外的模块来做这件事),构建DOMTree
,当遇到script
标签,会停止解析,交给js引擎
执行标签内的javascript
代码块,这就是我们通常说的js
会阻塞页面渲染。 此时js引擎
执行栈中只有第一个script
标签内的代码。一旦这段代码执行完毕,浏览器会检查event queue
,这个时候,根据event queue的情况以及浏览器自身的实现策略就可能会有不同的结果。浏览器可能会按照先后顺序,或者它预定的优先级依次取出全部事件给js引擎执行
callback
,还有可能为了加快页面渲染速度,只取出部分高优先级事件。最后浏览器继续解析页面,遇到下一个
script
标签再次交给js引擎
执行代码我在Chrome57下多次执行了你的代码,只会出现第1,3种情况
这很好理解,当第一段
script
执行完时,chrome
取出事件时,忽略低优先级的timeout
事件,如果已经有xhr
事件则取出,没有就继续解析HTML
,碰到第二个script
再次用js引擎
执行。最后,已上纯属个人见解。有些概念其实依赖于具体的实现的,不同浏览器的差异可能会导致表现出来的行为就不一样,要深入的了解细节只能去看它们的源码了。。。