一个健壮的前端轮询
一个健壮的前端轮询
阿里妹导读
本文讨论了在不使用websocket做服务端推送的情况下,如何写出一个健壮的前端轮询。文章提供了一些常见的前端轮询的应用场景以及可能遇到的问题,欢迎大家一起讨论。
一、前言
本文的前端轮询主要讨论的是定时异步任务,定时异步任务相比与定时同步任务需要考虑更多的因素。这里的异步任务一般包括发送网络请求及响应后的状态更新。从技术层面上,需要考虑到开启定时、发送请求、状态更新之间的逻辑顺序。此外,本文不讨论利用websocket做服务端推送,只考虑在仅前端变更的情况下做轮询(在某些时候,确实只能如此)。
二、应用场景
1.获取实时数据,例如数据大屏、实时股价。
2.监测进度,例如数据上传进度、下载进度。
3.监测后端处理状态,例如提交一批数据后,后端需要对数据进行分析,耗时不确定,前端需要获取分析结果,则此时需要前端轮询。
4.检测静态资源是否加载完成(一般来讲是定时同步任务),例如当函数a逻辑需要在静态资源A加载完成后才能执行,则需要在执行函数a之前,开启轮询来判断资源A是否加载完成。三、实现方式
3.1. 使用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);});
3.2. 使用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版
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-2swaitingResponse = 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版
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以保证在调用该方法时能拿到最新的statethis.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以保证在调用该方法时能拿到最新的statepolling && 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.你最常用和喜欢通义灵码编码助手哪些功能?分享一些你在使用过程中发现的小技巧。
👇欢迎点击”阅读原文“发表你的看法
浙公网安备 33010602011771号