实现一个“能中断”的ajax

最近使用Ajax,发现API中 open() 的第三个参数(是否同步执行)中的false值被禁用了?
在console中可以看到,虽然能正常发送请求:
ajax_console

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

这也就意味着: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时就去调用这个函数。但我们真正只是在函数中放置了一个非异步的 promise 的 resolve 甚至 reject 方法,这样(原本请求中)接口得到响应时再执行 resolve 或 reject (的程序)就会被忽略。
通过这样的方式来“模拟”阻断promise的进行!

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

<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(){};
	//...
}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

恪愚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值