一个健壮的前端轮询
阿里妹导读
一、前言
二、应用场景
1.获取实时数据,例如数据大屏、实时股价。
2.监测进度,例如数据上传进度、下载进度。
3.监测后端处理状态,例如提交一批数据后,后端需要对数据进行分析,耗时不确定,前端需要获取分析结果,则此时需要前端轮询。
三、实现方式
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.组件卸载或停止轮询后,仍然有轮询请求
1.开始轮询的途径有哪些?
常见的途径有页面组件加载后自动开始、按钮强制开始、参数变更后重新开始。在图3.1-3.3中,均只考虑了页面加载后自动开始轮询的情况。
2.如果有多个开启轮询的途径,怎么保证轮询的唯一性?
3.当轮询参数变更时,怎么终止旧的轮询并开始新的轮询?
这也是为了保证轮询的唯一性,同时避免旧数据覆盖新数据。
五、健壮的前端轮询
5.1. setInterval版
1.当一次定时执行时,此时可能有未响应的请求,可能需要跳过再次请求避免重复。
2.用户可能在任意时刻变更轮询的请求参数,这时即使有未响应的请求,也需要强制用新参数请求。
3.在2的情况发生后,会同时存在多个请求,当收到旧请求的响应时,需要跳过数据更新以避免旧数据覆盖。
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版
1.用户可能在任意时刻变更轮询的请求参数,这时即使有未响应的请求,也需要强制用新参数请求。
2.当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
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);
六、结语
能用AI写的代码,不允许程序员手写?!你怎么看?
以Copilot、通义灵码等为代表的AI智能编码助手成为越来越多开发者的必备工具,补全/续写代码、写单元测试、debug的功能不在话下,本期我们来聊聊你在使用AI编码助手过程中的感受和评价:
1.你认为 AI 编码助手真的能提效吗?
3.你最常用和喜欢通义灵码编码助手哪些功能?分享一些你在使用过程中发现的小技巧。
微信扫码关注该文公众号作者