最近有刷号抢号的需求,写了程序之后发现会存在重复抢号的风险。研究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)
	}
}