今天面试官问的我这个问题,说实话,我当时懵逼了。
我第一个想法竟然是:嘶~这问题挺简单的啊,不就是用 Date
对象判断是否大于某个时间差么?于是写出了下面这段代码:
let rest=function(){
// 开始是放在外面的,但是面试官说要尽可能不单独暴露值,于是鬼迷心窍(紧张)杀都没想就直接扔进来了...
let date=Date.now();
return new Promise((resolve,reject)=>{
let _date=Date.now();
if(_date-date>2000){
reject('请求超时');
}
resolve();
})
}
在我暗自得意是不是快结束了面试官一会儿会如何告诉我面试通过了的时候,对面突然传来一句话:
“你这个,确定要这样?”
“你这样if条件真的会执行么?”
我顿时感觉大事不妙大惊失色,正要仔细看时面试官已经说出了“行吧,这道题你再想想,这次的面试就到这里吧”
下来后仔细想想,“基于promise”的前提就已经提示了应该要“尽可能地应用promise的特性”,那么,这个问题应该用什么呢?
我想起了前两天写过的这篇文章:实现一个“能中断的”ajax 。里面提到了可以用 “ reject抢跑 ”的方法去中止后续回调的执行!
这里要记得一点:promise状态值改变一旦触发就不可逆,所以不可能真正“中断”promise,只能说通过同步与异步的先后规则(event Loop)去让“成功”的回调无法执行。
我似乎恍然大悟了起来…
但现在还有一个问题:怎么判断“超时”?
如果不出意外,你把两个 Date
放在一个函数中无论如何它们的值都是相同的!所以我想到了 setTimeout
!有了这个异步时间处理,我的思路就明朗了起来:在promise中有一个API叫:Promise.race()
,和 all()
不同的是,race在其中promise状态值有一个改变时就会立刻执行它的结果,就像这样:
let rest=function(_data=1000){
return Promise.race([
upload().then(data=>{console.log(data.data)}),
uploadTimeout(_data)
])
}
function upload(){
console.log('请求进行中...');
return new Promise((resolve,reject)=>{
let xhr=new XMLHttpRequest();
xhr.open('GET',"https://devapi.qweather.com/v7/weather/24h?location=这里是纬度和经度英文逗号分搁&key=这里是百度地图的key");
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(null);
// 【1】
})
};
function uploadTimeout(times){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
reject('请求超时,请重试'); // 【2】
},times)
})
}
到这里,基本实现了功能,但是运行后你会发现:第一个函数在报错后仍然执行了!
对,这就是上面说的:promise状态值改变的过程是不可逆的。而且虽然你下面返回了reject,但是这是两个promise,之间是不冲突的!
受上面说过的 reject抢跑 的启发,我们可以在代码中标注为【1】的地方写这样的代码:
// 向外暴露取消函数
cancelFn=function(msg){
reject('请求超时,请重试');
}
然后将代码中标注为【2】的地方的代码替换为:
cancelFn();
至此,一个“超时请求处理”的功能就真的实现了:
重新研究了promise源码,发现一件事:文档上描述的“Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。”其实并没有说完整,这样的描述其实很容易带给开发者一个想法:race在第一个promise执行结束时就会return(不再执行其他的promise了)。但是如上面所说,promise过程不可逆不可中断,那么已经开始执行的promise怎么办呢?
如果按上面代码中这样写,而且最后没有添加cancelFn函数,就会出现如下情况:
既报了错,又返回了执行完成的结果。
这显然不符合实际场景!
查阅文档发现,MDN是这样解释的:
race 函数返回一个 Promise,它将与第一个传递的 promise 相同的完成方式被完成。它可以是完成( resolves),也可以是失败(rejects),这要取决于第一个完成的方式是两个中的哪个。
如果传的迭代是空的,则返回的 promise 将永远等待。
如果迭代包含一个或多个非承诺值和/或已解决/拒绝的承诺,则 Promise.race 将解析为迭代中找到的第一个值。
根据描述即猜想,我们不难模拟出 promise.race
的实现:
Promise.Myrace=function(promises){
promises=Array.from(promises);
return new Promise((resolve,reject)=>{
if(promises.length===0){
resolve([]);
}else{
for(let i=0;i<promises.length;i++){
Promise.resolve(promises[i]).then(data=>{
resolve(data);
return;
},err=>{
reject(err);
return;
})
}
}
})
}
所以,我们可以将上面实现超时处理的代码中第2-5行改为这样,并可以去掉cancelFn函数的声明和调用:
return Promise.Myrace([
upload(),
uploadTimeout(_data)
])
.then((value)=>{
console.log(value)
})
(最后结果只输出一次,就不会出现上面的情况了)