一个健壮的前端轮询

570 阅读5分钟

本文主要讨论在后端不支持、不使用websocket的情况下,如何写一个健壮的前端轮询。

前端轮询主要讨论的是定时异步任务,定时异步任务相比与定时同步任务需要考虑更多的因素。这里的异步任务一般包括发送网络请求及响应后的状态更新。从技术层面上,需要考虑到开启定时、发送请求、状态更新之间的逻辑顺序。

一、应用场景

1.获取实时数据,例如数据大屏、实时股价。

2.监测进度,例如数据上传进度、下载进度。

3.监测后端处理状态,例如提交一批数据后,后端需要对数据进行分析,耗时不确定,前端需要获取分析结果,则此时需要前端轮询。

4.检测静态资源是否加载完成(一般来讲是定时同步任务),例如当函数a逻辑需要在静态资源A加载完成后才能执行,则需要在执行函数a之前,开启轮询来判断资源A是否加载完成。

二、实现方式

使用setInterval

如果是定时同步任务没有问题,但对于轮询这样的定时异步任务需要注意响应时间和定时时间。当响应时间大于实时时间时,会存在多个未响应的请求,同时受到网络状况的影响,网络请求的响应顺序可能和请求顺序不一致,从而产生一些预期之外的情况。 `

const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
async function timer(params) {  
    let {start,name} = params;  
    var now = new Date();  
    var det = now - start;  
    await sleep(2000); 
    // 模拟请求响应  
    now.setTime(det);  
    now.setHours(0);  
    document.getElementById("id_name").innerHTML = `${name} : ${now.toLocaleTimeString()}`;}
    // 组件加载时开始轮询
    addEventListener("load", (event) => {  timeout = setInterval(()=>timer({start,name}), 1000);});

使用setTimeout

使用setTimeout可以保证轮询请求的唯一性,其代码如下。但考虑到代码健壮性以及更多具体的业务问题,需要进一步处理。 `

let timeout;
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
async function timer(params) {  
    clearTimeout(timeout);  
    var now = new Date();  
    var det = now - params.start;  
    await sleep(2000); 
    // 模拟请求响应  
    now.setTime(det);  
    now.setHours(0);  
    document.getElementById("id_name").innerHTML=`${params.name} :${now.toLocaleTimeString()}`;  
    timeout = setTimeout(()=>{timer(params)},1000);
}
addEventListener("load", (event) => {timer({start,name})});

三、可能遇见问题

1.同时有好几条轮询请求,或者发现数据刷新频率比理论值高

2.组件卸载或停止轮询后,仍然有轮询请求

3.更改了轮询请求的参数,但被旧参数的数据给覆盖了 从业务层面上,需要注意的问题:

1.开始轮询的途径有哪些?

常见的途径有页面组件加载后自动开始、按钮强制开始、参数变更后重新开始。在图3.1-3.3中,均只考虑了页面加载后自动开始轮询的情况。

2.如果有多个开启轮询的途径,怎么保证轮询的唯一性?

3.当轮询参数变更时,怎么终止旧的轮询并开始新的轮询?

这也是为了保证轮询的唯一性,同时避免旧数据覆盖新数据。

4.结束轮询的条件是什么?

四、健壮的前端轮询、做一些改造

对于setInterval的前端轮询实现主要需要考虑以下几个问题:

1.当一次定时执行时,此时可能有未响应的请求,可能需要跳过再次请求避免重复。

2.用户可能在任意时刻变更轮询的请求参数,这时即使有未响应的请求,也需要强制用新参数请求。

3.在2的情况发生后,会同时存在多个请求,当收到旧请求的响应时,需要跳过数据更新以避免旧数据覆盖。

4.在强制触发新的定时时,一定要保证旧的定时已经清除,否则可能出现存在过时请求和卸载后仍然在轮询的问题。

其具体实现可以参考如下代码: `

let name = '参数1';
let start = new Date();
let component;
let timeout;
let waitingResponse;
let intervalCount; 
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
async function timer(params,needWaiting=true) {  if(needWaiting && waitingResponse){    
return;//上一次请求未响应,跳过请求。特殊情况:强制请求  
}  
var now = new Date();  
var det = now - params.start;    
waitingResponse = true;  
const res = await sleep(2000)//Math.random()*10000%2); // 模拟请求响应,响应时间随机0-2s  waitingResponse = false;  
// 已刷新,数据过时  
let isRefresh = params.name!=name || params.start!=start;   
// 满足结束条件  
let isFinished = res?.isFinished;   
if(!isRefresh){    
now.setTime(det);    
now.setHours(0);    
component.innerHTML = `${params.name} : ${now.toLocaleTimeString()}`;  }  if(isFinished){    clearTimeout(timeout);  }  }// 重启const restart = () => {  start = new Date();  intervalCount=0;  clearTimeout(timeout);  timeout = setInterval(()=>timer({start,name},intervalCount++!==0),1000);}//参数变更const change = () => {  name= "参数"+parseInt(Math.random()*100);  start = new Date();  intervalCount=0;  clearTimeout(timeout);  timeout = setInterval(()=>timer({start,name},intervalCount++!==0),1000);}//模拟组件卸载const unmount = () => {  component = null;  clearTimeout(timeout);}//模拟组件挂载const mount = () => {  component =document.getElementById("id_name");  intervalCount=0;  //挂载时自动开始轮询  timeout = setInterval(()=>timer({start,name},intervalCount++!==0),1000);}

setTimeout版 对于setTimeout的前端轮询实现主要需要考虑以下几个问题:

1.用户可能在任意时刻变更轮询的请求参数,这时即使有未响应的请求,也需要强制用新参数请求。

2.当1发生时,需要清除旧的定时,同时避免旧请求的响应继续触发定时(跳过)。

3.当1发生时,可能存在过时的响应,不应该使用过时数据更新状态。 其具体实现可以参考如下代码:

let name = '参数1';let start = new Date();let component;let timeout;const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))async function timer(params) {  clearTimeout(timeout);  var now = new Date();  var det = now - params.start;    const res = await sleep(2000)// 模拟请求响应  // 已刷新,数据过时  let isRefresh = params.name!=name || params.start!=start;   // 满足结束条件  let isFinished = res?.isFinished;   if(!isRefresh){    now.setTime(det);    now.setHours(0);    component.innerHTML = `${params.name} : ${now.toLocaleTimeString()}`;  }  if(!isRefresh && !isFinished && component){    timeout = setTimeout(()=>{timer(params)},1000);  }}// 重启const restart = () => {  start = new Date();  timer({start,name}); }//参数变更const change = () => {  name= "参数"+parseInt(Math.random()*100);  start = new Date();  timer({start,name}); }//模拟组件卸载const unmount = () => {  component = null;  clearTimeout(timeout);}//模拟组件挂载const mount = () => {  component =document.getElementById("id_name");  timer({start,name});//挂载时自动开始轮询}

总结

本文探讨的是在不使用websocket做服务端推送的情况下,如何写出一个健壮的前端轮询。