mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-28 05:11:24 +08:00
refactor(useSmoothStream): optimize chunk handling and state management (#8514)
* refactor(useSmoothStream): optimize chunk handling and state management - Replaced state management with refs for chunk queue to improve performance and reduce unnecessary re-renders. - Introduced Intl.Segmenter for better character segmentation based on language support. - Updated rendering logic to ensure final text is displayed correctly when the stream ends. - Cleaned up unused effects and comments for improved code clarity. * refactor(useSmoothStream): move segmenter initialization outside of addChunk function - Moved the initialization of Intl.Segmenter to the top of the file for better performance and to avoid redundant instantiation within the addChunk callback. - This change enhances the efficiency of chunk processing in the useSmoothStream hook.
This commit is contained in:
parent
6cc29c5005
commit
84157f7bd8
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
interface UseSmoothStreamOptions {
|
||||
onUpdate: (text: string) => void
|
||||
@ -7,15 +7,18 @@ interface UseSmoothStreamOptions {
|
||||
initialText?: string
|
||||
}
|
||||
|
||||
const languages = ['en-US', 'es-ES', 'zh-CN', 'zh-TW', 'ja-JP', 'ru-RU', 'el-GR', 'fr-FR', 'pt-PT']
|
||||
const segmenter = new Intl.Segmenter(languages)
|
||||
|
||||
export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialText = '' }: UseSmoothStreamOptions) => {
|
||||
const [chunkQueue, setChunkQueue] = useState<string[]>([])
|
||||
const chunkQueueRef = useRef<string[]>([])
|
||||
const animationFrameRef = useRef<number | null>(null)
|
||||
const displayedTextRef = useRef<string>(initialText)
|
||||
const lastUpdateTimeRef = useRef<number>(0)
|
||||
|
||||
const addChunk = useCallback((chunk: string) => {
|
||||
const chars = Array.from(chunk)
|
||||
setChunkQueue((prev) => [...prev, ...(chars || [])])
|
||||
const chars = Array.from(segmenter.segment(chunk)).map((s) => s.segment)
|
||||
chunkQueueRef.current = [...chunkQueueRef.current, ...(chars || [])]
|
||||
}, [])
|
||||
|
||||
const reset = useCallback(
|
||||
@ -23,7 +26,7 @@ export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialTe
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current)
|
||||
}
|
||||
setChunkQueue([])
|
||||
chunkQueueRef.current = []
|
||||
displayedTextRef.current = newText
|
||||
onUpdate(newText)
|
||||
},
|
||||
@ -32,12 +35,16 @@ export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialTe
|
||||
|
||||
const renderLoop = useCallback(
|
||||
(currentTime: number) => {
|
||||
// 1. 如果队列为空,等待下一帧
|
||||
if (chunkQueue.length === 0) {
|
||||
// 如果流还没结束但队列空了,就等待下一帧
|
||||
if (!streamDone) {
|
||||
animationFrameRef.current = requestAnimationFrame(renderLoop)
|
||||
// 1. 如果队列为空
|
||||
if (chunkQueueRef.current.length === 0) {
|
||||
// 如果流已结束,确保显示最终状态并停止循环
|
||||
if (streamDone) {
|
||||
const finalText = displayedTextRef.current
|
||||
onUpdate(finalText)
|
||||
return
|
||||
}
|
||||
// 如果流还没结束但队列空了,等待下一帧
|
||||
animationFrameRef.current = requestAnimationFrame(renderLoop)
|
||||
return
|
||||
}
|
||||
|
||||
@ -48,25 +55,29 @@ export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialTe
|
||||
}
|
||||
lastUpdateTimeRef.current = currentTime
|
||||
|
||||
setChunkQueue((prevQueue) => {
|
||||
// 3. 动态计算本次渲染的字符数
|
||||
// 如果队列积压严重,就一次性渲染更多字符来"追赶"
|
||||
const charsToRenderCount = Math.max(1, Math.floor(prevQueue.length / 5)) // 每次至少渲染1个,最多渲染队列的1/5
|
||||
// 3. 动态计算本次渲染的字符数
|
||||
let charsToRenderCount = Math.max(1, Math.floor(chunkQueueRef.current.length / 5))
|
||||
|
||||
const charsToRender = prevQueue.slice(0, charsToRenderCount)
|
||||
displayedTextRef.current += charsToRender.join('')
|
||||
// 如果流已结束,一次性渲染所有剩余字符
|
||||
if (streamDone) {
|
||||
charsToRenderCount = chunkQueueRef.current.length
|
||||
}
|
||||
|
||||
// 4. 立即更新UI
|
||||
onUpdate(displayedTextRef.current)
|
||||
const charsToRender = chunkQueueRef.current.slice(0, charsToRenderCount)
|
||||
displayedTextRef.current += charsToRender.join('')
|
||||
|
||||
// 返回新的队列
|
||||
return prevQueue.slice(charsToRenderCount)
|
||||
})
|
||||
// 4. 立即更新UI
|
||||
onUpdate(displayedTextRef.current)
|
||||
|
||||
// 5. 请求下一帧动画
|
||||
animationFrameRef.current = requestAnimationFrame(renderLoop)
|
||||
// 5. 更新队列
|
||||
chunkQueueRef.current = chunkQueueRef.current.slice(charsToRenderCount)
|
||||
|
||||
// 6. 如果还有内容需要渲染,继续下一帧
|
||||
if (chunkQueueRef.current.length > 0) {
|
||||
animationFrameRef.current = requestAnimationFrame(renderLoop)
|
||||
}
|
||||
},
|
||||
[chunkQueue, streamDone, onUpdate, minDelay]
|
||||
[streamDone, onUpdate, minDelay]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -81,22 +92,5 @@ export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialTe
|
||||
}
|
||||
}, [renderLoop])
|
||||
|
||||
// 当外部流结束,且队列即将变空时,进行最后一次"瞬移"渲染
|
||||
useEffect(() => {
|
||||
if (streamDone && chunkQueue.length > 0) {
|
||||
const remainingText = chunkQueue.join('')
|
||||
const finalText = displayedTextRef.current + remainingText
|
||||
|
||||
// 取消正在进行的动画循环
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current)
|
||||
}
|
||||
|
||||
// 直接更新到最终状态
|
||||
onUpdate(finalText)
|
||||
setChunkQueue([]) // 清空队列
|
||||
}
|
||||
}, [streamDone, chunkQueue, onUpdate])
|
||||
|
||||
return { addChunk, reset }
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user