什么是异步操作?
Ajax
异步JavaScript与XML(AJAX)是一个专用术语,用于实现在客户端脚本与服务器之间的数据交互过程。这一技术的优点在于,它向开发者提供了一种从Web服务器检索数据而不必把用户当前正在观察的页面回馈给服务器。与现代浏览器的通过存取浏览器DOM结构的编程代码 (JavaScript) 动态地改变被显示内容的支持相配合,AJAX让开发者在浏览器端更新被显示的HTML内容而不必刷新页面。换句话说,AJAX可以不用刷新整个页面就更新网页内容,使基于浏览器的应用程序更具交互性而且更类似传统型桌面应用程序。
XMLHttpRequest工作原理
从这个描述里面,我们可以看出来,Ajax包含的内容应该有
- 能够跟服务端发起网络请求(一般是http,但是也可以是ftp或者其他协议的请求),处理服务端的网络响应
- 能够根据服务端返回的数据,调用JS来修改DOM结构和渲染CSS样式
- 能够跟js事件循环交互处理异步的响应和回调
而Ajax实现核心功能其实就是通过XMLHttpRequest
。
XMLHttpRequest(XHR)
XHR在发起请求时,运行机制如图所示:
在XMLHttpRequest发起请求之后,渲染进程会将请求发送给网络进程,然后网络进程负责资源的下载,等网络进程接收到数据之后,就会利用 IPC 来通知渲染进程;渲染进程接收到消息之后,会将 xhr 的回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候,就会根据相关的状态来调用对应的回调函数
XMLHttpRequest关键方法和参数
- XMLHttpRequest.open:表示开启一个请求
- XMLHttpRequest.send:表示发送一个请求
- XMLHttpRequest.readyState:表示当前所处的状态
值 | 状态 | 描述 |
---|---|---|
0 | UNSENT | 代理被创建,但尚未调用 open() 方法。 |
1 | OPENED | open() 方法已经被调用。 |
2 | HEADERS_RECEIVED | send() 方法已经被调用,并且头部和状态已经可获得。可以通过 setRequestHeader() 方法来设置请求的头部, 可以调用 send() 方法来发起请求。 |
3 | LOADING | 下载中; responseText 属性已经包含部分数据。 |
4 | DONE | 下载操作已完成。 |
- XMLHttpRequest.onreadystatechange:请求状态变化时的回调,用户定义的回调函数在此调用
此处只列出了关键参数,XMLHttpRequest详细的从参数和方法可以参考MDN
XMLHttpRequest/readyState
JS实现Ajax
- 实现思路(以发起HTTP请求为例)
1.ajax接收json格式的参数option,json里面的参数用于配置发起网络请求的具体参数
2.参数option与HTTP请求相关配置参数处理,如果传入option里面有些必填的参数没有(比如:请求类型(get还是post),datatype等),那么ajax中可以设置默认值,或者判断之后抛出异常。
3.参数option的数据具体参数data json的格式处理
4.创建XMLHttpRequest
实例对象,考虑兼容性(可选),因为在IE6以下版本是ActiveXobject
5.XMLHttpRequest.open(method, url, async, user, password);
初始化请求,根据请求的类型是get还是post,确定open的从参数是如何传输的
6.XMLHttpRequest.send()
发送请求,如果是POST,参数data数据是由send发送的,但是send不支持json格式参数,所以要对data数据进行编码
7.当readyState状态变化,设置XMLHttpRequest.onreadystatechange()
回调,如果返回的HTTP请求状态码是请求成功相关的状态码,执行,success的回调,否则执行error的回调
8.请求超时未完成,XMLHttpRequest.abort()
自动中断
<script>
function ajax(options){
options = options ||{}; //调用函数时如果options没有指定,就给它赋值{},一个空的Object
options.type=(options.type || "GET").toUpperCase();/// 请求格式GET、POST,默认为GET
options.dataType=options.dataType || "json"; //响应数据格式,默认json
options.timeout=options.timeout|| 5000; //响应数据格式,默认json
var params=formatParams(options.data);//options.data请求的数据
var xhr;
//考虑兼容性
if(window.XMLHttpRequest){
xhr=new XMLHttpRequest();
}else if(window.ActiveObject){//兼容IE6以下版本
xhr=new ActiveXobject('Microsoft.XMLHTTP');
}
//启动并发送一个请求
if(options.type=="GET"){
xhr.open("GET",options.url+"?"+params,true);
xhr.send(null);
}else if(options.type=="POST"){
xhr.open("post",options.url,true);
//设置表单提交时的内容类型
//Content-type数据请求的格式
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xhr.send(params);
}
// 设置有效时间
setTimeout(function(){
if(xhr.readySate!=4){
xhr.abort();
}
},options.timeout)
// 接收
// options.success成功之后的回调函数 options.error失败后的回调函数
//xhr.responseText,xhr.responseXML 获得字符串形式的响应数据或者XML形式的响应数据
xhr.onreadystatechange=function(){
if(xhr.readyState==4){
var status=xhr.status;
if(status>=200&& status<300 || status==304){
options.success&&options.success(xhr.responseText,xhr.responseXML);
}else{
options.error&&options.error(status);
}
}
}
}
//格式化请求参数
function formatParams(data){
var arr=[];
for(var name in data){
arr.push(encodeURIComponent(name)+"="+encodeURIComponent(data[name]));
}
arr.push(("v="+Math.random()).replace(".",""));
return arr.join("&");
}
//基本的使用实例
ajax({
url:"http://server-name/login",
type:'post',
data:{
username:'username',
password:'password'
},
dataType:'json',
timeout:10000,
contentType:"application/json",
success:function(data){
。。。。。。//服务器返回响应,根据响应结果,分析是否登录成功
},
//异常处理
error:function(e){
console.log(e);
}
})
</script>
Promise
MDN中对于Promise 的定义是:表示一个异步操作最终完成 (或失败), 及其结果值.
了解过JS事件循环的机制的话,我们会发现,JS当中认为异步操作,在开始执行到获取最后结果之间,是允许其被挂起等待(无论它真实的执行事件多快),也就是说异步任务的代码执行了之后,可以先把这个任务的具体执行结果放一边,先执行其他同步代码,再来执行异步任务的回调。而且操作的结果可能是成功的也可能是失败的。因此在Promise
内部有一个属性[[PromiseState]]
,保存了三种状态,表示异步行为的整个过程。
- Pending:表示异步操作还没有结束
- fulfilled:表示异步操作执行成功了
- rejected:表示异步操作被拒绝了
对于fulfilled
和rejected
状态来说,代表的都是promise已经结束了,所以这两个状态又统称为settle
状态。
我们可以根据这三个状态的转换,来理解整个Promise的运行过程。
以promise
替代ajax
为例,我们会看到形如这样的代码:
const promise1 = new Promise(function(resolve,reject){
ajax(
{
url:'http://testUrl',
success:function(data){
resolve(data);
},
error:function(e){
reject(e);
}
}
)
});
promise1.then(function(data){
//get data from resolve
},
function(e){
//get e from reject
});
从Promise的使用的代码中,我们可以把它大致分成三个部分:
- Promise(function(resolve,[reject]))构造函数
- Promise.prototype.then
- onfulfilled和onReject回调
根据ecma的规范,我们可以看看每一个步骤中大概都发生了什么。
Promise(excutor)构造函数
- excutor必须是个函数对象,含有两个函数参数
resolve
和reject
,用来表示异步操作成功或者失败的情况下,执行的操作。
当这个构造函数调用的时候,有几个关键的步骤: - 创建一个Promise实例p
- 把p的
[[PromiseState]]
状态设为pending
- Promise resolve方法执行后,会把
[[PromiseState]]
状态设为fulfilled
ecma262/#sec-promise-resolve-functions - 同理如果执行的是Promise reject方法,会把
[[PromiseState]]
状态设为reject
- 执行Promise reject还是resolve会决定,then之后是执行fulfilled回调还是reject回调。
- 最后返回这个Promise实例p
ecma262/#sec-promise-executor
我们可以看出,Promise的状态是在excutor当中就完成pending
到settle
的过程。
Promise.prototype.then
- 首先,then中的this对象是当前调用它的promise实例
- 会根据这个实例的具体状态(是fulfilled还是reject)来判断,将fulfilled回调还是reject回调任务加到微任务队列当中,等待事件队列中的任务执行完才执行。
- 最后then返回一个新promise或者undefined
ecma262/#sec-performpromisethen
Promise.resolve(value)和Promise.reject(reason)可以理解为对excutor的封装
- Promise.resolve(value)
返回一个状态由给定value决定的Promise对象。如果该值是thenable(即,带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则的话(该value为空,基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled,并且将该value传递给对应的then方法。通常而言,如果你不知道一个值是否是Promise对象,使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该value以Promise对象形式使用。
在MDN中看到一个例子:
var original = Promise.resolve(33);
var cast = Promise.resolve(original);
cast.then(function(value) {
console.log('value: ' + value);
});
console.log('original === cast ? ' + (original === cast));
/*
* 打印顺序如下,这里有一个同步异步先后执行的区别
* original === cast ? true
* value: 33
*/
稍微有点疑惑为什么original和cast两个对象能够绝对相等?
查看了一下PromiseResolve
源码规范
从上述代码中我们可以看到,如果resolve的是promise对象,那么会直接返回这个对象x,所以original和cast其实就是同一个对象。
所以我们可以看到,虽然
Promise.resolve(33)
都是33,但是因为33不是promise,所以resolve会返回一个新的promise对象,original
和cast
不相等。
var original = Promise.resolve(33);
var cast = Promise.resolve(33);
console.log('original === cast ? ' + (original === cast));//输出false
- Promise.reject(reason)
返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法
promise.all(iterable)
用于处理多个promise事件,传入一个promise数组,最终会返回一个新的promise,resolve value返回数据数组,顺序跟promise数组里面的顺序一样,如果是reject状态,那么返回第一个报错的error信息。所以如果不报错,all会等所有promise执行完毕再返回结果promise.race(iterable)
race在英文中表示竞赛的意思,所以我们可以理解为promise.race(iterable)
就是传入的promise数组,会按照执行速度的快慢来返回结果(无论是resolve还是有异常发生)。
MDN-Promise/race
Promise链式调用
因为Promise构造函数和then、catch最终都是返回一个新的Promise对象,所以Promise可以用链式调用来解决回调地狱的问题。
new Promise((resolve, reject) => {
console.log('初始化');
resolve();
})
.then(() => {
throw new Error('有哪里不对了');
console.log('执行「这个」”');
})
.catch(() => {
console.log('执行「那个」');
})
.then(() => {
console.log('执行「这个」,无论前面发生了什么');
});
//输出结果
初始化
执行“那个”
执行“这个”,无论前面发生了什么