Redian新闻
>
一个健壮的前端轮询

一个健壮的前端轮询

科技

阿里妹导读


本文讨论了在不使用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 <div><div>Demo</div> <div>{name}:{data}</div> <Button onClick={restart}>重启</Button> <Button onClick={change}>参数变更</Button> </div>}
ReactDOM.render(<Demo />, mountNode);


六、结语

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

能用AI写的代码,不允许程序员手写?!你怎么看?


以Copilot、通义灵码等为代表的AI智能编码助手成为越来越多开发者的必备工具,补全/续写代码、写单元测试、debug的功能不在话下,本期我们来聊聊你在使用AI编码助手过程中的感受和评价:


1.你认为 AI 编码助手真的能提效吗?
2.个别公司要求能用AI写代码,不允许程序员手写,如果要手写,必须注释说明AI写不了这段代码的原因,你怎么看?

3.你最常用和喜欢通义灵码编码助手哪些功能?分享一些你在使用过程中发现的小技巧。

👇欢迎点击”阅读原文“发表你的看法

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
2个游泳池+2个健身房,奢华在细节里!曼岛中城西,交通中心位置,一切刚刚好~小思考 |改变的前提是意识到痛心!44岁的前网易新闻高管去世CVPR 2023最佳论文作者李弘扬:端到端自动驾驶的前景与挑战这种细菌24小时内可夺命!大温健壮华人消防员在香港被截肢保命!《回眸北斋之浮世绘世界》 下Apple Vision Pro 要上市了,来了解下它的前世今生新中国联邦是如何污辱女性的?“中国血王”的前世今生为什么前端技术栈如此复杂?美国2023年避免了经济衰退,这是专家们对2024年的前景预测纽约大学往事:一所女神校的前世今生[旅游] 旅行第7季第37城-德国Speyer --听生不逢时的前苏联航天飞机的哭泣核能的前途与危机暖心!把爱传出去!! 这个健身社团在年度慈善聚会留下$7000多小费....2024前端圈 “开年之战”:React挖坑不填,要靠文档来补?前端monorepo大仓权限设计的思考与实现巴西女婴天生长4个肾,1个发育不良被摘除,3个健康的全保留!绝对是报复!跪杀”弗洛伊德的前警察确认被刺22刀!使用Go实现健壮的内存型缓存干货:一键生成任意前端项目开源“靓仔”头像生成器:纯前端本地实现、完全随机绘制——保证独一无二!年度代码翻车现场 |前端代码评审问题总结QCon上海2023 参会内容分享:LLM 时代的大前端技术趋势冻一冻,远不止“抑制肿瘤生长”的优点!Cell子刊:仅2小时寒冷暴露,就能带来这个健康益处!从国家博弈角度看郭文贵现象吞吐量提升5倍,联合设计后端系统和前端语言的LLM接口来了淘宝买菜累计下单玩法的前世今生与技术思考实习派 | 易婧玮:从“零经验”到论文竞赛获奖,她想做负责任AI领域的前沿探索者澳洲知名连锁健身房宣布禁止三脚架拍摄!网友热议:每个健身房都应效仿!2024 年 7 个 Web 前端开发趋势《跌宕起伏心灵煎熬的14天》(2) 【警署事故中心】行业观察|2024年中国经济和商业的前景,他们为何如此展望?创业失败的前高管们:“大厂光环”是人生最大的错觉热搜,低调,游艇
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。