最近有刷号抢号的需求,写了程序之后发现会存在重复抢号的风险。研究javascript的异步、同步、循环的原理,进行重写。
最终目的:循环进行查号、抢号逻辑
1.初始逻辑
//初始逻辑如下,所有的请求方法都是异步的,但是同时返回的promise
setInterval(function () {
getAppointToken()
.then(function () {
return queryAll(); //多个异步查询 使用了Promise.allSettled 所有请求完成之后返回
})
.then(function () {
return fullData(); //填充有号信息
})
.then(function () {
return finalSubmit();
})
}, 1000)
2.重温promise
什么是promise?。
在这里我总结一下。promise是专门用来解决js异步变成问题的。也就是处理ajax里面succuess还有error之后的回调结果。
也就是说在then或者catch中按照同步的形式执行。
const http = require('http');
// 创建服务器 5s之后返回一个json对象
http.createServer( function (request, response) {
response.writeHead(200, {'Content-Type': 'text/html'});
let obj = {
name: 'wang',
age: 18
}
response.write(JSON.stringify(obj));
setTimeout(function () {
response.end();
}, 5000)
}).listen(8081);
console.log('Server running at http://127.0.0.1:8081/');
function get(num) {
console.log('===fetch========', num)
return fetch('http://127.0.0.1:8081/')
.then(function (response) {
return response.json();
})
.then(function (data) {
return data;
})
.catch(function (error) {
console.log(num + '--fail--' + error)
})
}
console.log('begin')
get(10).then(function(data){
console.log('10---ok:', data);
// get(11);
}).catch(function(data){
console.log('no:', data)
})
.finally(function () {
console.log('finally 10 ')
})
console.log('end')
结果是:
begin
===fetch======== 10
end
10---ok: {name: "wang", age: 18}
finally 10
我们再来看看如果异步请求中嵌套异步请求呢?
3.嵌套多个异步请求
function get(num) {
console.log('===fetch========', num)
return fetch('http://127.0.0.1:8081/')
.then(function (response) {
return response.json();
})
.then(function (data) {
return data;
})
.catch(function (error) {
console.log(num + '--fail--' + error)
})
}
console.log('begin')
get(10).then(function(data){
console.log('10---ok:', data);
get(11).then(function (data) {
console.log('11---ok---', data);
});
})
.then(function (data) {
console.log("10----then2-", data);
})
.catch(function(data){
console.log('no:', data)
})
.finally(function () {
console.log('finally 10 ')
})
console.log('end')
结果:
begin
===fetch======== 10
end
10---ok: {name: "wang", age: 18} //请求1
===fetch======== 11
10----then2- undefined
finally 10
11---ok--- {name: "wang", age: 18} //请求2
请注意请求2并不是包含在请求1中的。因为请求1的finally10
并不是最后输出的。这意味着想要在第二个then(即then2)中的逻辑与请求2没有同步,而是异步的。
Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。
也就是说如果你在then中嵌套的是异步,比如说请求2,then方法会继续向下执行,然后再到下一个then。如果你想同步,那么请把请求2当前请求1的返回值。
get(10).then(function(data){
console.log('10---ok:', data);
return get(11).then(function (data) { //此处增加return
console.log('11---ok---', data);
});
})
我们再来看看结果:
begin
===fetch======== 10
end
10---ok: {name: "wang", age: 18}
===fetch======== 11
11---ok--- {name: "wang", age: 18}
10----then2- undefined
finally 10
3. queryAll()
在这之前应该还有一个问题需要分析一下。我需要查询多天的号。那么必然发起多个请求(异步的)。
这里最完美的应该是:利用异步发起多个ajax请求,当有一个满足有号需求即可发起提交信息。追求最快的提交抢号信息。
//方案1 要求所有的promise都成功才会成功,一个失败即失败(不适用)
const p = Promise.all([p1, p2, p3]);
//方案2 捕捉第一个promise返回的结果,无论成功与否(不适用)
const p = Promise.race([p1, p2, p3]);
//方案3 等到所有的promise结束之后结束,只关心异步操作是否结束,所以结果总是成功(基本满足但是效率太低)
Promise.allSettled([p1, p2, p3])
//方案4 只要有一个成功,就返回成功,但是全部失败才算失败。(完美,但是只是草案没有这个功能)
Promise.any([p1, p2, p3])
对于queryAll()目前我采取了Promise.all(),勉强满足要求吧。
4.单次抢号
getAppointToken()
.then(function () {
return queryAll(); //多个异步查询 使用了Promise.allSettled 所有请求完成之后返回
})
.then(function () {
return fullData(); //填充有号信息
})
.then(function () {
return finalSubmit();
})
从之前的分析可以看出,上面的代码是一个同步的逻辑。那么为什么会出现重复抢号呢?
应该就是setInterval函数的问题了。
5.场景重现
修改了代码,每循环一次就进行输出,看看执行情况。
function get(num) {
console.log('===fetch========', num)
return fetch('http://127.0.0.1:8081/')
.then(function (response) {
return response.json();
})
.then(function (data) {
return data;
})
.catch(function (error) {
console.log(num + '--fail--' + error)
})
}
let i = 1;
function once(times) {
console.log('begin-------', times)
get(10).then(function(data){
console.log('10---ok:', data, '---', times);
return get(11).then(function (data) {
console.log('11---ok---', data, '-----', times);
});
})
.then(function (data) {
console.log("10----then2-", data, '-------', times);
})
.catch(function(data){
console.log('no:', data)
})
.finally(function () {
console.log('finally 10 -------',times)
console.log('\n\n\n\n')
})
console.log('end-------', times)
i++;
}
setInterval(function(){
once(i);
}, 2000)
begin------- 1 //第1次循环开始
===fetch======== 10
end------- 1
begin------- 2 //第2次循环开始
===fetch======== 10
end------- 2
begin------- 3 //第3次循环开始
===fetch======== 10
end------- 3
10---ok: {name: "wang", age: 18} --- 1
===fetch======== 11
begin------- 4 //第4次循环开始
===fetch======== 10
end------- 4
10---ok: {name: "wang", age: 18} --- 2
===fetch======== 11
begin------- 5 //第5次循环开始
===fetch======== 10
end------- 5
10---ok: {name: "wang", age: 18} --- 3
===fetch======== 11
begin------- 6 //第6次循环开始
===fetch======== 10
end------- 6
11---ok--- {name: "wang", age: 18} ----- 1
10----then2- undefined ------- 1
finally 10 ------- 1 //第1次循环正式结束
10---ok: {name: "wang", age: 18} --- 4
由此可以看出setInterval之间都是异步的。由此可见确实是setInterval带来的锅。
6.增加flag处理
let SUCCESS_FLAG = false;
function get(num) {
console.log('===fetch========', num)
return fetch('http://127.0.0.1:8081/')
.then(function (response) {
return response.json();
})
.then(function (data) {
return data;
})
.catch(function (error) {
console.log(num + '--fail--' + error)
})
}
let i = 1;
function once(times) {
i++;
console.log('begin-------', times)
return get(10).then(function(data){
console.log('10---ok:', data, '---', times);
return get(11).then(function (data) {
console.log('11---ok---', data, '-----', times);
});
})
.then(function (data) {
console.log("10--提交--then2-", data, '-------', times);
SUCCESS_FLAG = true;
})
.catch(function(data){
console.log('no:', data)
})
.finally(function () {
console.log('finally 10 -------',times)
console.log('\n\n\n\n')
})
console.log('end-------', times)
}
let mote = setInterval(function(){
if(SUCCESS_FLAG) {
clearInterval(mote);
console.log("已经抢到了号, 停止访问")
return ;
}
once(i);
}, 2000)
结果:
begin------- 1
===fetch======== 10
begin------- 2
===fetch======== 10
begin------- 3
...
10---ok: {name: "wang", age: 18} --- 3
===fetch======== 11
begin------- 6
===fetch======== 10
11---ok--- {name: "wang", age: 18} ----- 1
10--提交--then2- undefined ------- 1
finally 10 ------- 1
10---ok: {name: "wang", age: 18} --- 4
===fetch======== 11
已经抢到了号, 停止访问
11---ok--- {name: "wang", age: 18} ----- 2
10--提交--then2- undefined ------- 2
.... //一直到第6次
也就是利用flag的方式并未做到限制提交,因为需要成功提交flag才为true。然而服务器至少5s之后返回成功信息,那么5s之内的循环自然合理提交。
7.setInterval原理
setInterval中的任务会被添加到消息队列中,等待浏览器主线程执行。这些任务都是一步的,setInterval并不会在意他们是否完成,只要时间一到就会执行一次。这样就算增加了flag,网络延迟大一点就会造成重复提交了。
8.处理
那么怎么处理呢?
我们可以利用promise.finally进行补偿,如果前面的预约没有成功,那么再发起一遍请求。
promise....
.finally{
if(!SUCCESS_FLAG ) {
setTimeout(function(){
once(i);
}, 1000)
}
}