写点什么

实现一个“能中断”的 ajax

用户头像
云小梦
关注
发布于: 2021 年 03 月 29 日

最近使用 Ajax,发现 API 中 open() 的第三个参数(是否同步执行)中的 false 值被禁用了?

在 console 中可以看到,虽然能正常发送请求:


但是并不会正常执行 onreadystatechange 回调函数中的语句:


这也就意味着:ajax 只能支持异步模式了!


这当然是好事:因为发送一个同步请求会让浏览器进入暂时性的假死状态,特别是请求需要处理大量数据、长时间等待的接口。这会导致“失败”的用户体验。


-----


前两天被淘系电面时面试官问了我一个问题:在你做过的项目中如果需要发送多个请求获取不同数据而且这些请求是要根据上一次的返回数据做不同处理的你会怎么做?

那必然考虑用 promise 营造 异步 http 请求 环境啊!就像这样:


let readUrlPromise=url=>{    return new Promise((resolve,reject)=>{        let xhr=new XMLHttpRequest();        xhr.open("GET",url);        xhr.onreadystatechange=function(){            if(xhr.readyState==4 && xhr.status==200){                // console.log(JSON.parse(xhr.responseText));                // reject("请求失败了");                resolve(JSON.parse(xhr.responseText));            }else if(xhr.readyState==4 && xhr.status!=200){                reject('请求失败');            }        }        xhr.onerror=function(){            reject('请求失败');        }        xhr.send(null);    })}readUrlPromise("这里是百度地图某url,就不放出来,嘿嘿").then(data=>{	console.log('1231');	console.log(data);	return readUrlPromise("这里是百度地图某url,就不放出来,嘿嘿")}).then(data2=>{	console.log('2231');	console.log(data2)})
复制代码


其实在这里说的就是:如果第一个请求没有返回数据或者发生错误了,那么后面的请求实际就没有必要进行了 —— 不然一个 loading 在页面正中间转了半天然后在用户焦急期待的目光中出现一个大大的 Error ,就有些难受了。


但是之后我又考虑了一个问题:如果这两个(或多个)请求之间(或这些请求对其它请求)是可以互相影响的,比如:在用户中心页面如果当前请求返回的数据异常或者携带某个标志位又或者用户触发了另一个操作,那么就取消已经开始执行的请求。

这也可以用在“按钮误触”等场景下。


在笔者想到这里的时候,突然想到了一个现成的库 —— axios !

很多人也许没用过。但事实上,axios 中的 请求拦截 不仅可以用于加工、处理数据,也可以拦截本次请求的发送:这得益于 axios 的拦截器机制


// 请求拦截器axios.interceptors.request.use(config=>{	// ...},error=>{	return Promise.reject(error)})
// 响应拦截器axios.interceptors.response.use(response=>{ // ...},error=>{ return Promise.reject(error)})
复制代码


在里面,axios 增加了一个叫 CancelToken 的属性,用于完成内置的“取消请求”操作,文档上介绍说这 可用于防误触多点 ,cancel token:

为了达到这一目的,我们可以先在全局设置“得到取消请求类”:


const cancelToken=axios.CancelToken;
复制代码

然后在 axios 请求中或者在请求拦截器中利用工厂类创建取消对象并在头信息中设置取消 token:


const source=cancelToken.source();config.cancelToken=source.token;
复制代码


最后在 请求拦截器中 判断比如如果当前是 loading 状态或已经发过请求了(比如用字符串比较或设置标志位判断),就触发 cancel()方法进行取消操作:


source.cancel('可能误触,已拦截当前请求');
复制代码


-----


回到上面的说法,借鉴 axios-token,我们也许可以实现一个“ 可取消的 promise ”。


大家都知道,promise 的逻辑一旦开始执行就无法中止,除非它执行完成。


let ps=new Promise((resolve,reject)=>{	resolve('这是result');	// 注意这个resolve	resolve('我才是真正的result,但是被忽略了...')	console.log('没有感情的runing...')})console.log(ps)
复制代码



如上,实际上我们可以对 promise 进行一层封装,向外暴露一个取消函数,需要取消 promise 时就去调用这个函数。但我们真正只是在函数中放置了一个非异步的 promise 的 resolve 甚至 reject 方法,这样(原本请求中)接口得到响应时再执行 resolve 或 reject (的程序)就会被忽略。

通过这样的方式来“模拟”阻断 promise 的进行!


先来看下效果 —— 这里是请求的百度地图的某接口,为了达到“某些大量数据请求的延时”的效果,笔者这里用setTimeout 等待 2s:


<div class="page">    <div class="article">        <h1>我是标题部分</h1>        <p>黑化肥发灰,灰化肥发黑</p>        <p>黑化肥发灰会挥发;灰化肥挥发会发黑</p>        <p>黑化肥挥发发灰会花飞;灰化肥挥发发黑会飞花</p>    </div>    <p class="footer">嘿嘿</p>
<button class="send">发送请求</button> <button class="cancel">取消</button></div>
复制代码


const baseURL='https://devapi.qweather.com/v7/weather/24h?location=这里是纬经度坐标&key=这里是百度地图key值';
// 初始化取消函数,防止调用时报错let cancelFn=function(){}
function request(req){ return new Promise((resolve,reject)=>{ let xhr=new XMLHttpRequest(); xhr.open(req.method || 'GET',baseURL); xhr.onload=function(){ if(xhr.readyState==4 && (xhr.status>=200 && xhr.status<300)){ setTimeout(()=>{ resolve({data:JSON.parse(xhr.responseText)}) },2000) }else{ reject(xhr.status) } } xhr.onerror=function(){ reject('请求失败了...') } xhr.send(req.data || null);
// 向外暴露取消函数 cancelFn=function(msg){ reject({message:msg}) } })};
// test// 上面那些完全可以放在单独的文件中被外链let send=document.querySelector('.send');let cancel=document.querySelector('.cancel');send.addEventListener('click',async function(){ console.log('请求进行中...') let {data}=await request({}) console.log(data)});cancel.addEventListener('click',function(){ cancelFn('取消了上一次的请求');})
复制代码


至此效果实现了,但其中有三点却是 ajax 请求中“不得不提的”:


1. xhr.onerroronreadystatechange 事件是每次请求状态(xhr.readyState)改变时都会触发,一个请求要触发好几次 change 事件,所以千万不要在对 readyStatestatus 的判断中用 else 暴露 reject ,那样大概率你的程序不会正确执行!

2. xhr.onload :笔者发现一件很奇怪的事,如果你的 ajax 被包裹在 promise 中且是自己执行的,那么你就可以用 onreadystatechange 事件来做回调,但是如果是在用户触发的事件中被调用的,那么在其中就只能用 onload 做回调,具体原因 emmmmmmm 欢迎告知!

3. 上述代码中在外部(全局)初始化 cancelFn 函数一是为了“在没有触发请求函数时触发取消函数不至于报错但是无反应”;二是为了“在请求函数中模拟了一个向外暴露函数的操作”


当然了,类似的比较方便的 http 请求库还有 fetch 之类的,至于是要用原生的还是封装好的库,在什么项目中用哪种方式,就要看各位如何抉择了...


-----


相关扩展


上面提到的这些,不管是 axios 的 interceptors ,还是对 promise + ajax 的封装,都“赤裸裸地”提到了一个概念:拦截!

而在 node 中比较出名的 mini-proxy 代理模块中我也找到了相关代码:


function MiniProxy(options){	//...	this.onBeforeRequest=options.onBeforeRequest || function(){};	this.onBeforeResponse=options.onBeforeResponse || function(){};	//...}
复制代码

毫无疑问,拦截已经成为数据处理方面的“首选”!

发布于: 2021 年 03 月 29 日阅读数: 18
用户头像

云小梦

关注

求知若渴,虚心若愚 2019.05.11 加入

江湖人称“云小梦”。一个大前端路上还未“毕业”的“小学生”。爱好分享、执着探索、乐于开源;着迷于vue、node、css、可视化、前端智能化以及原生js技术。csdn链接:https://yunxiaomeng.blog.csdn.net/

评论

发布
暂无评论
实现一个“能中断”的ajax