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

Table of Contents

在 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!如果你有任何问题或建议,欢迎在评论区讨论。

My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


相关文章

评论