Event Loop 中异步任务的疑问◔ ‸◔?

这两天看了 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

我的具体问题:

  1. 第二个<script>标签中的同步代码的执行结果为什么排在异步回调之后?

    • $RYF 文章中第四章讲到:

    • 执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。

  2. setTimeout()的回调函数为什么会在主进程(另外一个<script>标签中的同步代码)和异步任务(XHR请求 jQuery 文件)之前调用?

    • $RYF 文章中第五章讲到:

    • setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。

    • $MDN 中的 事件循环 -> 添加消息 段落讲到:

    • 调用 setTimeout 函数会在一个时间段过去后在队列中添加一个消息。这个时间段作为函数的第二个参数被传入。如果队列中没有其它消息,消息会被马上处理。

    • 难道测试结果二和三种,setTimeout()的回调函数出现在XHR的回调函数之前是因为当时消息队列中为空吗?

  3. 往消息队列中添加消息是在事件触发的时候嘛?

    • 例如,通过XHR请求 jQuery 资源,待 jQuery 文件加载完成会触发“加载完成事件”,这时候会在消息队列中添加一个消息,等待主进程读取并调用对应的回调函数。

  4. 为什么 jQuery 资源加载时间的变化,会引起另外一个<script>标签中的同步代码、XHR的回调以及setTimeout()方法的回调,这三段代码执行先后的变化?

如有描述错误地方,请指正。。

--------------------2017/3/17分割线--------------------------

为各位补充一篇我今天找到的关于浏览器工作原理的文章:
浏览器的工作原理:新式网络浏览器幕后揭秘

如果连接无法打开,可以访问这个备用的:
印象笔记

阅读 5k
5 个回答

说下我的理解吧,先明确几个基本概念

  1. js引擎只负责实现ecmascript标准,按照标准执行代码,它不关心也不知道event loop的存在,你给它什么代码它就执行什么

  2. event loopjs运行环境内部使用的机制,运行环境也就是Node以及各种各样的浏览器

  3. js引擎执行代码时会有一个执行栈,执行栈中的代码执行完毕后,浏览器(JS运行环境)才会从event queue中取出事件,如果该事件有对应的callback,则再次交给js引擎执行。

  4. 所有的事件在产生后都会被浏览器放到event queue中,事件可以来源于鼠标键盘、网络IO、定时器等。event queue一般是由多个不同优先级的队列组成,分别对应不同类型的事件,具体的细节由实现者自己决定。

现在,根据你描述的问题,我尝试还原一下整个过程。

  1. 浏览器取得HTML文件后,做的第一件事就是解析HTML(它内部有另外的模块来做这件事),构建DOMTree,当遇到script标签,会停止解析,交给js引擎执行标签内的javascript代码块,这就是我们通常说的js会阻塞页面渲染。 此时js引擎执行栈中只有第一个script标签内的代码。一旦这段代码执行完毕,浏览器会检查event queue,这个时候,根据event queue的情况以及浏览器自身的实现策略就可能会有不同的结果。

  2. 浏览器可能会按照先后顺序,或者它预定的优先级依次取出全部事件给js引擎执行callback,还有可能为了加快页面渲染速度,只取出部分高优先级事件。

  3. 最后浏览器继续解析页面,遇到下一个script标签再次交给js引擎执行代码

我在Chrome57下多次执行了你的代码,只会出现第1,3种情况
图片描述
图片描述

这很好理解,当第一段script执行完时,chrome取出事件时,忽略低优先级的timeout事件,如果已经有xhr事件则取出,没有就继续解析HTML,碰到第二个script再次用js引擎执行。

最后,已上纯属个人见解。有些概念其实依赖于具体的实现的,不同浏览器的差异可能会导致表现出来的行为就不一样,要深入的了解细节只能去看它们的源码了。。。

先贴个有相关性的问题链接
1.第一个问题还是比较好理解的,很可能是在第二个script还未被解析(先解析后执行)的时候,第一个script中的异步回调被触发了。此时执行栈是空的。
2.前一半同问题一,后一半,异步回调的执行顺序跟建立的顺序无关,谁先触发,谁就在前面。
3.我觉得是对的。
4.等别的回答。
也得纠正上面的错误。

这是一个蛮有意思的问题,但追根到底其实就是一个事件驱动的问题。

对于浏览器而言,setTimeout 消息和 XMLHttpRequest AJAX消息会被当作两种不同的消息放在消息队列里面。

消息,就会有众多因素而倒置输出的顺序不可预见!

既然如此,那么如果我们把两个消息拿掉,那么你的三个执行顺序全是正常的,这样第一个问题就迎刃而解。

以下来自MDN的一张Event Loop的图式:

图片描述

其中 Queue 由浏览器维护,换句话说队列中的消息是由浏览器来放入的。

但这里有一个前置条件,就是不管是哪种消息,都遵循一种CPU TICK的时间限定,大概就是4ms(如果使用的是电池且量底的情况下可能会更长)。

因此,不知道题主有没有注意到一个细节,即两个消息的时间不可能会存在相隔1ms的情况,即:

javascript-temp.html:17 setTimeout延迟完成,执行回调: 1489664225856
javascript-temp.html:24 XHR请求完成,执行回调: 1489664225861
// 二者的时间不会存在 1489664225856 和 1489664225857

而对于Event Loop中的消息处理是不存在这种因素:

javascript-temp.html:25 XHR请求完成,执行回调: 1489654052881
javascript-temp.html:42 另外一个<script>标签: 1489654052882
// 当一个消息放进后,主线程的立即开始

不管是什么的消息,都是依赖于浏览器的事件驱动,那么这一话题就会伸到不同浏览器之间的差异。假如你把测试代码放在 Chrome、IE、Safari 等浏览器下,所执行的结果也和 Firefox 不同。更甚至,可能你把 <script><body> 迁移到 <head> 里面,也可能会引发不同的结果。

说了,那么多,我也没办法提供一个准确的答案!

推荐问题
宣传栏