深入讲解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 笔记二 Array和Date对象方法
May 22 Javascript
ExtJS4如何自动生成控制grid的列显示、隐藏的checkbox
May 02 Javascript
js中通过父级进行查找定位元素
Jun 15 Javascript
$.extend 的一个小问题
Jun 18 Javascript
JS实现的文字与图片定时切换效果代码
Oct 06 Javascript
Javascript基于对象三大特性(封装性、继承性、多态性)
Jan 04 Javascript
JS组件Bootstrap Select2使用方法解析
May 30 Javascript
Bootstrap开发实战之响应式轮播图
Jun 02 Javascript
JS仿百度自动下拉框模糊匹配提示
Jul 25 Javascript
js中的深浅拷贝问题简析
May 10 Javascript
OpenLayers3加载常用控件使用方法详解
Sep 25 Javascript
抖音短视频(douyin)去水印工具的实现代码
Mar 30 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数组操作
2011/12/30 PHP
php 数组动态添加实现代码(最土团购系统的价格排序)
2011/12/30 PHP
php获取301跳转URL简单实例
2013/12/16 PHP
php简单实现屏蔽指定ip段用户的访问
2015/04/29 PHP
php获取当前url地址的方法小结
2017/01/10 PHP
js 多种变量定义(对象直接量,数组直接量和函数直接量)
2010/05/24 Javascript
JS中的异常处理方法分享
2013/12/22 Javascript
js过滤特殊字符输入适合输入、粘贴、拖拽多种情况
2014/03/22 Javascript
无需 Flash 使用 jQuery 复制文字到剪贴板
2016/04/26 Javascript
js获取所有checkbox的值的简单实例
2016/05/30 Javascript
AngularJS 模型详细介绍及实例代码
2016/07/27 Javascript
jQuery插件EasyUI设置datagrid的checkbox为禁用状态的方法
2016/08/05 Javascript
js 自带的sort() 方法全面了解
2016/08/16 Javascript
JS正则获取HTML元素的方法
2017/03/31 Javascript
node文件上传功能简易实现代码
2017/06/16 Javascript
webpack项目使用eslint建立代码规范实现
2019/05/16 Javascript
操作按钮悬浮固定在微信小程序底部的实现代码
2019/08/02 Javascript
基于PHP pthreads实现多线程代码实例
2020/06/24 Javascript
JavaScript实现瀑布流布局的3种方式
2020/12/27 Javascript
[03:02]2020完美世界城市挑战赛(秋季赛)总决赛回顾
2021/03/11 DOTA
在Python中用split()方法分割字符串的使用介绍
2015/05/20 Python
Python的Django框架中自定义模版标签的示例
2015/07/20 Python
玩转python爬虫之正则表达式
2016/02/17 Python
Python基础之函数基本用法与进阶详解
2020/01/02 Python
opencv python如何实现图像二值化
2020/02/03 Python
python将数据插入数据库的代码分享
2020/08/16 Python
CSS3 圆角效果
2009/07/15 HTML / CSS
Paul Smith英国官网:英国国宝级时装品牌
2019/03/21 全球购物
精选奢华:THE LIST
2019/09/05 全球购物
兼职业务员岗位职责
2014/01/01 职场文书
工作迟到检讨书范文
2015/05/06 职场文书
《认识年月日》教学反思
2016/02/19 职场文书
创业计划书之干洗店
2019/09/10 职场文书
Nginx tp3.2.3 404问题解决方案
2021/03/31 Servers
pytorch 实现变分自动编码器的操作
2021/05/24 Python
php去除deprecated的实例方法
2021/11/17 PHP