网易首页 > 网易号 > 正文 申请入驻

一个健壮的前端轮询

0
分享至

导读:本文讨论了在不使用websocket做服务端推送的情况下,如何写出一个健壮的前端轮询。文章提供了一些常见的前端轮询的应用场景以及可能遇到的问题,欢迎大家一起讨论。

一 前言

本文的前端轮询主要讨论的是定时异步任务,定时异步任务相比与定时同步任务需要考虑更多的因素。这里的异步任务一般包括发送网络请求及响应后的状态更新。从技术层面上,需要考虑到开启定时、发送请求、状态更新之间的逻辑顺序。此外,本文不讨论利用websocket做服务端推送,只考虑在仅前端变更的情况下做轮询(在某些时候,确实只能如此)。

二 应用场景

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

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

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

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

三 实现方式

3.1. 使用setInterval

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

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);
});

3.2. 使用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.结束轮询的条件是什么?

五 健壮的前端轮询

5.1. setInterval版

如图5.1,对于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);
}

5.2. setTimeout版



如图5.2,对于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});//挂载时自动开始轮询

5.3. 工具化及使用demo

本小节根据setTimeout版简单实现了一个前端轮询的工具asyncPooling,并提供了一个在React函数组件中的使用demo。(类实现的小工具比之前的函数版更好用,之前的已经去掉了)


import React, { useState, useEffect, useCallback } from "react";
import ReactDOM from "react-dom";
const mountNode = document.getElementById("root");
import { Button } from '@alifd/next';

class asyncPooling {
/**
*
* @param {*} interval 轮询的间隔时间
* @param {*} func 轮询的请求函数
* @param {*} callback 请求响应数据的处理函数
* /** callback的参数
* @param params, 原请求参数
* @param res,请求的响应数据
* @param isRefresh, 有新的轮询在运行,响应数据可能已过时
* */
*/
constructor(interval,func,callback){
this.interval = interval;
this.func = func;
this.callback = callback;
this.params = {};
}
run(params){
this.isFinished = false;
this.params = {...params}; //每次run时params设同一个引用,当再次run时可用来判断isRefresh。即可区分不同run,很方便
this.runTurn(this.params);
}
stop(){
this.isFinished = true;
}
destroy() {
clearTimeout(this.timeout);
}
async runTurn(params){
clearTimeout(this.timeout);
const res = await this.func(params);
let isRefresh = params!==this.params;
this.callback(params,res,isRefresh);
if(!isRefresh && !this.isFinished){
this.timeout = setTimeout(()=>this.runTurn(params),this.interval);
}
}
setCallBack(callback){
// 由于函数组件的闭包陷阱,需要重新设置callback以保证在调用该方法时能拿到最新的state
this.callback = callback;
}
}
function Demo(props) {
const [name, setName] = useState("参数1");
const [start, setStart] = useState(new Date());
const [data, setData] = useState();
const [polling, setPolling] = useState();

const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));

const updateDate = useCallback((params, res,isRefresh) => {
// let isRefresh = params.name != name || params.start != start;
let isFinished = res?.isFinished;
if(isFinished){
polling.stop();
}
if (!isRefresh) {
var now = new Date();
var det = now - params.start;
now.setTime(det);
now.setHours(0);
setData(now.toLocaleTimeString());
}
},[polling]);
// 由于函数组件的闭包陷阱,需要重新设置callback以保证在调用该方法时能拿到最新的state
polling && polling.setCallBack(updateDate);
useEffect(() => {
let p = new asyncPooling(1000,(params) => sleep(2000),updateDate);
setPolling(p);
p.run({ start, name });
return () => (polling || p).destroy();
}, [])
// 重启
const restart = () => {
let s = new Date();
setStart(s);
polling.run({ start: s, name });
}
//参数变更
const change = () => {
let n = "参数" + parseInt(Math.random() * 100);
let s = new Date();
setName(n);
setStart(s);
polling.run({ start: s, name: n });
}
return

Demo


{name}:{data}


重启
参数变更


ReactDOM.render(, mountNode);

六 结语

本文讨论了在不使用websocket做服务端推送的情况下,如何写出一个健壮的前端轮询。本文提供了一些常见的前端轮询的应用场景(第2节)以及可能遇到的问题(第4节),非常欢迎大家加入讨论、提供意见,丰富这些内容。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
刘伯温、李淳风神秘预言:2024更加难熬,会有一位圣人浮出尘世

刘伯温、李淳风神秘预言:2024更加难熬,会有一位圣人浮出尘世

心灵短笛
2023-11-23 16:54:00
美军喊话台湾:一旦解放军动手,只要撑住1个月,美军就能登陆

美军喊话台湾:一旦解放军动手,只要撑住1个月,美军就能登陆

兵器展望
2024-11-27 17:06:49
已经顾不上南海了!菲律宾国内大乱?16万菲军被要求马上行动!

已经顾不上南海了!菲律宾国内大乱?16万菲军被要求马上行动!

阿纂看事
2024-11-30 11:44:36
汪小菲携夫人厦门参加会议,马筱梅黑色西装显干练 网友:转变了

汪小菲携夫人厦门参加会议,马筱梅黑色西装显干练 网友:转变了

晴晴给你讲故事
2024-11-30 12:29:41
vivo打响“价格战”,6500mAh+防水耐摔,12GB+256GB才卖1536元

vivo打响“价格战”,6500mAh+防水耐摔,12GB+256GB才卖1536元

知心数码
2024-11-30 22:36:16
张铁林与波兰籍前妻的混血女儿张月亮,颜值爆表

张铁林与波兰籍前妻的混血女儿张月亮,颜值爆表

视点历史
2024-10-11 00:20:02
笑不活了,原来这就是老夫老妻的真实状态,太有共鸣了!

笑不活了,原来这就是老夫老妻的真实状态,太有共鸣了!

猫小狸同学
2024-11-28 20:20:03
如果你的血压总是150/90mmHg降不下来,尝试6种方法,9成能正常

如果你的血压总是150/90mmHg降不下来,尝试6种方法,9成能正常

心血管汤医生
2023-04-25 12:10:08
恭喜快船!鹈鹕前锋英格拉姆或搭档哈登 翻版杜兰特组三巨头冲冠

恭喜快船!鹈鹕前锋英格拉姆或搭档哈登 翻版杜兰特组三巨头冲冠

湖人侃球师
2024-11-30 15:55:18
2025款迈巴赫GLS正式上市,售价183.3万起

2025款迈巴赫GLS正式上市,售价183.3万起

沙雕小琳琳
2024-11-30 14:13:44
任端平履新海南省营商环境建设厅长,此前在市场监管总局任职

任端平履新海南省营商环境建设厅长,此前在市场监管总局任职

澎湃新闻
2024-11-30 14:56:27
CBA最惨球队!拼到无人可用,主教练被迫当中锋,打不过还使坏

CBA最惨球队!拼到无人可用,主教练被迫当中锋,打不过还使坏

后仰大风车
2024-11-30 09:25:03
努尔基奇:来太阳后我受到的批评最多 人们想让我扮演KD的角色

努尔基奇:来太阳后我受到的批评最多 人们想让我扮演KD的角色

直播吧
2024-11-30 12:41:21
“太炸裂了!”江苏,某女子赌博连输十几局,没钱还债想用肉偿!

“太炸裂了!”江苏,某女子赌博连输十几局,没钱还债想用肉偿!

现代小青青慕慕
2024-11-29 00:01:46
庆典搞砸了,巴萨在125周年纪念日第二天主场遭遇输球

庆典搞砸了,巴萨在125周年纪念日第二天主场遭遇输球

懂球帝
2024-11-30 23:18:08
鹿晗和张一山的处境,其实都差不多,都是一着不慎满盘皆输的代表

鹿晗和张一山的处境,其实都差不多,都是一着不慎满盘皆输的代表

人情皆文史
2024-11-29 23:43:19
英国通过恶劣决议,称中国主权“不含台湾”,我方回应掷地有声:

英国通过恶劣决议,称中国主权“不含台湾”,我方回应掷地有声:

现代春秋
2024-11-30 13:14:28
我在印度生活了9个月,说几句可能很多人不爱听的大实话

我在印度生活了9个月,说几句可能很多人不爱听的大实话

风飘飘而吹衣
2024-11-30 16:35:43
执行教练训主教练,主教练满不在乎,只有一人拼命的深圳如何赢球

执行教练训主教练,主教练满不在乎,只有一人拼命的深圳如何赢球

秋眼体育
2024-11-30 07:18:09
日本祖先被证实,不是徐福后代,DNA检测让日本人无法接受

日本祖先被证实,不是徐福后代,DNA检测让日本人无法接受

杨哥历史
2024-11-30 09:38:36
2024-11-30 23:35:00
阿里云云栖号
阿里云云栖号
阿里云官方内容社区!
2989文章数 865关注度
往期回顾 全部

科技要闻

官宣!华为+广汽,联手造车!

头条要闻

被问是否担心《美墨加协定》被中国"利用" 加财长回应

头条要闻

被问是否担心《美墨加协定》被中国"利用" 加财长回应

体育要闻

历史第一!詹姆斯再创里程碑宝刀不老

娱乐要闻

恶意炒作!李行亮麦琳和好后口碑崩塌

财经要闻

雪松爆雷前实控人张劲在香港抛售房产

汽车要闻

比亚迪方程豹豹8推送首次OTA 新增暴力模式

态度原创

教育
家居
时尚
旅游
军事航空

教育要闻

留学就是要不学有术!

家居要闻

现代设计感 温馨两居室

48岁满头银发又怎样?她活成了所有女人都想成为的样子

旅游要闻

驻意大利使馆提醒旅意中国公民注意风险防范

军事要闻

叙反对派武装近10年来首次攻入阿勒颇

无障碍浏览 进入关怀版