JavaScript异步(AJAX/axios/Promise/async/await)

893 阅读16分钟

在浏览器不重新加载任何窗口或窗体内容操控HTTP的需要使用JavaScript异步技术。

HTTP规定了WEB浏览器如何从WEB服务器获取文档和像WEB服务器提交表单内容,以及WEB服务器如何响应这些请求和提交。 WEB浏览器会处理大量的HTTP。通常,HTTP并不在JavaScript脚本的控制下,只有当用户单击链接、提交表单和输入URL时才发生。 但是,用JavaScript代码操纵HTTP是可行的。 用脚本设置window对象的location属性或者调用表单对象的submit()方法,都会初始化HTTP请求。在这两种情况下,浏览器会重新加载页面。————《JavaScript权威指南》

一、HTTP的一些知识

(一)请求报文

1、请求报文组成部分

  •  起始行
  •  首部
  •  主体  

 2、请求报文起始行简单介绍

(1)method(方法):用来告诉服务端做什么事。

  •  GET: 从服务器获取一份文档
  •  HEAD:只从服务器获取文档的首部 
  • POST:向服务器发送需要处理的数据 
  • PUT:将请求的主体部分存储在服务器上 
  • TRACE:对可能经过代理服务器传送到服务器上去的报文进行追踪 
  • OPTIONS:决定从服务器上执行哪些方法 
  • DELETE:从服务器删除一份文档 

(2) request-URL(请求URL) 所请求资源在服务器的路径。

通用格式为<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<qeury>#<frag> ,HTTP协议请求资源时,为http,默认80 。

(二)响应报文 

1、响应报文组成部分

  • 起始行  
  • 首部  
  • 主体  

 2、响应报文起始行简单介绍

(1)status(状态码):用来告诉客户端发生了什么事 

  • 100-199(已定义100-101):信息提示 
  • 200-299(已定义200-206):成功 
  • 300-399(已定义300-305):重定向 
  • 400-499(已定义400-415):客户端错误 
  • 500-599(一定义500-505):服务器错误 

(2)reason-phrase(原因短语) 为状态码提供文本形式的解释,和状态码成对出现。

(三)报文流

一、AJAX

AJAX:使用脚本操纵HTTP和WEB服务器进行交换数据,而不会导致页面重新加载的通信协议。

参考:developer.mozilla.org/zh-CN/docs/…

使用AJAX做两件事:

  • 在不重新加载页面的情况下发送请求给服务器。
  • 接受并使用从服务器发来的数据。

(一)使用AJAX与服务器通信过程

步骤一:首先要有一个完成这些事的对象

// Old compatibility code, no longer needed.
if (window.XMLHttpRequest) { // Mozilla, Safari, IE7+ ...
    httpRequest = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE 6 and older
    httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
}

步骤二:遵循先订阅后发布的原则,定义响应状态改变后回调哪个函数处理响应

httpRequest.onreadystatechange = function(){
    // Process the server response here.
};

步骤三:初始化并发送请求

//初始化请求
httpRequest.open('GET', 'http://www.example.org/some.file', true);  //true表示异步
//发送请求
httpRequest.send();

1、XMLHttpRequest.open(method, url, async)函数

我们知道请求报文的起始行包含method和request-URL参数,调用open()函数时需要指定这些内容,对应第一个参数和第二个参数,第三个参数表示是同步还是异步请求,一般设置为异步,默认也是异步。

2、XMLHttpRequest.send(body)函数

body是一个可选的参数,其作为请求主体;如果请求方法是 GET 或者 HEAD,则应将请求主体设置为 null,没有赋值也默认是null。如果请求方法是POST,则需要指定请求主体。

(二)步骤二处理响应的函数怎么写

1、如何知道通信过程到哪一步:XMLHttpRequest.readyState

也可以表述为:0 (请求还未初始化) ,1 (已建立服务器链接),2 (请求已接受),3 (正在处理请求) ,4 (请求已完成并且响应已准备好)。

XMLHttpRequest.readyState取值为XMLHttpRequest.DONE或者4就是通信完成服务器响应接收到的时候

2、如何得知通信结果——响应的状态码:XMLHttpRequest.status

取值是整数,是标准的HTTP的状态码,2xx范围的表示成功。在请求完成前,status值为0。值得注意的是,如果 XMLHttpRequest 出错,浏览器返回的 status 也为0。

3、通信成功如何获得响应报文主体:XMLHttpRequest.responseText和XMLHttpRequest.responseXML。

  • XMLHttpRequest.responseText – 服务器以文本字符的形式返回。
  • XMLHttpRequest.responseXML – 以 XMLDocument 对象方式返回,之后就可以使用JavaScript来处理。

综上,处理响应的函数第一步应该判断通信是否结束能获得响应,然后判断服务端处理结果(状态码)是怎样,如果是处理成功的就去读取想要的报文,框架如下

if (httpRequest.readyState === XMLHttpRequest.DONE) {
    // Everything is good, the response was received.
    if (httpRequest.status === 200) {
        // Perfect!
    } else {
        // There was a problem with the request.
        // For example, the response may have a 404 (Not Found)
        // or 500 (Internal Server Error) response code.
    }} else {
    // Not ready yet.
}

在通信错误的事件中(例如服务器宕机),在访问响应状态 onreadystatechange 方法中会抛出一个例外。为了缓和这种情况,则可以使用 try...catch 把上面的 if...else 语句包裹起来。

(三)发送post请求时的请求报文的首部和主体

实体首部提供有关实体及其内容的大量信息,可以告诉报文接收者(服务器)它在对什么进行处理。实体首部中有一部分是内容首部,其中的Content-Type首部,表示请求主体的对象类型,即客户端告诉服务器实际发送的数据类型。HTTP POST 方法 发送数据给服务器时,其请求主体的类型由 Content-Type 首部指定。

1、请求首部:XMLHTTPRequest(XHR)设置Content-Type的方法

XMLHttpRequest.setRequestHeader(header,value) :设置 HTTP 请求头的值。必须在 open() 之后、send() 之前调用 setRequestHeader() 方法。设置Content-Type则header设置为"Content-Type"。

2、请求主体:XMLHTTPRequest的请求主体作为send()方法的参数

3、XHR发送不同主体时首部的设置

  • 请求主题是简单的文本:Content-Type设置为'text/plain;charset=utf-8',可以不设置,请求主体传入会自动设置。
  • 请求主题是表单数据(a=1&b=2&c=3的形式的表单数据):Content-Type应设置为'application/x-www-form-urlencoded',可以不设置,请求主体传入会自动设置。
  • 请求主题是复杂的表单数据(包含上传文件表单数据,FormData数据):Content-Type设置为'multipart/form-data',FromData请求主体传入会自动设置developer.mozilla.org/zh-CN/docs/…
  • 请求主题是JSON数据(JSON.stringfy()序列化Jason对象):Content-Type设置为'application/json'。

4、Content-Type的语法

Content-Type:<media-type>;<charset>;<boundary>

  • media-type:资源或数据的媒体类型(MIME type)
  • charset:字符编码标准 ,us-ascii:ASCII字符编码;ios-8859-x:x可取值为1,2,5,6,7,8,15,是一套欧洲字符编码。常用的是ios-8859-1,是对ASCII的8位扩展,以支持西欧的多种语言;utf-8:UTF-8字符编码。(默认编码)
  • boundary:多部份实体中是必须的,用来封装消息的多个部分的边界。

二、axios

文档:www.npmjs.com/package/axi…

可以使用json-server搭建一个服务器来学习,只需三步,

文档:www.npmjs.com/package/jso…

axios发送请求后返回的是promise对象,发送请求的方法列举如下:

(一)aixos发送请求

1、Axios API发送请求

  • axios(config)

    //config中通过method属性指定不同请求方法 axios({ method:"get", url: "http://localhost:3000/posts/1" }).then((response)=>{ console.log(response.data) })

  • axios(url[,config])

    axios('/user/12345'); 默认方法就是get,不用指定config了

2、请求方法别名API发送请求

  • axios.request(config)——发送各类请求

  • axios.get(url[, config])——GET请求

  • axios.delete(url[, config])——DELETE请求

  • axios.head(url[, config])——HEAD请求

  • axios.options(url[, config])——OPTION请求

  • axios.post(url[, data[, config]])——POST请求

  • axios.put(url[, data[, config]])——PUT请求

  • axios.patch(url[, data[, config]])

    //request发送请求,config中通过method属性指定不同请求方法 axios.request({ method:"get", url: "http://localhost:3000/comments/1" }).then((response)=>{ console.log(response) }) //get发送请求 axios.get("http://localhost:3000/comments/1").then((response)=>{ console.log(response) }) //post发送请求axios.post("http://localhost:3000/comments",{ "body": "你好啊", "postId": 1 }).then((response)=>{ console.log(response) })

3、创建axios实例,实例发送请求

  • axios.create([config])

  axios实例创建出来后,这个实例发请求的用法和(一)中axios的使用是一样的。

创建实例可以复用一些基本的config配置,还可以创建不同的实例连接到不同的服务器。

 const instance=axios.create({
	baseURL:"http://localhost:3000",
	timeout:3000
});
instance({
	method:"GET",
	url: "/posts/1"
}).then((response)=>{
	console.log(response.data)
})

4、关于参数传递

可以根据业务需要,比如路由配置,后台接口要求,选取不同的传参方式。

(1)query方式传参(这个记忆的时候可以联想数据库查询时拼接查询参数)

axios.get('/user?ID=12345')

(2)params方式传参

方式一:

axios.get('/user/12345');

方式二:

axios.get('/user', {
    params: {
      ID: 12345
    }
  })

(二)aixos特色功能

1、对请求和响应进行拦截

可以在then/catch方法处理响应数据前对请求或者响应进行拦截。比如,根据后台接口返回的响应,做一个判断是否已经登录的处理,登录与否走不同的逻辑,就可以对响应进行拦截。

响应拦截:我们知道"2xx”都是服务端处理成功的状态码,其他则是出错的状态码,拦截器可以对这些情况都做一些拦截处理。

// Add a response interceptor
axios.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
  }, function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
  });

一个响应拦截的例子:

这个改造过程中经常会用到以下两个函数:Promise.resolve()Promise.reject() ,是手动创建一个已经 resolve 或者 reject 的 Promise 快捷方法。

axios.interceptors.response.use(function (response) {    
	let res= response.data;    
	if(res.status==0){
		//假设后台正常返回状态码是0        
		return res.data; //假设res.data是请求成功时后台返回的数据    
	}else if(res.status==10){
		//假设后台接口返回的用户未登录的错误码是10
		window.location.href="/#/login";         
		return Promise.reject(res);    
	}else{        
		console.log(res.msg)        
		return Promise.reject(res);    
	}  
}, function (error) {    
	console.log(error);   //处理2xx以外的错误码    
	return Promise.reject(error);  
});

2、取消请求:有需要时参看官方文档

三、Promise

B站有一个讲解promise很不错的视频:

www.bilibili.com/video/BV1GA…

MDN参考文档

developer.mozilla.org/zh-CN/docs/…

developer.mozilla.org/zh-CN/docs/…

(一)Promise是什么?为什么要用它?

Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。 是异步编程的一种解决方案。一个Promise对象有两个很重要的属性:[[PromiseState]]状态和[[PromiseResult]]值。

使用Promise,可以更优雅的写出异步任务成功和失败的处理过程。特别是对于一个异步任务执行完成后再继续下一个异步任务的场景,使用promise会让代码可读性更高。看下面的代码,是一个“回调地狱”:

doFirstSomething(function(firstResult) {
  //第一个异步任务执行成功,执行第二个异步任务
  doSecondThing(firstResult, function(secondResult) {    
//第二个异步任务执行成功,执行第三个异步任务
    doThirdThing(secondResult, function(finalResult) {      
        console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

让doFirstSomething返回一个promise对象,使用promise就可以很优雅的写成这样,这就是promise的链式调用。

doSomethingFirst().then(firstResult=>{
   return doSecondThing(firstResult)
}).then(secondResult=>{
   return doThirdThing(secondeThing)
}).catch(failureCallback);

(二)Promise对象的状态

Promise对象怎么去代表异步任务的成功和失败?写代码,在创建Promise时就让他根据异步任务调用结果指定成功/失败的条件。

  • 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。 
  • 已兑现(fulfilled): 意味着操作成功完成。 ——异步任务成功
  • 已拒绝(rejected): 意味着操作失败。——异步任务失败

1、改变Promise状态的方法

let p=new Promise((resolve,reject)=>{            
	//1、resolve,pending=>fullfiled(resolved)            
	//resolve("执行成功")         
	//2、reject,reject("执行失败") 
	//pending=>reject            
	//3、抛出错误 ,pending=>reject           
	throw '出问题了'           
})

2、一个创建Promise的例子,例子中根据异步任务的结果n的取值处理为成功或失败。

 let p=new Promise((resolve,reject)=>{
   setTimeout(()=>{                
	let n=rand(1,100);              
	console.log(n);                
	if(n<=30){                    
		resolve(n);  //将promise对象的状态置为成功,函数的参数值回传递给then中的成功回调方法                
	}else{                    
		reject(n); //将promise对象的状态置为失败,函数的参数值回传递给then中的失败回调方法                
	}            
   },3000)               
});

(三)成功回调&失败回调&成功或失败都进行的回调

1、Promise.prototype.then():返回一个Promise**。**这个函数可以有两个参数,第一个参数指定异步任务成功时的回调,第二个参数执行异步任务失败时的回调。语法:

p.then(onFulfilled[, onRejected]);

p.then(value => {
  // fulfillment
}, reason => {
  // rejection
})

2、Promise.prototype.catch():返回一个Promise。这个函数指定异步任务失败时的回调,相当于then()方法只指定失败回调,不指定成功回调的场景。语法

p.catch(onRejected);

p.catch(function(reason) {
   // 拒绝
});

为什么有了then()方法,还有有一个catch()方法呢,强大之处就是“链式调用”的时候,可以链式穿透,一个地方捕获进行n个promise的失败回调,比使用then一个个定义失败回调少写了不少代码。

let p=new Promise((resolve,reject)=>{
	//reject("NO");            
	resolve("YES")        
})        
p.then(value=>{            
	throw("出错了")        
}).then(value=>{            
	console.log(222);         
}).then(value=>{            
	console.log(333);          
}).catch(error=>{
//catch能捕获以上任一个promise抛出的异常            
	console.log(error);         
})

3、Promise.prototype.finally(),返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。语法:

p.finally(onFinally);

p.finally(function() {
   // 返回状态为(resolved 或 rejected)
});

综上,假设doSomething()返回一个Promise对象,一个比较完善的Promise使用框架如下:

doSomething().then(function(response) {
    //success,process response
  }) .catch(function(error) { 
    //fail,process error}).finally(function() {
    //do something whether success or fail 
});

(四)怎么理解链式调用

链式调用:一个异步任务执行完成后在继续下一个异步任务

第n(n>=2)个异步任务的执行与否有什么决定?由其前一个then()返回的Promise的状态决定,如果时成功的就执行下一个then()的成功回调,如果是失败就执行下一个then()的失败回调或者是catch()的回调,如果是未确定则跳出链式调用。

回调函数有以下几种执行结果:

let p=new Promise((resolve,reject)=>{            
	resolve("OK");        
})        
let result=p.then(value=>{            
	//1、抛出错误:状态是rejected            
	//throw "出了问题"            
	//2、返回的是非promise的任意值:状态时resolved ,值是"hello world"。            
	//return "hello world"            
	//3、返回的是promise,状态由这个promise的状态决定            
	return new Promise((resolve,reject)=>{                
		//resolve("oh yes");                
		reject("oh no");            
	})        
},error=>{            
		alert(error)        
})

then方法返回的promise的状态由then指定的回调函数执返回结果决定。

 (1)如果抛出异常,新的promise变为rejected,reason为抛出的异常 ;

 (2)如果返回的是非promise的任意值,新promise变为resolved,value为返回的值 ;

 (3)如果返回的是另一个新的promise,此promise的结果就会变为新promise的结果。

(五)中断Promise

即终端链式调用,当且仅当then()方法中返回一个pending状态的promise对象时,才能终端链式调用。

let p=new Promise((resolve,reject)=>{            
	resolve("YES")        
})        
p.then(value=>{            
	console.log(111);             
	return new Promise(()=>{})   //中断Promise        
}).then(value=>{            
	console.log(222);   //不会输出        
}).then(value=>{            
	console.log(333);    //不会输出        
}).catch(error=>{            
	console.log(error);        
})

(六)将一些旧式回调API改造成Promise

1、setTimeout函数封装

const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); 
//从右往左赋值,wait是一个函数
wait(10000)
.then(() => {
    saySomething("10 seconds")
}).catch(
    failureCallback
);

2、Ajax封装

 function sendAJAX(url){        
	const p=new Promise((resolve,reject)=>{        
		let xmlhttp=new XMLHttpRequest();        
		xmlhttp.open("GET",url);        
		xmlhttp.send();        
		xmlhttp.onreadystatechange=function(){            
			if(xmlhttp.readyState==4&&xmlhttp.status==200){                
				resolve(xmlhttp.responseText);  //将promise对象的状态置为成功,函数的参数值回传递给then中的成功回调方法            
			}
			else{                
				reject(xmlhttp.status+","+xmlhttp.readyState);  //将promise对象的状态置为失败,函数的参数值回传递给then中的失败回调方法            
			}        
		}        
	});          
	return p;          
}

sendAJAX("https://api.apiopen.top/getJoke").then((value)=>{        
	console.log(value);    
},(err)=>{        
	console.log(err)    
})

四、async&await

async和await是基于Promise的语法糖,让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用Promise。

(一)async——返回的是Promise

async关键字声明的函数,返回值的是一个Promise对象。Promise的状态由这个函数的返回值决定:

1、返回值是非Promise对象,则为fullfiled,结果值就是返回的值

2、返回值是Promise对象,由这个promise的执行结果决定

 async function test(){        
	// return "abc";    //等价于return Promise.resolve("abc")        
	return new Promise((resolve,reject)=>{            
		//resolve("OK")  //成功            
		//reject("ERR")  //失败            
		throw("hhhh")    //失败               
	})
}

(二)await——返回等待的Promise的[[PromiseResult]]值

await 操作符用于等待一个Promise 对象。它只能在异步函数 async function 中使用。await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。语法:

[返回值] = await 表达式;

await 右侧的表达式一般为Promise对象,但也可以是其他类型的值

1、如果表达式是Promise对象,若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。若 Promise 处理异常(rejected/throw error),await 表达式会把 Promise 的异常原因抛出,需要通过try...catch捕获处理

2、如果表达式是其他值,直接将此值作为await的返回值 。

async function test(){        
	let p= new Promise((resolve,reject)=>{            
		//resolve("yeah ok")            
		reject("oh no")        
	});        
	//1、await右侧是promise表达式        
	//let res=await p;        
	//2、await右侧是其它类型的数据        
	//let  res="abc"        
	try{            
		let res=await p;        
	}catch(e){            
		console.log(e);  /* 3、捕获promise异常 */        
	}           
}

(三)async和await怎么简洁Promise链式调用

MDN参考文档:developer.mozilla.org/zh-CN/docs/…

"await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成",基于此,链式代码中,除最后一个then之外,链中其他等待Promise执行完成的操作都放在await函数中,一个Promise对应一个await。这就实现了链式调用的效果。

1、Promise链式调用形式

fetch('coffee.jpg')
.then(response => response.blob())
.then(myBlob => {
  let objectURL = URL.createObjectURL(myBlob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
})
.catch(e => {
  console.log('There has been a problem with your fetch operation: ' + e.message);
});

2、async和await形式

async function myFetch() {
  let response = await fetch('coffee.jpg');
  let myBlob = await response.blob();

  let objectURL = URL.createObjectURL(myBlob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}

myFetch()
.catch(e => {
  console.log('There has been a problem with your fetch operation: ' + e.message);
});

由于await本身也是返回一个Promise对象,所以上面的代码还可以结合Promise做进一步的优化。

async function myFetch() {
  let response = await fetch('coffee.jpg');
  let myBlob = await response.blob();
  return myBlob;
}
myFetch().then(myBlob=>{  let objectURL = URL.createObjectURL(myBlob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);}).catch(e => {  console.log('There has been a problem with your fetch operation: ' + e.message);
});

五、等待多个异步任务完成再继续

1、Promise.all():这个方法的参数是promise的iterable类型(注:Array,Map,Set都属于ES6的iterable类型),一般来说传一个数组,数组里面的元素是Promise对象。返回值是一个Promise对象;

(1)参数中n个Promise对象状态都是fulfilled或者这个参数是空的(比如,[]),那么返回的那个Promise对象的状态就是fulfilled,值是数组。

(2)参数中存在一个Promise对象状态都是rejected,那么返回的那个Promise对象就是rejected;

(3)其他情况就是pending状态。

MDN参考文档:developer.mozilla.org/zh-CN/docs/…

下面是来自MDN很典型的例子:

const promise1 = Promise.resolve(3);
const promise2 = 42;  //非Promise类型就是fullfilled的Promise对象
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});
// expected output: Array [3, 42, "foo"]