深入讲解xhr(XMLHttpRequest)/jsonp请求之abort


Posted in Javascript onJuly 26, 2017

前言

相信大家在工作中经常需要使用AJAX,所以当大家看到文章标题的时候可能会觉得这是一个老生常谈的话题。

前端开发中向后端发起xhr(XMLHttpRequest)请求(代表性的就是熟悉的ajax)是再正常不过的事。

但在前端开发过程中,不怎么重视xhr的abort(中止掉xhr请求,及表示取消本次请求)。往往会带来一些不可意料的结果。

比如:切换tab,发起xhr请求,渲染同一个列表。就这么简单的拉取数据渲染列表的功能,并且可以根据tab切换。想想应该是很简单。但是假如你只顾着发起xhr请求,而没有abort掉它,想想会发生什么。很有可能就是当前选中的tab数据,并不是你想要的。说白了就是数据错了。这时候你可能就要考虑是不是xhr请求返回数据的顺序问题。

答案是肯定的,xhr请求返回数据顺序是不固定的。所以你要做的就是abort掉你之前的xhr请求,然后再发起一个新的xhr请求。

结合上面所说的例子可以知道xhr使用不当会存在以下问题:

  • 容易出现页面最终数据与状态不一致的问题,这可能再列表筛选是出现的概率比较大。
  • xhr请求达到一定数量之后,浏览器就会显得非常的慢。因为有太多的请求在请求服务器资源。

为了解决上面的问题,我们在进行页面的时候就必须考虑abort掉所有的xhr请求。

那么如何实现xhr的abort方法呢,或者通过何种方式abort掉xhr呢?

一个简单的xhr

我们都知道,现在的框架(例如:jQuery的ajax模块)对xhr都进行了封装,是为了让我们更好的使用xhr。但是也蒙蔽了我们的眼睛。让我们抛开框架,来看看一个简单的xhr怎么实现。

//仅供参考 xhr
function ajax(type ,url , data , successCallBack , errorCallBack){
 let xhr = new XMLHttpRequest();
 xhr.onload = ()=>{
 if(xhr.status === 200){
  return successCallBack(xhr.response||xhr.responseText);
 }
 return errorCallBack('请求失败');
 }
 xhr.onerror = ()=>{
 return errorCallBack('出错了');
 }
 xhr.open(type,url);
 xhr.send(data ? data:null);
}

这就是一个简单的xhr请求的实现,我把它命名为ajax,我们现在可以通过以下方式进行调用:

ajax('get','/test/getUserList' , undefined , function(result){
 console.log('成功了。', result);
} ,function(error){
 console.log(error);
});

如果使用这个方法我们是没办法abort掉xhr请求的。好吧,现在我们把它改造一下,让它支持abort方法:

//仅供参考 xhr.abort
function ajax(type ,url , data , successCallBack , errorCallBack){
 let xhr = new XMLHttpRequest();
 xhr.onload = ()=>{
 if(xhr.status === 200){
  return successCallBack(xhr.response||xhr.responseText);
 }
 return errorCallBack('请求失败');
 }
 xhr.onerror = ()=>{
 return errorCallBack('出错了');
 }
 xhr.open(type,url);
 xhr.send(data ? data:null);
 return xhr;//返回XMLHttpRequest实例对象
}

好像没有什么变化对吧。不错,只要在函数的末尾添加return xhr;将XMLHttpRequest实例对象返回即可。那我们在就已经可以如愿的abort掉xhr请求。

let xhr = ajax('get','/test/getUserList' , undefined , function(result){
 console.log('成功了。', result);
} ,function(error){
 console.log(error);
});
//abort
xhr.abort();

好像我们已经大功告成了。但是问题来了,现在Promise这么好用,为什么不把它加进来呢。像这样没法在我们的Promise链式调用上使用它。

Promise封装xhr

好了,现在的首要任务是封装出一个Promise版的ajax库。首要要确认的就是,ajax方法需要返回的是Promise实例对象,而不再是原生的XMLHttpRequest实例对象。知道了这一点那就可以进行封装了。

//仅供参考 promise
function ajax(type ,url , data ){
 let xhr = new XMLHttpRequest();
 let promise = new Promise(function(resolve , reject){
 xhr.onload = ()=>{
  if(xhr.status === 200){
  return resolve(xhr.response||xhr.responseText);
  }
  return reject('请求失败');
 }
 xhr.onerror = ()=>{
  return reject('出错了');
 }
 xhr.open(type,url);
 xhr.send(data ? data:null);
 });
 return promise;//返回Promise实例对象
}

使用了Promise之后我们不再需要传入回调函数。所以参数减少了。这样我们就可以愉快的进行链式调用了。

let promise = ajax('get','/test/getUserList');
promise.then((result)=>{
 console.log('成功了。', result);
},(error)=>{
 console.log(error);
})

可问题又来了,Promise实例是没有abort方法的。假如我们把ajax方法修改为返回xhr,我们是可以如期调用abort方法杀死请求,但是我们就不能使用Promise带给我们的好处了。

仔细思考,最后一句return promise; 这里是不能改。我们只能另外想办法。

最简单的解决方式就是创建一个xhr和promise的映射关系。也就是每一个promise对应一个唯一的xhr请求。有了思路之后,解决方案就来了。

let map = [];//用于保存promise和xhr之间的映射关系
//仅供参考 promise abort
function ajax(type ,url , data ){
 let xhr = new XMLHttpRequest();
 let promise = new Promise(function(resolve , reject){
 xhr.onload = ()=>{
  if(xhr.status === 200){
  return resolve(xhr.response||xhr.responseText);
  }
  return reject('请求失败');
 }
 xhr.onerror = ()=>{
  return reject('出错了');
 }
 xhr.open(type,url);
 xhr.send(data ? data:null);
 });
 map.push({promise:promise,request:xhr});//创建promise和xhr之间的映射关系,保存到全局的一个数组中。
 return promise;//返回Promise实例对象
}
//abort 请求
function abort(promise){
 for(let i = 0 ; i < map.length ; i++ ){
 if ( map[i].promise === promise ){
  map[i].request.abort();
 }
 }
}

通过在全局创建一个map保存所有的promise和xhr之间的映射关系。这样我们就可以在需要abort请求的时候根据映射关系找到xhr并abort请求。

let promise = ajax('get','/test/getUserList');
promise.then((result)=>{
 console.log('成功了。', result);
},(error)=>{
 console.log(error);
})
abort(promise);

好吧,到这里Promise版的ajax,我们已经实现了。是不是很简单啊。

何为jsonp

假如你还不明白jsonp是何物,那希望下面的篇幅能让你明白。可能你零星的知道跨越请求,但是可能没有在实战中碰到过。那么我们先来看看,一个简单的jsonp函数是怎么实现的吧。

let index = 0;
//仅供参考 jsonp
function jsonp(url,jsonp,successCallback , errorCallback){
 let script = document.createElement('script');
 let result ;
 script.onload = function(){
 successCallback(result);
 }
 script.onerror = function(){
 errorCallback('出错了');
 }
 let callBackName = 'jsonpCallback'+index++;
 script.src=url+(url.indexOf('?') >=0 ? '&':'?')+jsonp+'='+callBackName;
 window[callBackName]=function(){//拿给后端进行输出执行的。
 result = Array.prototype.slice.call(arguments);
 }
 document.head.append(script);
}

jsonp算起来应该就是通过script加载实现的跨域请求。其中重要的就是数据返回的接收,我们需要和后端开发同学协商回调函数的变量名。然后后端获取到回调函数名,并且在返回时把回调函数和数据拼接成字符串返回到前端。前端我们添加一个window对象的函数用于接收数据,在函数执行完成后,就会触发script.onload事件,这样就可以真正执行用户回调函数了。

可能你会觉得有点绕,其实细细的理一下,应该就明白了。

后端其实很简单,只要获取到jsonp函数变量名就可以了。然后把函数和数据拼接成字符串返回即可。

下面我们来看看Node.js中的实现:

let query = ctx.request.query;
let jsonp = query.jsonp;//与后端协商的回调参数
ctx.body = jsonp+'({code:0,msg:"success"})';

这个回调函数并不是用户输入的successCallback,而是jsonp函数内部的window[callBackName] ,为什么要这样。你细想一下JavaScript的作用域应该就会知道。这就好比你在script标签中执行一个函数一样。

有可能我们第一次调用jsonp函数服务器会返回如下结果:

<script >
 //只有这一行是服务器返回的,
 //script标签是document.head.append(script)时候加的
 jsonpCallback0({code:0,msg:"success"});
</script>

所以,得出结论就是:函数必须能通过window对象上访问到。不然执行时就会报错。这就是为什么我们不能直接把用户传入的回调直接用来当成回调接收数据的真正原因。

再次强调:JavaScript作用域。

一次成功的jsonp应该是:添加script标签到head,后端接收到jsonp数据,返回拼接好的函数名和数据字符串,执行window对象上的函数拿到数据,执行script.onload事件,执行成功回调。

jsonp的abort方法何去何从

现在你已经知道了jsonp的原理了。那么如何才能对script加载数据进行abort呢。

犯难的问题来了,script并没有真正的abort方法给我们使用。我们所做的就是尽最大的努力提供类似于abort功能的方法。

思路就是使用Event事件对象。触发script的error监听事件。所以我们得对jsonp函数添加一个trigger辅助函数进行触发error事件。

//[trigger 触发事件]
function trigger(element,event){
 if( !isString(event) ) {
 return;
 }
 if ( element.dispatchEvent ){
 let evt = document.createEvent('Events');// initEvent接受3个参数
 evt.initEvent(event, true, true);
 element.dispatchEvent(evt);
 }else if ( element.fireEvent ){ //IE
 element.fireEvent('on'+event);
 }else{
 element['on'+event]();
 }
}
let index = 0;
//仅供参考 jsonp.abort
function jsonp(url,jsonp,successCallback , errorCallback){
 let script = document.createElement('script');
 let result ;
 script.onload = function(){
 successCallback(result);
 }
 script.onerror = function(){
 errorCallback('出错了');
 }
 let callBackName = 'jsonpCallback'+index++;
 script.src=url+(url.indexOf('?') >=0 ? '&':'?')+jsonp+'='+callBackName;
 window[callBackName]=function(){//拿给后端进行输出执行的。
 result = Array.prototype.slice.call(arguments);
 }
 script.abort = ()=>{
 return trigger(script,'error');
 };
 document.head.append(script);
 return script;
}

我们把Promise也使用进来,那样的话,我们就可以脱离回调地狱了不是吗?

let index = 0;
//仅供参考 jsonp.abort
function jsonp(url,query,jsonp){
 let script = document.createElement('script');
 let result ;
 let promise = new Promise(function(resolve,reject){
 script.onload = function(){
  return resolve(result);
 }
 script.onerror = function(){
  return reject('出错了');
 }
 let callBackName = 'jsonpCallback'+index++;
 script.src=url+(url.indexOf('?') >=0 ? '&':'?')+jsonp+'='+callBackName;
 window[callBackName]=function(){//拿给后端进行输出执行的。
  result = Array.prototype.slice.call(arguments);
 }
 document.head.append(script);
 });
 script.abort = ()=>{
 return trigger(script,'error');
 };
 map.push({promise:promise,request:script});//创建promise和script之间的映射关系,保存到全局的一个数组中。
 return promise;
}

同样的我们套用上面的xhr的abort函数封装。这样我们就大功告成了。基本的功能我们就全部实现了。我们就可以开始进行调用了。

let promise = jsonp('/test/getUserList','jsonp');
promise.then((result)=>{
 console.log('成功了。', result);
},(error)=>{
 console.log(error);
})
abort(promise);

总结

虽然,我们已经完成了封装,但是还有很多的意外没有考虑,要想再实战中运用还必须进行封装和重构。我们必须重视abort方法在xhr/jsonp中的运用,但是也不能滥用,适可而止。存在多层服务器调用时,应该更需要慎重考虑。

要想了解更多,可以参考这是我封装好的一个Promise版本的ajax/jsonp库https://github.com/Yi-love/xhrp,大家也可以通过本地进行下载。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
javascript据option的value值快速设定初始的selected选项
Aug 13 Javascript
js 巧妙去除数组中的重复项
Jan 25 Javascript
判断js中各种数据的类型方法之typeof与0bject.prototype.toString讲解
Nov 07 Javascript
浅谈JavaScript函数节流
Dec 09 Javascript
JavaScript分页功能的实现方法
Apr 25 Javascript
Bootstrap Modal对话框如何在关闭时触发事件
Dec 02 Javascript
JavaScript奇技淫巧44招【实用】
Dec 11 Javascript
BootStrapValidator校验方式
Dec 19 Javascript
JQuery 获取多个select标签option的text内容(实例)
Sep 07 jQuery
浅谈vue,angular,react数据双向绑定原理分析
Nov 28 Javascript
express+vue+mongodb+session 实现注册登录功能
Dec 06 Javascript
vue 防止页面加载时看到花括号的解决操作
Nov 09 Javascript
基于ExtJs在页面上window再调用Window的事件处理方法
Jul 26 #Javascript
Angular中自定义Debounce Click指令防止重复点击
Jul 26 #Javascript
JavaScript利用fetch实现异步请求的方法实例
Jul 26 #Javascript
深入探究angular2 UI组件之primeNG用法
Jul 26 #Javascript
WdatePicker.js时间日期插件的使用方法
Jul 26 #Javascript
关于Stream和Buffer的相互转换详解
Jul 26 #Javascript
JS 60秒后重新发送验证码的实例讲解
Jul 26 #Javascript
You might like
php获取文章内容第一张图片的方法示例
2017/07/03 PHP
TP5框架实现自定义分页样式的方法示例
2020/04/05 PHP
php实现商城购物车的思路和源码分析
2020/07/23 PHP
JavaScript 学习笔记(七)字符串的连接
2009/12/31 Javascript
js获取当前select 元素值的代码
2010/04/19 Javascript
计算世界完全对称日的js代码,粗糙版
2011/11/04 Javascript
JS JSON对象转为字符串的简单实现方法
2013/11/18 Javascript
ie8本地图片上传预览示例代码
2014/01/12 Javascript
使用javascript实现简单的选项卡切换
2015/01/09 Javascript
原生js实现类似弹窗抖动效果
2015/04/02 Javascript
简介JavaScript中的setTime()方法的使用
2015/06/11 Javascript
JavaScript实现文本框中默认显示背景图片在获得焦点后消失的方法
2015/07/01 Javascript
编写高质量JavaScript代码的基本要点
2016/03/02 Javascript
Node.js中看JavaScript的引用
2017/04/22 Javascript
jQuery简介_动力节点Java学院整理
2017/07/04 jQuery
js实现会跳动的日历效果(完整实例)
2017/10/18 Javascript
详解vue组件开发脚手架
2018/06/15 Javascript
JS 设计模式之:单例模式定义与实现方法浅析
2020/05/06 Javascript
[52:07]完美世界DOTA2联赛PWL S3 LBZS vs access 第二场 12.10
2020/12/13 DOTA
决策树的python实现方法
2014/11/18 Python
用Python实现通过哈希算法检测图片重复的教程
2015/04/02 Python
Python实现对象转换为xml的方法示例
2017/06/08 Python
对python for 文件指定行读写操作详解
2018/12/29 Python
python函数的作用域及关键字详解
2019/08/20 Python
通过python扫描二维码/条形码并打印数据
2019/11/14 Python
win7上tensorflow2.2.0安装成功 引用DLL load failed时找不到指定模块 tensorflow has no attribute xxx 解决方法
2020/05/20 Python
CSS3实现超慢速移动动画效果非常流畅无卡顿
2014/06/15 HTML / CSS
美国全球旅游运营商:Pacific Holidays
2018/06/18 全球购物
澳大利亚工具仓库:Tools Warehouse
2018/10/15 全球购物
英格兰足协官方商店:England Store
2019/07/12 全球购物
保安的辞职报告怎么写
2014/01/20 职场文书
周年庆典邀请函范文
2014/01/24 职场文书
法制报告会主持词
2014/04/02 职场文书
员工激励培训演讲稿
2014/09/16 职场文书
安全教育第一课观后感
2015/06/17 职场文书
vue完美实现el-table列宽自适应
2021/05/08 Vue.js