![](https://news.xinpengboligang.com/upload/keji/d7ec9900a342aaf21197f635a40ac2a3.jpeg)
導讀
本文討論了在不使用websocket做服務端推送的情況下,如何寫出一個健壯的前端輪詢。文章提供了一些常見的前端輪詢的應用場景以及可能遇到的問題,歡迎大傢一起討論。
一、前言
本文的前端輪詢主要討論的是定時異步任務,定時異步任務相比與定時同步任務需要考慮更多的因素。這裡的異步任務一般包括發送網絡請求及響應後的狀態更新。從技術層面上,需要考慮到開啟定時、發送請求、狀態更新之間的邏輯順序。此外,本文不討論利用websocket做服務端推送,隻考慮在僅前端變更的情況下做輪詢(在某些時候,確實隻能如此)。
二、應用場景
1.獲取實時數據,例如數據大屏、實時股價。
2.監測進度,例如數據上傳進度、下載進度。
3.監測後端處理狀態,例如提交一批數據後,後端需要對數據進行分析,耗時不確定,前端需要獲取分析結果,則此時需要前端輪詢。
4.檢測靜態資源是否加載完成(一般來講是定時同步任務),例如當函數a邏輯需要在靜態資源A加載完成後才能執行,則需要在執行函數a之前,開啟輪詢來判斷資源A是否加載完成。
三、實現方式
3.1. 使用setInterval
![](https://news.xinpengboligang.com/upload/keji/c1632caa3cbc6cc5f50ae182c20b5ebf.jpeg)
如果是定時同步任務沒有問題,但對於輪詢這樣的定時異步任務需要註意響應時間和定時時間。如圖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
![](https://news.xinpengboligang.com/upload/keji/9edc3c75e240da8139fb3189b71b579e.jpeg)
使用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版
![](https://news.xinpengboligang.com/upload/keji/1b528a0a35a5afbc3517d886f8174e30.jpeg)
如圖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版
![](https://news.xinpengboligang.com/upload/keji/02bc09371af9e1de61c25b01368e0396.jpeg)
如圖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節),非常歡迎大傢加入討論、提供意見,豐富這些內容。
作者:素柯
來源:微信公眾號:阿裡雲開發者
出處
:https://mp.weixin.qq.com/s/6AqmZJUwPHrbyNMUejNnCw