引言
在现代Web应用中,实时数据更新是一个常见需求,在AI场景下会用到很多。虽然WebSocket是实现实时通信的理想选择,但在某些情况下,轮询(Polling)仍然是一种简单有效的替代方案。本文将介绍一个强大的Vue 3组合式函数usePoller
,它提供了一种优雅的方式来处理轮询请求,并且包含了错误重试、指数退避等高级特性。
usePoller的核心功能
usePoller
是一个专为Vue 3设计的组合式函数,它封装了轮询的复杂逻辑,提供了以下核心功能:
- 定时发送请求:按照指定的时间间隔自动发送请求
- 智能错误处理:内置错误重试机制,支持指数退避策略
- 灵活的配置选项:支持立即执行、最大重试次数等配置
- 生命周期管理:组件卸载时自动清理资源
- 状态监控:提供轮询状态的实时反馈
usePoller源码
ts
import { onScopeDispose, ref } from 'vue'
/**
* 请求函数类型定义
* @template T 响应数据类型
* @template P 请求参数类型
*/
export type RequestFunction<T, P> = (params?: P) => Promise<T>
/**
* 回调函数类型定义,用于处理成功的响应
* @template T 响应数据类型
*/
type CallbackFunction<T> = (response: T) => void
/**
* 错误回调函数类型定义
* 返回 true 表示继续重试,false 表示停止轮询
*/
type ErrorCallbackFunction = (error: any) => boolean | Promise<boolean>
/**
* 停止回调函数类型定义
* @param reason 停止原因:'error'(错误)、'manual'(手动)、'max_retries'(达到最大重试次数)、'component_unmounted'(组件卸载)
* @param error 当停止原因为错误时,提供错误对象
*/
type StopCallbackFunction = (reason: 'error' | 'manual' | 'max_retries' | 'component_unmounted', error?: any) => void
/**
* usePoller - 一个用于处理轮询请求的Vue组合式函数
*
* 功能:
* - 定时发送请求并处理响应
* - 支持错误重试机制,包括指数退避策略
* - 支持自定义错误处理
* - 提供轮询状态监控
* - 组件卸载时自动清理
*
* @template T 响应数据类型
* @template P 请求参数类型,默认为void
*
* @returns {Object} 包含poll和stop方法以及isPolling状态的对象
*
* @example
* // 基本使用
* const { poll, stop } = usePoller<ApiResponse, ApiParams>()
*
* poll(
* fetchData, // 请求函数
* 5000, // 轮询间隔(毫秒)
* handleResponse, // 响应处理回调
* { id: '12345' }, // 请求参数
* {
* immediate: true, // 是否立即执行第一次请求
* maxRetries: 3, // 最大重试次数
* onError: (error) => {
* // 根据错误类型决定是否继续重试
* return error.status === 503 // 只有服务暂时不可用时才重试
* },
* onStop: (reason, error) => {
* // 处理轮询停止事件
* console.log(`轮询已停止,原因: ${reason}`, error)
* }
* }
* )
*
* // 手动停止轮询
* stop()
*/
export function usePoller<T, P = void>() {
const interval = ref<number>(1000)
const timer = ref<ReturnType<typeof setInterval> | null>(null)
const callback = ref<CallbackFunction<T> | null>(null)
const errorCallback = ref<ErrorCallbackFunction | null>(null)
const stopCallback = ref<StopCallbackFunction | null>(null)
const request = ref<RequestFunction<T, P> | null>(null)
const doImmediate = ref<boolean>(false)
const maxRetries = ref<number>(3)
const isPolling = ref<boolean>(false) // 轮询状态标志
let retryCount = 0
let requestParams: P | undefined
let lastError: any = null
/**
* 停止轮询
* @param reason 停止原因
*/
const stopPolling = (reason: 'error' | 'manual' | 'max_retries' | 'component_unmounted' = 'manual'): void => {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
const wasPolling = isPolling.value
isPolling.value = false // 设置轮询状态为停止
retryCount = 0
// 只有在之前正在轮询的情况下才触发回调
if (wasPolling && stopCallback.value) {
stopCallback.value(reason, reason === 'error' ? lastError : undefined)
}
}
/**
* 重试函数,处理请求失败的情况
* @param error 错误对象
* @returns 是否继续轮询
*/
const retry = async (error: any): Promise<boolean> => {
// 保存最后一次错误
lastError = error
// 检查轮询是否已停止
if (!isPolling.value) {
return false
}
// 如果有错误回调,让用户决定是否继续重试
if (errorCallback.value) {
const shouldRetry = await errorCallback.value(error)
if (!shouldRetry) {
return false // 不再重试,外部会处理停止轮询
}
}
if (retryCount < maxRetries.value) {
retryCount++
// 改进的指数退避策略: 2^retryCount * 500ms 并添加随机抖动
const backoffTime = Math.min(1000 * Math.pow(2, retryCount - 1) + Math.random() * 1000, 30000)
await new Promise(resolve => setTimeout(resolve, backoffTime))
// 再次检查轮询是否已停止(可能在等待期间被停止)
if (!isPolling.value) {
return false
}
if (request.value && callback.value) {
try {
const response = await request.value(requestParams)
callback.value(response)
return true
} catch (error) {
console.error('Error during retry:', error)
return retry(error)
}
}
} else {
console.error('Max retry attempts reached')
// 达到最大重试次数,通知外部
stopPolling('max_retries')
// 如果没有错误回调,则在达到最大重试次数后抛出错误
if (!errorCallback.value) {
throw error
}
return false // 达到最大重试次数,外部会处理停止轮询
}
return false
}
/**
* 启动轮询
*/
const start = async (): Promise<void> => {
stopPolling() // 先停止之前的轮询
isPolling.value = true // 设置轮询状态为启动
lastError = null // 重置错误
// 如果设置了立即执行,则立即发送第一次请求
if (doImmediate.value && request.value && callback.value) {
try {
// 检查轮询是否已停止
if (!isPolling.value) {
return
}
const response = await request.value(requestParams)
callback.value(response)
} catch (error) {
console.error(error)
const shouldContinue = await retry(error)
if (!shouldContinue) {
stopPolling('error') // 如果不应继续重试,停止轮询
return // 直接返回,不启动定时器
}
}
doImmediate.value = false
}
// 再次检查轮询是否已停止(可能在immediate执行期间被停止)
if (!isPolling.value) {
return
}
// 设置定时器,开始轮询
timer.value = setInterval(async () => {
// 检查轮询是否已停止
if (!isPolling.value) {
stopPolling() // 确保定时器被清除
return
}
if (request.value && callback.value) {
try {
const response = await request.value(requestParams)
callback.value(response)
// 成功后重置重试计数
retryCount = 0
} catch (error) {
console.error(error)
const shouldContinue = await retry(error)
// 如果 retry 返回 false,表示不应继续重试,此时需要停止轮询
if (!shouldContinue) {
stopPolling('error')
}
}
}
}, interval.value)
}
/**
* 开始轮询
* @param req 请求函数
* @param delay 轮询间隔(毫秒)
* @param cb 响应处理回调
* @param params 请求参数
* @param options 配置选项
* @param options.immediate 是否立即执行第一次请求
* @param options.maxRetries 最大重试次数
* @param options.onError 错误处理回调
* @param options.onStop 停止轮询回调
*/
const poll = (
req: RequestFunction<T, P>,
delay: number,
cb: CallbackFunction<T>,
params?: P,
options: {
immediate?: boolean
maxRetries?: number
onError?: ErrorCallbackFunction
onStop?: StopCallbackFunction
} = {},
): void => {
const { immediate = false, onError, onStop, maxRetries: retries } = options
request.value = req
interval.value = delay
callback.value = cb
doImmediate.value = immediate
requestParams = params
if (retries !== undefined) {
maxRetries.value = retries
}
if (onError) {
errorCallback.value = onError
}
if (onStop) {
stopCallback.value = onStop
}
start()
}
/**
* 手动停止轮询
*/
const stop = (): void => {
stopPolling('manual')
}
// 组件卸载时自动清理
onScopeDispose(() => {
stopPolling('component_unmounted')
})
return {
poll,
stop,
isPolling, // 暴露轮询状态,方便外部检查
}
}
export default usePoller
基本用法
下面是usePoller
的基本用法示例:
vue
<script setup>
import { usePoller } from '@/composables/use-poller'
import { ref } from 'vue'
// 定义API请求函数
const fetchData = async (params) => {
const response = await fetch(`https://api.example.com/data?id=${params.id}`)
if (!response.ok) {
throw new Error('请求失败')
}
return response.json()
}
// 使用usePoller
const { poll, stop, isPolling } = usePoller()
const data = ref(null)
// 处理响应数据
const handleResponse = (response) => {
data.value = response
console.log('数据已更新:', response)
}
// 开始轮询
poll(
fetchData, // 请求函数
5000, // 轮询间隔(毫秒)
handleResponse, // 响应处理回调
{ id: '12345' }, // 请求参数
{
immediate: true, // 立即执行第一次请求
maxRetries: 3, // 最大重试次数
}
)
</script>
<template>
<div>
<h1>实时数据</h1>
<div v-if="isPolling">正在获取数据...</div>
<pre v-if="data">{{ JSON.stringify(data, null, 2) }}</pre>
<button @click="stop">停止轮询</button>
</div>
</template>
使用场景与示例
场景一:实时数据仪表盘
在监控仪表盘中,需要定期刷新数据以显示最新状态。
vue
<script setup>
import { usePoller } from '@/composables/use-poller'
import { ref } from 'vue'
const { poll, stop, isPolling } = usePoller()
const dashboardData = ref({})
const loading = ref(false)
// 获取仪表盘数据
const fetchDashboardData = async () => {
loading.value = true
try {
const response = await fetch('/api/dashboard/stats')
if (!response.ok) throw new Error('获取仪表盘数据失败')
return await response.json()
} finally {
loading.value = false
}
}
// 处理响应
const updateDashboard = (data) => {
dashboardData.value = data
}
// 开始轮询,每30秒更新一次
poll(
fetchDashboardData,
30000,
updateDashboard,
undefined,
{
immediate: true,
onError: (error) => {
console.error('仪表盘数据更新失败:', error)
return true // 继续重试
},
onStop: (reason) => {
console.log(`仪表盘数据更新已停止,原因: ${reason}`)
}
}
)
</script>
<template>
<div class="dashboard">
<div class="status-bar">
<span v-if="isPolling && !loading">自动更新中</span>
<span v-if="loading">加载中...</span>
<button @click="stop">停止自动更新</button>
</div>
<div class="dashboard-content">
<!-- 仪表盘内容 -->
<div v-for="(value, key) in dashboardData" :key="key" class="metric">
<h3>{{ key }}</h3>
<div class="value">{{ value }}</div>
</div>
</div>
</div>
</template>
场景二:长时间运行的任务状态检查
当启动一个长时间运行的后台任务时,可以使用轮询来检查任务的进度。
vue
<script setup>
import { usePoller } from '@/composables/use-poller'
import { ref } from 'vue'
const { poll, stop, isPolling } = usePoller()
const taskStatus = ref({
status: 'pending',
progress: 0,
result: null
})
// 启动任务
const startTask = async () => {
const response = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'data-processing' })
})
if (!response.ok) throw new Error('启动任务失败')
const { taskId } = await response.json()
// 开始轮询任务状态
pollTaskStatus(taskId)
return taskId
}
// 检查任务状态
const checkTaskStatus = async (taskId) => {
const response = await fetch(`/api/tasks/${taskId}`)
if (!response.ok) throw new Error('获取任务状态失败')
return await response.json()
}
// 处理任务状态更新
const handleTaskUpdate = (data) => {
taskStatus.value = data
// 如果任务已完成或失败,停止轮询
if (['completed', 'failed'].includes(data.status)) {
stop()
}
}
// 开始轮询任务状态
const pollTaskStatus = (taskId) => {
poll(
() => checkTaskStatus(taskId),
2000, // 每2秒检查一次
handleTaskUpdate,
undefined,
{
immediate: true,
maxRetries: 5,
onError: (error) => {
console.error('检查任务状态失败:', error)
// 只有在网络错误时继续重试,其他错误停止轮询
return error.name === 'NetworkError'
}
}
)
}
</script>
<template>
<div class="task-monitor">
<h2>任务状态监控</h2>
<div v-if="!isPolling && taskStatus.status === 'pending'">
<button @click="startTask">启动任务</button>
</div>
<div v-else class="status-display">
<div class="status">状态: {{ taskStatus.status }}</div>
<div v-if="taskStatus.status === 'processing'" class="progress">
<progress :value="taskStatus.progress" max="100"></progress>
<span>{{ taskStatus.progress }}%</span>
</div>
<div v-if="taskStatus.status === 'completed'" class="result">
<h3>任务结果:</h3>
<pre>{{ JSON.stringify(taskStatus.result, null, 2) }}</pre>
</div>
<div v-if="taskStatus.status === 'failed'" class="error">
<h3>任务失败:</h3>
<p>{{ taskStatus.error }}</p>
</div>
</div>
</div>
</template>
场景三:聊天应用中的消息轮询
在不使用WebSocket的简单聊天应用中,可以使用轮询来获取新消息。
vue
<script setup>
import { usePoller } from '@/composables/use-poller'
import { ref, computed } from 'vue'
const { poll, stop, isPolling } = usePoller()
const messages = ref([])
const lastMessageId = ref(0)
const newMessage = ref('')
const userId = ref('user123') // 假设这是当前用户ID
// 获取新消息
const fetchNewMessages = async (params) => {
const response = await fetch(`/api/chat/messages?after=${params.lastId}`)
if (!response.ok) throw new Error('获取消息失败')
return await response.json()
}
// 处理新消息
const handleNewMessages = (data) => {
if (data.messages && data.messages.length > 0) {
messages.value = [...messages.value, ...data.messages]
// 更新最后一条消息的ID
const maxId = Math.max(...data.messages.map(m => m.id))
if (maxId > lastMessageId.value) {
lastMessageId.value = maxId
}
}
}
// 发送消息
const sendMessage = async () => {
if (!newMessage.value.trim()) return
try {
const response = await fetch('/api/chat/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: newMessage.value,
userId: userId.value
})
})
if (!response.ok) throw new Error('发送消息失败')
// 清空输入框
newMessage.value = ''
// 获取最新消息(包括刚发送的)
const data = await fetchNewMessages({ lastId: lastMessageId.value })
handleNewMessages(data)
} catch (error) {
console.error('发送消息失败:', error)
}
}
// 开始轮询新消息
const startPolling = () => {
poll(
fetchNewMessages,
3000, // 每3秒检查一次新消息
handleNewMessages,
{ lastId: lastMessageId.value },
{
immediate: true,
maxRetries: 10,
onError: (error) => {
console.error('获取新消息失败:', error)
// 网络错误时继续重试
return true
},
onStop: (reason) => {
if (reason !== 'manual') {
alert('消息更新已停止,请刷新页面')
}
}
}
)
}
// 组件挂载时开始轮询
startPolling()
// 计算属性:按时间排序的消息
const sortedMessages = computed(() => {
return [...messages.value].sort((a, b) => a.timestamp - b.timestamp)
})
</script>
<template>
<div class="chat-container">
<div class="chat-header">
<h2>聊天室</h2>
<div class="status">
<span v-if="isPolling" class="online">在线</span>
<span v-else class="offline">离线</span>
<button v-if="isPolling" @click="stop">暂停更新</button>
<button v-else @click="startPolling">恢复更新</button>
</div>
</div>
<div class="messages-container">
<div v-if="messages.length === 0" class="no-messages">
暂无消息
</div>
<div v-else class="message-list">
<div
v-for="msg in sortedMessages"
:key="msg.id"
:class="['message', msg.userId === userId ? 'own' : 'other']"
>
<div class="message-header">
<span class="username">{{ msg.username }}</span>
<span class="time">{{ new Date(msg.timestamp).toLocaleTimeString() }}</span>
</div>
<div class="message-content">{{ msg.content }}</div>
</div>
</div>
</div>
<div class="message-input">
<input
v-model="newMessage"
placeholder="输入消息..."
@keyup.enter="sendMessage"
/>
<button @click="sendMessage">发送</button>
</div>
</div>
</template>
场景四:带有条件轮询的表单验证
在某些表单中,可能需要在用户输入特定值后验证该值是否可用(如用户名检查)。
vue
<script setup>
import { usePoller } from '@/composables/use-poller'
import { ref, watch } from 'vue'
const { poll, stop, isPolling } = usePoller()
const username = ref('')
const validationStatus = ref({
isChecking: false,
isValid: null,
message: ''
})
// 检查用户名是否可用
const checkUsername = async (params) => {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500))
const response = await fetch(`/api/users/check-username?username=${params.username}`)
if (!response.ok) throw new Error('验证失败')
return await response.json()
}
// 处理验证结果
const handleValidationResult = (result) => {
validationStatus.value = {
isChecking: false,
isValid: result.available,
message: result.available ? '用户名可用' : '用户名已被占用'
}
// 如果用户名可用或已明确不可用,停止轮询
if (result.available || result.status === 'final') {
stop()
}
}
// 监听用户名变化
watch(username, (newValue) => {
// 停止之前的轮询
stop()
// 重置验证状态
validationStatus.value = {
isChecking: false,
isValid: null,
message: ''
}
// 用户名长度至少3个字符才开始验证
if (newValue.length >= 3) {
validationStatus.value.isChecking = true
// 开始轮询验证
poll(
checkUsername,
2000, // 每2秒检查一次
handleValidationResult,
{ username: newValue },
{
immediate: true,
maxRetries: 3,
onError: (error) => {
console.error('用户名验证失败:', error)
validationStatus.value.message = '验证服务暂时不可用'
validationStatus.value.isChecking = false
return false // 不再重试
}
}
)
}
})
</script>
<template>
<div class="username-form">
<div class="form-group">
<label for="username">用户名</label>
<input
id="username"
v-model="username"
type="text"
placeholder="请输入用户名"
/>
<div class="validation-status">
<span v-if="validationStatus.isChecking || isPolling" class="checking">
正在验证...
</span>
<span
v-else-if="validationStatus.isValid !== null"
:class="validationStatus.isValid ? 'valid' : 'invalid'"
>
{{ validationStatus.message }}
</span>
<span v-else-if="username.length > 0 && username.length < 3" class="hint">
用户名至少需要3个字符
</span>
</div>
</div>
</div>
</template>
场景五:带有智能重试的文件上传状态检查
在大文件上传场景中,客户端发起上传后,可以使用轮询来检查服务器端的处理状态。
vue
<script setup>
import { usePoller } from '@/composables/use-poller'
import { ref, computed } from 'vue'
const { poll, stop, isPolling } = usePoller()
const file = ref(null)
const uploadStatus = ref({
state: 'idle', // idle, uploading, processing, completed, failed
progress: 0,
uploadId: null,
result: null,
error: null
})
// 计算上传状态文本
const statusText = computed(() => {
switch (uploadStatus.value.state) {
case 'idle': return '准备上传';
case 'uploading': return `上传中 ${uploadStatus.value.progress}%`;
case 'processing': return '服务器处理中...';
case 'completed': return '上传完成';
case 'failed': return `上传失败: ${uploadStatus.value.error}`;
default: return '';
}
})
// 处理文件选择
const handleFileChange = (event) => {
file.value = event.target.files[0]
// 重置上传状态
uploadStatus.value = {
state: 'idle',
progress: 0,
uploadId: null,
result: null,
error: null
}
}
// 上传文件
const uploadFile = async () => {
if (!file.value) return
try {
uploadStatus.value.state = 'uploading'
// 创建FormData对象
const formData = new FormData()
formData.append('file', file.value)
// 上传文件
const response = await fetch('/api/files/upload', {
method: 'POST',
body: formData,
// 使用上传进度事件
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
uploadStatus.value.progress = percentCompleted
}
})
if (!response.ok) throw new Error('上传失败')
const data = await response.json()
uploadStatus.value.uploadId = data.uploadId
uploadStatus.value.state = 'processing'
// 开始轮询检查处理状态
pollProcessingStatus(data.uploadId)
} catch (error) {
console.error('文件上传失败:', error)
uploadStatus.value.state = 'failed'
uploadStatus.value.error = error.message
}
}
// 检查文件处理状态
const checkProcessingStatus = async (params) => {
const response = await fetch(`/api/files/status/${params.uploadId}`)
if (!response.ok) throw new Error('获取处理状态失败')
return await response.json()
}
// 处理状态更新
const handleStatusUpdate = (data) => {
// 更新处理进度
if (data.state === 'processing') {
uploadStatus.value.progress = data.progress || 0
}
// 处理完成
else if (data.state === 'completed') {
uploadStatus.value.state = 'completed'
uploadStatus.value.result = data.result
stop() // 停止轮询
}
// 处理失败
else if (data.state === 'failed') {
uploadStatus.value.state = 'failed'
uploadStatus.value.error = data.error
stop() // 停止轮询
}
}
// 开始轮询处理状态
const pollProcessingStatus = (uploadId) => {
poll(
checkProcessingStatus,
3000, // 每3秒检查一次
handleStatusUpdate,
{ uploadId },
{
immediate: true,
maxRetries: 5,
// 智能重试策略
onError: (error) => {
console.error('检查处理状态失败:', error)
// 如果是服务器暂时不可用,继续重试
if (error.status === 503) {
return true
}
// 如果是其他错误,停止轮询并更新状态
uploadStatus.value.state = 'failed'
uploadStatus.value.error = '处理状态检查失败: ' + error.message
return false
},
onStop: (reason, error) => {
if (reason === 'max_retries') {
uploadStatus.value.state = 'failed'
uploadStatus.value.error = '处理状态检查超时,请稍后查看结果'
}
}
}
)
}
// 取消上传
const cancelUpload = () => {
stop() // 停止轮询
// 如果有上传ID,通知服务器取消
if (uploadStatus.value.uploadId) {
fetch(`/api/files/cancel/${uploadStatus.value.uploadId}`, {
method: 'POST'
}).catch(error => {
console.error('取消上传失败:', error)
})
}
// 重置状态
uploadStatus.value = {
state: 'idle',
progress: 0,
uploadId: null,
result: null,
error: null
}
// 清除文件选择
file.value = null
document.getElementById('file-input').value = ''
}
</script>
<template>
<div class="file-uploader">
<h2>文件上传</h2>
<div class="upload-form">
<div class="file-input">
<input
id="file-input"
type="file"
@change="handleFileChange"
:disabled="uploadStatus.state !== 'idle'"
/>
<div v-if="file" class="file-info">
<span>{{ file.name }}</span>
<span>({{ (file.size / 1024).toFixed(2) }} KB)</span>
</div>
</div>
<div class="actions">
<button
@click="uploadFile"
:disabled="!file || uploadStatus.state !== 'idle'"
>
上传
</button>
<button
@click="cancelUpload"
:disabled="uploadStatus.state === 'idle' || uploadStatus.state === 'completed'"
>
取消
</button>
</div>
</div>
<div v-if="uploadStatus.state !== 'idle'" class="status-container">
<div class="status-text">{{ statusText }}</div>
<div v-if="['uploading', 'processing'].includes(uploadStatus.state)" class="progress-bar">
<div class="progress" :style="{width: `${uploadStatus.progress}%`}"></div>
</div>
<div v-if="uploadStatus.state === 'completed'" class="result">
<h3>处理结果:</h3>
<div class="result-content">
<a :href="uploadStatus.result.url" target="_blank">查看文件</a>
<div class="metadata">
<div>文件ID: {{ uploadStatus.result.fileId }}</div>
<div>处理时间: {{ uploadStatus.result.processedAt }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
高级使用技巧
1. 动态调整轮询间隔
在某些情况下,可能需要根据服务器负载或响应时间动态调整轮询间隔。
javascript
// 初始轮询间隔为5秒
let pollInterval = 5000
// 根据响应时间动态调整轮询间隔
const adaptivePolling = async () => {
const { poll, stop } = usePoller()
const fetchWithTiming = async (params) => {
const startTime = Date.now()
const result = await fetchData(params)
const responseTime = Date.now() - startTime
// 根据响应时间调整下一次轮询间隔
if (responseTime > 1000) {
// 服务器响应慢,增加间隔
pollInterval = Math.min(pollInterval * 1.5, 30000) // 最大30秒
} else if (responseTime < 200 && pollInterval > 2000) {
// 服务器响应快,减少间隔
pollInterval = Math.max(pollInterval * 0.8, 2000) // 最小2秒
}
return result
}
const handleResponse = (data) => {
// 处理数据...
// 停止当前轮询并以新的间隔重新开始
stop()
poll(fetchWithTiming, pollInterval, handleResponse, params, { immediate: false })
}
// 开始初始轮询
poll(fetchWithTiming, pollInterval, handleResponse, params, { immediate: true })
}
2. 条件轮询
只在特定条件满足时才进行轮询:
javascript
import { usePoller } from '@/composables/use-poller'
import { ref, watch } from 'vue'
const { poll, stop, isPolling } = usePoller()
const shouldPoll = ref(false)
const data = ref(null)
// 监听条件变化
watch(shouldPoll, (newValue) => {
if (newValue && !isPolling.value) {
// 开始轮询
poll(fetchData, 5000, handleResponse, params, { immediate: true })
} else if (!newValue && isPolling.value) {
// 停止轮询
stop()
}
})
// 根据用户交互或其他条件改变shouldPoll的值
const startDataUpdates = () => {
shouldPoll.value = true
}
const stopDataUpdates = () => {
shouldPoll.value = false
}
3. 多轮询协调
在复杂应用中,可能需要同时管理多个轮询:
javascript
import { usePoller } from '@/composables/use-poller'
import { reactive } from 'vue'
// 创建多个轮询实例
const userDataPoller = usePoller()
const notificationsPoller = usePoller()
const systemStatusPoller = usePoller()
// 统一管理所有轮询
const pollers = reactive({
userData: userDataPoller,
notifications: notificationsPoller,
systemStatus: systemStatusPoller
})
// 启动所有轮询
const startAllPollers = () => {
pollers.userData.poll(fetchUserData, 10000, handleUserData, null, { immediate: true })
pollers.notifications.poll(fetchNotifications, 5000, handleNotifications, null, { immediate: true })
pollers.systemStatus.poll(fetchSystemStatus, 30000, handleSystemStatus, null, { immediate: true })
}
// 停止所有轮询
const stopAllPollers = () => {
Object.values(pollers).forEach(poller => poller.stop())
}
// 根据网络状态管理轮询
window.addEventListener('online', startAllPollers)
window.addEventListener('offline', stopAllPollers)
性能优化建议
使用轮询时,需要注意以下性能优化点:
- 合理设置轮询间隔:间隔太短会增加服务器负载,间隔太长会影响实时性
- 使用条件轮询:只在需要时才启动轮询,不需要时及时停止
- 实现指数退避:在错误情况下,逐渐增加重试间隔
- 设置最大重试次数:避免无限重试导致资源浪费
- 组件卸载时清理:确保在组件销毁时停止轮询
结论
usePoller
组合式函数为Vue 3应用提供了一种强大而灵活的轮询解决方案。通过封装复杂的轮询逻辑,它使开发者能够轻松实现各种实时数据更新场景,同时内置的错误处理和重试机制确保了应用的稳定性和可靠性。
虽然在许多情况下WebSocket可能是更好的实时通信选择,但轮询仍然是一种简单、兼容性好的替代方案,特别适用于不需要高频率更新的场景。通过本文介绍的usePoller
和各种使用场景,你应该能够在自己的Vue 3应用中轻松实现高质量的轮询功能。