JS事件循环机制——XMLHttpRequest

回调函数 VS 系统调用栈

那什么是回调函数呢(Callback Function)?

将一个函数作为参数传递给另外一个函数,那作为参数的这个函数就是回调函数。简化的代码如下所示:

1
2
3
4
5
6
7
8
9
let callback = function() {
console.log('i am do homework')
}
function doWork(cb) {
console.log('start do work')
cb()
console.log('end do work')
}
doWork(callback)

在上面示例代码中,我们将一个匿名函数赋值给变量callback,同时将callback作为参数传递给了doWork()函数,这时在函数doWork()callback就是回调函数。

上面的回调方法有个特点,就是回调函数callback是在主函数doWork返回之前执行的,我们把这个回调过程称为同步回调。

既然有同步回调,那肯定也有异步回调。下面我们再来看看异步回调的例子:

1
2
3
4
5
6
7
8
9
let callback = function() {
console.log('i am do homework')
}
function doWork(cb) {
console.log('start do work')
setTimeout(cb,1000)
console.log('end do work')
}
doWork(callback)

在这个例子中,我们使用了setTimeout函数让callbackdoWork函数执行结束后,又延时了 1 秒再执行,这次callback并没有在主函数doWork内部被调用,我们把这种回调函数在主函数外部执行的过程称为异步回调。

现在你应该知道什么是同步回调和异步回调了,那下面我们再深入点,站在消息循环的视角来看看同步回调和异步回调的区别。理解了这些,可以让你从本质上理解什么是回调。

我们还是先来回顾下页面的事件循环系统,浏览器页面是通过事件循环机制来驱动的,每个渲染进程都有一个消息队列,页面主线程按照顺序来执行消息队列中的事件,如执行 JavaScript 事件、解析 DOM 事件、计算布局事件、用户输入事件等等,如果页面有新的事件产生,那新的事件将会追加到事件队列的尾部。所以可以说是消息队列和主线程循环机制保证了页面有条不紊地运行。

这里还需要补充一点,那就是当循环系统在执行一个任务的时候,都要为这个任务维护一个系统调用栈。这个系统调用栈类似于 JavaScript 的调用栈,只不过系统调用栈是 Chromium 的开发语言 C++ 来维护的,其完整的调用栈信息你可以通过chrome://tracing/来抓取。当然,你也可以通过Performance来抓取它核心的调用信息,如下图所示:

这幅图记录了一个 Parse HTML 的任务执行过程,其中黄色的条目表示执行 JavaScript 的过程,其他颜色的条目表示浏览器内部系统的执行过程。

通过该图你可以看出来,Parse HTML 任务在执行过程中会遇到一系列的子过程,比如在解析页面的过程中遇到了 JavaScript 脚本,那么就暂停解析过程去执行该脚本,等执行完成之后,再恢复解析过程。然后又遇到了样式表,这时候又开始解析样式表……直到整个任务执行完成。

需要说明的是,整个 Parse HTML 是一个完整的任务,在执行过程中的脚本解析、样式表解析都是该任务的子过程,其下拉的长条就是执行过程中调用栈的信息。

每个任务在执行过程中都有自己的调用栈,那么同步回调就是在当前主函数的上下文中执行回调函数,这个没有太多可讲的。下面我们主要来看看异步回调过程,异步回调是指回调函数在主函数之外执行,一般有两种方式:

  • 第一种是把异步函数做成一个任务,添加到信息队列尾部;
  • 第二种是把异步函数添加到微任务队列中,这样就可以在当前任务的末尾处执行微任务了。

XMLHttpRequest 运作机制

理解了什么是同步回调和异步回调,接下来我们就来分析XMLHttpRequest背后的实现机制,具体工作过程你可以参考下图:

这是XMLHttpRequest的总执行流程图,下面我们就来分析从发起请求到接收数据的完整流程。

我们先从XMLHttpRequest的用法开始,首先看下面这样一段请求代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function GetWebData(URL){
/**
* 1: 新建 XMLHttpRequest 请求对象
*/
let xhr = new XMLHttpRequest()

/**
* 2: 注册相关事件回调处理函数
*/
xhr.onreadystatechange = function () {
switch(xhr.readyState){
case 0: // 请求未初始化
console.log(" 请求未初始化 ")
break;
case 1://OPENED
console.log("OPENED")
break;
case 2://HEADERS_RECEIVED
console.log("HEADERS_RECEIVED")
break;
case 3://LOADING
console.log("LOADING")
break;
case 4://DONE
if(this.status == 200||this.status == 304) {
console.log(this.responseText);
}
console.log("DONE")
break;
}
}

xhr.ontimeout = function(e) { console.log('ontimeout') }
xhr.onerror = function(e) { console.log('onerror') }

/**
* 3: 打开请求
*/
xhr.open('Get', URL, true);// 创建一个 Get 请求, 采用异步

/**
* 4: 配置参数
*/
xhr.timeout = 3000 // 设置 xhr 请求的超时时间
xhr.responseType = "text" // 设置响应返回的数据格式
xhr.setRequestHeader("X_TEST","time.geekbang")

/**
* 5: 发送请求
*/
xhr.send();
}

上面是一段利用了XMLHttpRequest来请求数据的代码,再结合上面的流程图,我们可以分析下这段代码是怎么执行的。

第一步:创建XMLHttpRequest对象。

当执行到let xhr = new XMLHttpRequest()后,JavaScript 会创建一个XMLHttpRequest对象xhr,用来执行实际的网络请求操作。

第二步:为xhr对象注册回调函数。

因为网络请求比较耗时,所以要注册回调函数,这样后台任务执行完成之后就会通过调用回调函数来告诉其执行结果。

XMLHttpRequest的回调函数主要有下面几种:
ontimeout,用来监控超时请求,如果后台请求超时了,该函数会被调用;
onerror,用来监控出错信息,如果后台请求出错了,该函数会被调用;
onreadystatechange,用来监控后台请求过程中的状态,比如可以监控到 HTTP 头加载完成的消息、HTTP 响应体消息以及数据加载完成的消息等。

第三步:配置基础的请求信息。

注册好回调事件之后,接下来就需要配置基础的请求信息了,首先要通过open接口配置一些基础的请求信息,包括请求的地址、请求方法(是 get 还是 post)和请求方式(同步还是异步请求)。

然后通过xhr内部属性类配置一些其他可选的请求信息,你可以参考文中示例代码,我们通过xhr.timeout = 3000来配置超时时间,也就是说如果请求超过 3000 毫秒还没有响应,那么这次请求就被判断为失败了。

我们还可以通过xhr.responseType = "text"来配置服务器返回的格式,将服务器返回的数据自动转换为自己想要的格式,如果将responseType的值设置为json,那么系统会自动将服务器返回的数据转换为 JavaScript 对象格式。下面的图表是我列出的一些返回类型的描述:

假如你还需要添加自己专用的请求头属性,可以通过xhr.setRequestHeader来添加。

第四步:发起请求。

一切准备就绪之后,就可以调用xhr.send来发起网络请求了。你可以对照上面那张请求流程图,可以看到:渲染进程会将请求发送给网络进程,然后网络进程负责资源的下载,等网络进程接收到数据之后,就会利用 IPC 来通知渲染进程;渲染进程接收到消息之后,会将 xhr 的回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候,就会根据相关的状态来调用对应的回调函数。

如果网络请求出错了,就会执行xhr.onerror
如果超时了,就会执行xhr.ontimeout
如果是正常的数据接收,就会执行onreadystatechange来反馈相应的状态。
这就是一个完整的XMLHttpRequest请求流程。

XMLHttpRequest使用过程中的“坑”
上述过程看似简单,但由于浏览器很多安全策略的限制,所以会导致你在使用过程中踩到非常多的“坑”。

浏览器安全问题是前端工程师避不开的一道坎,通常在使用过程中遇到的“坑”,很大一部分都是由安全策略引起的,不管你喜不喜欢,它都在这里。本来很完美的一个方案,正是由于加了安全限制,导致使用起来非常麻烦。

而你要做的就是去正视这各种的安全问题。也就是说要想更加完美地使用XMLHttpRequest,你就要了解浏览器的安全策略。

下面我们就来看看在使用XMLHttpRequest的过程中所遇到的跨域问题和混合内容问题。

  1. 跨域问题
    比如在极客邦的官网使用XMLHttpRequest请求极客时间的页面内容,由于极客邦的官网是www.geekbang.org,极客时间的官网是time.geekbang.org,它们不是同一个源,所以就涉及到了跨域(在 A 站点中去访问不同源的 B 站点的内容)。默认情况下,跨域请求是不被允许的,你可以看下面的示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    var xhr = new XMLHttpRequest()
    var url = 'https://time.geekbang.org/'
    function handler() {
    switch(xhr.readyState){
    case 0: // 请求未初始化
    console.log(" 请求未初始化 ")
    break;
    case 1://OPENED
    console.log("OPENED")
    break;
    case 2://HEADERS_RECEIVED
    console.log("HEADERS_RECEIVED")
    break;
    case 3://LOADING
    console.log("LOADING")
    break;
    case 4://DONE
    if(this.status == 200||this.status == 304) {
    console.log(this.responseText);
    }
    console.log("DONE")
    break;
    }
    }

    function callOtherDomain() {
    if(xhr) {
    xhr.open('GET', url, true)
    xhr.onreadystatechange = handler
    xhr.send();
    }
    }
    callOtherDomain()
    你可以在控制台测试下。首先通过浏览器打开www.geekbang.org,然后打开控制台,在控制台输入以上示例代码,再执行,会看到请求被 Block 了。控制台的提示信息如下:

Access to XMLHttpRequest at 'https://time.geekbang.org/' from origin 'https://www.geekbang.org' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
因为www.geekbang.orgtime.geekbang.com不属于一个域,所以以上访问就属于跨域访问了,这次访问失败就是由于跨域问题导致的。

  1. HTTPS 混合内容的问题
    了解完跨域问题后,我们再来看看 HTTPS 的混合内容。HTTPS 混合内容是 HTTPS 页面中包含了不符合 HTTPS 安全要求的内容,比如包含了 HTTP 资源,通过 HTTP 加载的图像、视频、样式表、脚本等,都属于混合内容。

通常,如果 HTTPS 请求页面中使用混合内容,浏览器会针对 HTTPS 混合内容显示警告,用来向用户表明此 HTTPS 页面包含不安全的资源。比如打开站点https://www.iteye.com/groups,可以通过控制台看到混合内容的警告,参考下图:

从上图可以看出,通过 HTML 文件加载的混合资源,虽然给出警告,但大部分类型还是能加载的。而使用XMLHttpRequest请求时,浏览器认为这种请求可能是攻击者发起的,会阻止此类危险的请求。比如我通过浏览器打开地址https://www.iteye.com/groups,然后通过控制台,使用XMLHttpRequest来请求http://img-ads.csdn.net/2018/201811150919211586.jpg,这时候请求就会报错,出错信息如下图所示:

打赏
  • Copyrights © 2017-2023 WSQ
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信