# useQueue: 一个强大的 React 队列管理 Hook

目录

在 React 开发中,我们经常需要管理各种数据结构,其中队列(Queue)是一个非常重要的概念。队列遵循”先进先出”(FIFO)的原则,在任务调度、消息处理、动画序列等场景中非常有用。今天,我们将深入探讨一个自定义 Hook —— useQueue,它能够优雅地管理队列操作。

为什么需要 useQueue?

在 React 中,当我们使用 useState 管理数组状态时,每次进行队列操作都需要手动处理:

const [queue, setQueue] = useState<string[]>([])
// 传统方式添加元素到队列
const addToQueue = (item: string) => {
setQueue((prev) => [...prev, item])
}
// 传统方式从队列移除元素
const removeFromQueue = () => {
setQueue((prev) => {
const [first, ...rest] = prev
return rest
})
// 需要额外处理返回值
}

这种方式虽然可行,但存在以下问题:

  1. 代码重复: 每次操作都需要手动展开数组
  2. 逻辑分散: 队列操作逻辑分散在各个函数中
  3. 类型安全: 缺乏统一的类型约束
  4. 性能考虑: 没有优化重新渲染

useQueue 就是为了解决这些问题而生的。

useQueue 的实现原理

让我们来看看 useQueue 的核心实现:

import { useCallback, useState } from 'react'
export default function useQueue<T>(initialValue: T[]) {
const [queue, setQueue] = useState(initialValue)
const add = useCallback((element: T) => {
setQueue((s) => [...s, element])
}, [])
const remove = useCallback(() => {
let firstItem: T | undefined
setQueue((s) => {
firstItem = s[0]
return s.slice(1)
})
return firstItem
}, [])
const clear = useCallback(() => {
setQueue([])
}, [])
const at = useCallback(
(index: number) => {
return queue.at(index)
},
[queue],
)
const setQueueState = useCallback((newQueue: T[] | ((prevQueue: T[]) => T[])) => {
setQueue(newQueue)
}, [])
return {
queue,
add,
size: queue.length,
remove,
clear,
at,
setQueueState,
}
}

关键特性分析

  1. 类型安全: 使用泛型 T 确保类型安全,支持任意类型的队列
  2. 性能优化: 使用 useCallback 避免不必要的重新渲染
  3. 完整 API: 提供添加、移除、清空、访问等完整的队列操作
  4. 状态同步: 确保状态更新的一致性

核心方法详解

1. add(element: T)

将元素添加到队列末尾,这是标准的入队操作:

const add = useCallback((element: T) => {
setQueue((s) => [...s, element])
}, [])

2. remove()

从队列头部移除并返回第一个元素,这是标准的出队操作:

const remove = useCallback(() => {
let firstItem: T | undefined
setQueue((s) => {
firstItem = s[0]
return s.slice(1)
})
return firstItem
}, [])

注意: 这里使用了一个巧妙的设计,通过闭包在 setQueue 回调中获取 firstItem,然后返回它。

3. at(index: number)

获取队列中指定位置的元素,使用现代的 Array.prototype.at() 方法:

const at = useCallback(
(index: number) => {
return queue.at(index)
},
[queue],
)

4. clear()

清空整个队列:

const clear = useCallback(() => {
setQueue([])
}, [])

5. setQueueState()

提供全量设置队列内容的能力,支持直接传入数组或函数:

const setQueueState = useCallback((newQueue: T[] | ((prevQueue: T[]) => T[])) => {
setQueue(newQueue)
}, [])

使用示例

基本用法

import useQueue from './hooks/useQueue'
function TaskQueue() {
const { queue, add, remove, size, clear } = useQueue<string>([])
const handleAddTask = () => {
const taskName = `Task ${Date.now()}`
add(taskName)
}
const handleProcessTask = () => {
const task = remove()
if (task) {
console.log(`Processing: ${task}`)
}
}
return (
<div>
<h3>Task Queue ({size} tasks)</h3>
<button onClick={handleAddTask}>Add Task</button>
<button onClick={handleProcessTask}>Process Next Task</button>
<button onClick={clear}>Clear All</button>
<ul>
{queue.map((task, index) => (
<li key={index}>{task}</li>
))}
</ul>
</div>
)
}

消息队列示例

function MessageQueue() {
const { queue, add, remove, size } = useQueue<{
id: string
message: string
timestamp: Date
}>([])
const sendMessage = (message: string) => {
add({
id: crypto.randomUUID(),
message,
timestamp: new Date()
})
}
const processMessage = () => {
const message = remove()
if (message) {
console.log(`Processing message: ${message.message}`)
// 处理消息逻辑
}
}
return (
<div>
<h3>Message Queue ({size} messages)</h3>
<input
type="text"
placeholder="Enter message"
onKeyPress={(e) => {
if (e.key === 'Enter') {
sendMessage(e.currentTarget.value)
e.currentTarget.value = ''
}
}}
/>
<button onClick={processMessage}>Process Next Message</button>
<div>
{queue.map((msg) => (
<div key={msg.id}>
<span>{msg.message}</span>
<small>{msg.timestamp.toLocaleTimeString()}</small>
</div>
))}
</div>
</div>
)
}

动画序列队列

function AnimationQueue() {
const { queue, add, remove, size } = useQueue<() => void>([])
const addAnimation = (animation: () => void) => {
add(animation)
}
const playNextAnimation = () => {
const animation = remove()
if (animation) {
animation()
}
}
const addFadeIn = () => {
addAnimation(() => {
console.log('Playing fade in animation')
// 实际的动画逻辑
})
}
const addSlideIn = () => {
addAnimation(() => {
console.log('Playing slide in animation')
// 实际的动画逻辑
})
}
return (
<div>
<h3>Animation Queue ({size} animations)</h3>
<button onClick={addFadeIn}>Add Fade In</button>
<button onClick={addSlideIn}>Add Slide In</button>
<button onClick={playNextAnimation}>Play Next Animation</button>
</div>
)
}

文件上传队列

function FileUploadQueue() {
const { queue, add, remove, size, at } = useQueue<File>([])
const addFile = (file: File) => {
add(file)
}
const uploadNext = async () => {
const file = remove()
if (file) {
try {
console.log(`Uploading: ${file.name}`)
// 实际的上传逻辑
await uploadFile(file)
console.log(`Uploaded: ${file.name}`)
} catch (error) {
console.error(`Failed to upload: ${file.name}`)
// 可以选择重新加入队列或丢弃
}
}
}
const getFileInfo = (index: number) => {
const file = at(index)
return file ? { name: file.name, size: file.size } : null
}
return (
<div>
<h3>File Upload Queue ({size} files)</h3>
<input
type="file"
multiple
onChange={(e) => {
const files = Array.from(e.target.files || [])
files.forEach(addFile)
}}
/>
<button onClick={uploadNext}>Upload Next File</button>
<div>
{Array.from({ length: size }, (_, index) => {
const fileInfo = getFileInfo(index)
return fileInfo ? (
<div key={index}>
{fileInfo.name} ({(fileInfo.size / 1024).toFixed(2)} KB)
</div>
) : null
})}
</div>
</div>
)
}
async function uploadFile(file: File) {
// 模拟文件上传
return new Promise((resolve) => {
setTimeout(resolve, 1000)
})
}

高级用法

优先级队列

interface PriorityItem<T> {
item: T
priority: number
}
function usePriorityQueue<T>() {
const { queue, add, remove, size } = useQueue<PriorityItem<T>>([])
const addWithPriority = (item: T, priority: number) => {
const priorityItem: PriorityItem<T> = { item, priority }
// 按优先级插入到合适位置
const insertIndex = queue.findIndex((pq) => pq.priority < priority)
if (insertIndex === -1) {
add(priorityItem)
} else {
// 这里需要重新实现插入逻辑
// 为了简化,我们直接添加到末尾
add(priorityItem)
}
}
const removeHighestPriority = () => {
// 找到最高优先级的项目
const highestIndex = queue.reduce((maxIndex, current, currentIndex) => {
return current.priority > queue[maxIndex].priority ? currentIndex : maxIndex
}, 0)
// 移除并返回最高优先级的项目
const [removed] = queue.splice(highestIndex, 1)
return removed?.item
}
return {
queue: queue.map((pq) => pq.item),
add: addWithPriority,
remove: removeHighestPriority,
size,
}
}

循环队列

function useCircularQueue<T>(maxSize: number) {
const { queue, add, remove, size, clear } = useQueue<T>([])
const addToCircular = (element: T) => {
if (size >= maxSize) {
// 移除最旧的元素
remove()
}
add(element)
}
const getNext = (currentIndex: number) => {
return (currentIndex + 1) % size
}
const getPrevious = (currentIndex: number) => {
return (currentIndex - 1 + size) % size
}
return {
queue,
add: addToCircular,
remove,
size,
clear,
getNext,
getPrevious,
isFull: size >= maxSize,
isEmpty: size === 0,
}
}

性能优化建议

1. 使用 useCallback 优化

useQueue 已经使用了 useCallback 来优化性能,但如果你在组件中使用队列操作,也要注意:

// ✅ 推荐:使用 useCallback 包装队列操作
const handleAddItem = useCallback(
(item: T) => {
add(item)
},
[add],
)
// ❌ 避免:每次渲染都创建新函数
const handleAddItem = (item: T) => {
add(item)
}

2. 批量操作

对于需要大量添加元素的场景,考虑批量操作:

const addMultiple = useCallback(
(items: T[]) => {
setQueueState((prev) => [...prev, ...items])
},
[setQueueState],
)

3. 使用 React.memo

如果队列组件是纯组件,使用 React.memo 优化:

const QueueDisplay = React.memo<{ queue: T[] }>(({ queue }) => {
return (
<div>
{queue.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
)
})

测试用例

让我们为 useQueue 编写一些测试用例:

import { renderHook, act } from '@testing-library/react'
import useQueue from './useQueue'
describe('useQueue', () => {
it('should initialize with empty queue', () => {
const { result } = renderHook(() => useQueue<string>([]))
expect(result.current.queue).toEqual([])
expect(result.current.size).toBe(0)
})
it('should add elements to queue', () => {
const { result } = renderHook(() => useQueue<string>([]))
act(() => {
result.current.add('item1')
result.current.add('item2')
})
expect(result.current.queue).toEqual(['item1', 'item2'])
expect(result.current.size).toBe(2)
})
it('should remove elements from queue', () => {
const { result } = renderHook(() => useQueue<string>(['item1', 'item2']))
let removedItem: string | undefined
act(() => {
removedItem = result.current.remove()
})
expect(removedItem).toBe('item1')
expect(result.current.queue).toEqual(['item2'])
expect(result.current.size).toBe(1)
})
it('should clear queue', () => {
const { result } = renderHook(() => useQueue<string>(['item1', 'item2']))
act(() => {
result.current.clear()
})
expect(result.current.queue).toEqual([])
expect(result.current.size).toBe(0)
})
it('should access elements by index', () => {
const { result } = renderHook(() => useQueue<string>(['item1', 'item2', 'item3']))
expect(result.current.at(0)).toBe('item1')
expect(result.current.at(1)).toBe('item2')
expect(result.current.at(2)).toBe('item3')
expect(result.current.at(3)).toBeUndefined()
expect(result.current.at(-1)).toBe('item3')
})
})

与其他数据结构的对比

特性useQueueuseState + ArrayuseReducer
队列操作内置方法手动实现手动实现
类型安全完全支持基础支持完全支持
性能优化已优化需要手动优化需要手动优化
学习成本中等
灵活性中等

最佳实践

1. 选择合适的队列类型

  • 简单队列: 使用 useQueue
  • 优先级队列: 扩展 useQueue 或使用专门的 hook
  • 循环队列: 使用 useCircularQueue
  • 双端队列: 考虑使用 useDeque

2. 错误处理

const safeRemove = () => {
if (size === 0) {
console.warn('Queue is empty')
return undefined
}
return remove()
}

3. 队列大小限制

const addWithLimit = (element: T, maxSize: number) => {
if (size >= maxSize) {
console.warn('Queue is full')
return false
}
add(element)
return true
}

4. 持久化

const { queue, add, remove, clear } = useQueue<string>(() => {
const saved = localStorage.getItem('myQueue')
return saved ? JSON.parse(saved) : []
})
// 在队列变化时保存到 localStorage
useEffect(() => {
localStorage.setItem('myQueue', JSON.stringify(queue))
}, [queue])

总结

useQueue 是一个强大而实用的自定义 Hook,它简化了 React 中的队列操作管理。通过提供完整的队列 API 和优秀的性能优化,它让队列操作变得更加优雅和高效。

主要优势:

  • 类型安全: 完全支持 TypeScript
  • 性能优化: 使用 useCallback 避免不必要的重新渲染
  • API 完整: 提供所有必要的队列操作方法
  • 易于使用: 学习成本低,使用简单

适用场景:

  • 任务队列管理
  • 消息处理系统
  • 动画序列控制
  • 文件上传队列
  • 事件处理队列

虽然它不能完全替代 useStateuseReducer,但在需要队列操作的场景中,它确实是一个很好的工具。记住,选择合适的状态管理方案应该基于具体的需求和场景。

useQueue 完整实现
import { useCallback, useState } from 'react'
/**
* 操作队列hook
* @param initialValue 初始值,接受一个数组,可以是任意类型的数组
* @returns add 将元素添加到队列末尾
* @returns remove 从队列中移除并返回第一个元素
* @returns clear 清空队列方法
* @return at 获取队列中指定位置的元素方法
* @returns queue 当前队列
* @returns size 队列长度
* @returns setQueueState 全量设置队列内容
*/
export default function useQueue<T>(initialValue: T[]) {
const [queue, setQueue] = useState(initialValue)
const add = useCallback((element: T) => {
setQueue((s) => [...s, element])
}, [])
const remove = useCallback(() => {
let firstItem: T | undefined
setQueue((s) => {
firstItem = s[0]
return s.slice(1)
})
return firstItem
}, [])
const clear = useCallback(() => {
setQueue([])
}, [])
const at = useCallback(
(index: number) => {
return queue.at(index)
},
[queue],
)
const setQueueState = useCallback((newQueue: T[] | ((prevQueue: T[]) => T[])) => {
setQueue(newQueue)
}, [])
return {
queue,
add,
size: queue.length,
remove,
clear,
at,
setQueueState,
}
}

希望这篇文章能帮助你更好地理解和使用 useQueue Hook!如果你有任何问题或建议,欢迎在评论区讨论。


评论