上一篇文章中讲了setTimeout的定义方法以及清除方法,并且也知道了setTimeout的任务不能放在消息队列中因为这样无法在指定的事件之后执行任务,需要将延迟执行的任务放在另外一个消息队列中,这样执行完普通函数执行就可以去拿到延迟执行的任务
这篇文章我们继续往下走,看看XMLHttpRequest是如何实现的
有了JavaScript之后,我们通过DOM就可以对页面元素进行各种操作了,这属于页面层面的修改,如果页面一部分是需要从后端获取数据,那么想要修改这一部分就需要将整个页面进行刷新。
而XMLHTTPRequest提供了web前端从后端获取数据的能力,拿到数据之后,就可以单独修改这部分内容了,只修改其中一部分就行,不需要整个页面进行刷新,大大提高了渲染效率。
深入讲解XMLHttpRequest之前,先介绍一下 同步回调 和 异步回调
回调函数 && 系统调用栈
什么是回调函数 (Callback Function) ?
将一个函数作为参数传给另一个函数,作为参数的这个函数就是 回调函数, 示例代码如下:
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就是doWork函数的回调函数,并且callback是在doWork函数返回之前执行的,这个回调过程就叫做 同步回调
既然有同步回调,那么对应的是不是也有 异步回调 , 看下面一个异步回调的例子:
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函数让回调函数在doWork结束之后,并且延迟一秒去执行,这次的回调是在主函数doWork之外执行的,这种回调称为 异步回调
通过例子,认清了什么是同步回调,什么是异步回调,那么如果再深入点,站在消息循环的视角来看这两种回调方式的不同,会是一种什么 原理呢?
前面的文章我们知道了,浏览器页面是通过消息队列和事件循环机制来驱动的,每个渲染进程都有一个消息队列,页面主线程按照顺序来执行消息队列中的任务,新的任务会加到队列尾部,这样,消息队列和主线程事件循环机制,保证了页面的运行
当循环系统在执行一个任务的时候,都要为这个任务维护一个系统调用栈,通过 Performance 来抓取它核心的调用信息,如下图所示:
这幅图记录了一个 Parse HTML 的任务执行过程,其中黄色的条目表示执行 JavaScript 的过程,其他颜色的条目表示浏览器内部系统的执行过程
通过该图你可以看出来,Parse HTML 任务在执行过程中会遇到一系列的子过程,比如在解析页面的过程中遇到了 JavaScript 脚本,那么就暂停解析过程去执行该脚本,等执行完成之后,再恢复解析过程。然后又遇到了样式表,这时候又开始解析样式表……直到整个任务执行完成。
每个任务在执行过程中都有自己的调用栈,那么同步回调就是在当前主函数的上下文中执行回调函数,这个没有太多可讲的。下面我们主要来看看异步回调过程,异步回调是指回调函数在主函数之外执行,一般有两种方式:
- 第一种是把异步函数做成一个任务,添加到信息队列尾部;
- 第二种是把异步函数添加到微任务队列中,这样就可以在当前任务的末尾处执行微任务了。
XMLHttpRequest 运作机制
理解了什么是同步回调和异步回调,接下来我们就来分析 XMLHttpRequest 背后的实现机制,具体工作过程你可以参考下图:
上图是XMLHttpRequest 的总执行流程图,结合下面代码,看一个完整的请求:
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对象
let xhr = new XMLHttpRequest(); xhr对象用来执行实际的网络请求操作
第二步:为xhr对象注册回调函数
因为网络请求比较耗时,所以要注册回调函数,这样后台任务执行完成之后就可以通过调用回调函数来告诉执行结果。
回调函数主要有以下几种:
- ontimeout:用来监控超时请求,如果后台请求超时,该函数会被调用
- onerror:用来监控出错信息,如果后台请求出错了,该函数会被调用
- onreadystatechange:监控后台请求过程中的状态变化,比如监控到HTTP头加载完成的消息、HTTP响应体消息以及数据加载完成的消息等
配置基础的请求信息
注册好回调事件之后,然后就需要配置基础的请求信息,首先通过open
接口配置基本的请求信息,包括请求的地址,请求方法,请求方式(同步或异步)
xhr.open(‘GET’, url, true) // get请求,异步
然后通过xhr的内部属性来配置一些可选的请求参数,xhr.timeout = 3000
通过配置xhr.responseType = “text” 来配置服务器返回的格式,将服务器返回的数据自动转换为自己想要的格式,返回类型如下图所示:
需要额外添加自己的专用请求头属性的话,可以使用xhr.setRequestHeader来添加
发起请求
一切配置好之后,可以调用xhr.send来发起网络请求,完整的请求流程参考下图:
渲染进程将请求发送至网络进程,网络进程完成服务器资源下载,网络进程拿到数据之后,利用IPC来通知渲染进程,渲染进程收到消息,将xhr的回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候,就会根据相关的状态来调用对应的回调函数。