最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Vue3實(shí)現(xiàn)AI流式打字機(jī)的完整解決方案

 更新時(shí)間:2026年04月12日 14:35:24   作者:英俊瀟灑美少年  
本文介紹了基于MessageChannel實(shí)現(xiàn)Vue AI流式對(duì)話的方案,通過(guò)SSE流式解析、時(shí)間切片、任務(wù)隊(duì)列等技術(shù),實(shí)現(xiàn)了非阻塞UI渲染和完美處理分包粘包問(wèn)題,同時(shí)提供了一套可復(fù)用的Hook,并進(jìn)行了詳細(xì)的對(duì)比分析,需要的朋友可以參考下

Vue 實(shí)現(xiàn) AI 流式對(duì)話時(shí),高頻更新易造成頁(yè)面卡頓、輸入阻塞,且沒(méi)有 React 內(nèi)置的并發(fā)渲染能力。
本文基于 MessageChannel 實(shí)現(xiàn)時(shí)間切片,模擬 React 低優(yōu)先級(jí)更新調(diào)度,并對(duì) SSE 流式解析、分包粘包、任務(wù)隊(duì)列、內(nèi)存安全做完整工程化抽離

一、核心原理

  1. SSE 流式解析:buffer 拼接解決 TCP 分包/粘包
  2. 時(shí)間切片(Time Slicing):模擬 React 并發(fā),非阻塞 UI 渲染
  3. MessageChannel:宏任務(wù)調(diào)度,優(yōu)先級(jí)低于交互、高于定時(shí)器
  4. 任務(wù)隊(duì)列:避免任務(wù)覆蓋、丟失,保證打字機(jī)不跳字不漏字
  5. 安全兜底:異常捕獲、取消流、組件銷毀清理,無(wú)內(nèi)存泄漏

二、目錄結(jié)構(gòu)

src/
├─ hooks/
│  ├─ useTimeSlicedQueue.js    // 時(shí)間切片調(diào)度(模擬并發(fā))
│  └─ useSseParser.js          // SSE 流式解析(分包處理)
└─ views/
   └─ ChatStream.vue           // AI 對(duì)話組件

三、工具 Hook 抽離(可復(fù)用)

1. useTimeSlicedQueue.js — 時(shí)間切片調(diào)度器

/**
 * 時(shí)間切片隊(duì)列,模擬 React 并發(fā)更新
 * @param sliceTime 每片執(zhí)行時(shí)間,默認(rèn) 8ms
 */
export function useTimeSlicedQueue(sliceTime = 8) {
  const taskQueue = []
  let isScheduling = false

  const channel = new MessageChannel()
  const { port1, port2 } = channel

  port2.onmessage = () => {
    const start = performance.now()
    // 時(shí)間切片:避免長(zhǎng)時(shí)間占用主線程
    while (taskQueue.length > 0) {
      const task = taskQueue.shift()
      task()
      if (performance.now() - start > sliceTime) break
    }
    isScheduling = false
    // 剩余任務(wù)繼續(xù)調(diào)度
    if (taskQueue.length > 0) schedule()
  }

  function schedule() {
    if (!isScheduling) {
      isScheduling = true
      port1.postMessage('')
    }
  }

  // 添加低優(yōu)先級(jí)更新任務(wù)
  function addTask(task) {
    taskQueue.push(task)
    schedule()
  }

  // 清空隊(duì)列(組件銷毀用)
  function clearQueue() {
    taskQueue.length = 0
  }

  return {
    addTask,
    clearQueue
  }
}

2. useSseParser.js — SSE 解析器

/**
 * SSE 流式解析,處理分包/粘包
 * @param onChunk 解析完成回調(diào)
 */
export function useSseParser(onChunk) {
  let buffer = ''

  // 推入 chunk 并按換行拆分完整行
  function feed(chunk) {
    buffer += chunk
    const lines = buffer.split('\n')
    buffer = lines.pop() || ''
    lines.forEach(line => parseLine(line))
  }

  // 解析單行 SSE
  function parseLine(line) {
    const trimLine = line.trim()
    if (!trimLine.startsWith('data: ')) return

    const dataStr = trimLine.replace('data: ', '').trim()
    if (dataStr === '[DONE]') return onChunk?.({ done: true })

    try {
      const data = JSON.parse(dataStr)
      onChunk?.({ data })
    } catch (e) {
      // 分包導(dǎo)致不完整 JSON,忽略
    }
  }

  // 結(jié)束時(shí)沖刷剩余數(shù)據(jù)
  function flush() {
    if (buffer.trim()) parseLine(buffer)
    buffer = ''
  }

  // 清空緩存
  function clearParser() {
    buffer = ''
  }

  return {
    feed,
    flush,
    clearParser
  }
}

四、Vue3 對(duì)話組件(業(yè)務(wù)層)

<template>
  <div class="chat-container">
    <div class="message-list" ref="messageListRef">
      <div v-for="(msg, idx) in msgList" :key="idx" :class="['msg', msg.role]">
        <div class="bubble">{{ msg.content }}</div>
      </div>
    </div>
    <div class="input-bar">
      <textarea
        v-model="inputText"
        @keydown.enter.exact="sendMessage"
        placeholder="輸入問(wèn)題..."
      />
      <button @click="sendMessage" :disabled="loading">發(fā)送</button>
      <button v-if="loading" @click="stopGenerate">停止生成</button>
    </div>
  </div>
</template>
<script setup>
import { ref, onUnmounted, nextTick } from 'vue'
import { useTimeSlicedQueue } from '@/hooks/useTimeSlicedQueue'
import { useSseParser } from '@/hooks/useSseParser'
const inputText = ref('')
const msgList = ref([])
const loading = ref(false)
const messageListRef = ref(null)
// 時(shí)間切片(低優(yōu)先級(jí)更新)
const { addTask, clearQueue } = useTimeSlicedQueue(8)
// SSE 解析
const { feed, flush, clearParser } = useSseParser(onChunkResult)
// 流控制
let controller = null
let reader = null
let fullText = ''
let aiMsgIndex = -1
// 發(fā)送消息
async function sendMessage() {
  if (!inputText.value.trim() || loading.value) return
  const text = inputText.value.trim()
  inputText.value = ''
  // 插入對(duì)話
  msgList.value = [
    ...msgList.value,
    { role: 'user', content: text },
    { role: 'ai', content: '' }
  ]
  aiMsgIndex = msgList.value.length - 1
  fullText = ''
  loading.value = true
  controller = new AbortController()
  try {
    const res = await fetch('/api/stream', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt: text }),
      signal: controller.signal
    })
    if (!res.ok) throw new Error(`請(qǐng)求錯(cuò)誤 ${res.status}`)
    if (!res.body) throw new Error('當(dāng)前環(huán)境不支持流式')
    reader = res.body.getReader()
    const decoder = new TextDecoder('utf-8')
    while (true) {
      const { done, value } = await reader.read()
      if (done) {
        flush()
        break
      }
      feed(decoder.decode(value))
    }
  } catch (err) {
    const tip = err.name === 'AbortError' ? '\n[已停止]' : '\n[加載失敗]'
    updateContentView(fullText + tip)
  } finally {
    loading.value = false
    reader = null
    controller = null
  }
}
// SSE 解析回調(diào)
function onChunkResult({ data, done }) {
  if (done) return
  const content = data?.content || data?.delta?.content || ''
  if (!content) return
  fullText += content
  updateContentView(fullText)
}
// 時(shí)間切片更新視圖(不阻塞輸入)
function updateContentView(text) {
  addTask(() => {
    if (aiMsgIndex >= 0) {
      msgList.value[aiMsgIndex].content = text
    }
    nextTick(scrollToBottom)
  })
}
// 停止生成
function stopGenerate() {
  controller?.abort()
  reader?.cancel().catch(() => {})
}
// 自動(dòng)滾動(dòng)到底部
function scrollToBottom() {
  const el = messageListRef.value
  if (el) el.scrollTop = el.scrollHeight
}
// 組件銷毀清理
onUnmounted(() => {
  stopGenerate()
  clearQueue()
  clearParser()
})
</script>
<style scoped>
.chat-container {
  max-width: 800px;
  margin: 0 auto;
  height: 100vh;
  display: flex;
  flex-direction: column;
}
.message-list {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
}
.msg {
  margin-bottom: 12px;
}
.msg.ai {
  text-align: left;
}
.msg.user {
  text-align: right;
}
.bubble {
  display: inline-block;
  padding: 8px 14px;
  border-radius: 12px;
  background: #f1f3f4;
  max-width: 75%;
  white-space: pre-wrap;
}
.msg.user .bubble {
  background: #007bff;
  color: #fff;
}
.input-bar {
  padding: 12px;
  border-top: 1px solid #eee;
}
textarea {
  width: 100%;
  height: 60px;
  margin-bottom: 8px;
  padding: 8px;
  border-radius: 6px;
  border: 1px solid #ddd;
  resize: none;
}
button {
  margin-right: 8px;
  padding: 6px 12px;
}
</style>

五、核心亮點(diǎn)

  1. 純 Vue3 實(shí)現(xiàn),無(wú)第三方依賴
  2. 時(shí)間切片模擬 React 并發(fā),輸入框永不卡頓
  3. SSE 分包粘包完美處理,不丟字、不亂碼
  4. 任務(wù)隊(duì)列安全機(jī)制,不覆蓋、不丟失、不漏更
  5. 工程化抽離 Hook,可復(fù)用、易維護(hù)、易擴(kuò)展
  6. 完整異常處理 + 內(nèi)存安全,支持生產(chǎn)環(huán)境
  7. 支持 停止生成、自動(dòng)滾動(dòng)、回車發(fā)送

六、面試/問(wèn)答亮點(diǎn)

  • Vue 沒(méi)有原生并發(fā),如何實(shí)現(xiàn)非阻塞流式渲染?
    → 使用 MessageChannel + 時(shí)間切片 + 任務(wù)隊(duì)列 模擬低優(yōu)先級(jí)更新。
  • 流式為什么會(huì)卡頓?
    → 高頻更新阻塞主線程,必須把 UI 更新降級(jí)為低優(yōu)先級(jí)任務(wù)。
  • SSE 為什么需要 buffer?
    → TCP 分包/粘包會(huì)導(dǎo)致 JSON 不完整,必須按行拼接解析。

七、重點(diǎn)對(duì)比:Vue 方案 VS React useTransition

1. 兩者體驗(yàn)差距

在 AI 流式場(chǎng)景下:Vue 方案 ≈ React 95% 體驗(yàn)
用戶幾乎感知不到區(qū)別。

2. 核心原理差異

Vue(本文方案)

  • DOM 更新任務(wù)切小
  • 執(zhí)行 8ms → 暫停 → 繼續(xù)
  • DOM 更新一旦開(kāi)始,不能中斷
  • 屬于:事后優(yōu)化、工程手段

React useTransition

  • 不直接操作 DOM,在內(nèi)存中構(gòu)建 Fiber 樹(shù)
  • render 階段可中斷、可丟棄、可重啟
  • commit 階段才同步更新 DOM
  • 屬于:框架級(jí)并發(fā)架構(gòu)

3. React 到底如何實(shí)現(xiàn)“隨時(shí)中斷”?

靠三大底層設(shè)計(jì):

(1)Fiber 鏈表

把渲染從遞歸改為迭代鏈表,每個(gè)節(jié)點(diǎn)一個(gè)工作單元。
每執(zhí)行一個(gè)節(jié)點(diǎn)就判斷:

  • 時(shí)間到 5ms 了嗎?
  • 有更高優(yōu)先級(jí)任務(wù)嗎?

(2)雙緩存 WIP 樹(shù)

  • Current Tree:頁(yè)面真實(shí) DOM 樹(shù)
  • WorkInProgress Tree:內(nèi)存中計(jì)算的新樹(shù)

所有 diff 都在內(nèi)存進(jìn)行,可隨時(shí)扔掉,不影響界面。

(3)優(yōu)先級(jí)調(diào)度(Lane 模型)

  • 用戶輸入、點(diǎn)擊 = 高優(yōu)先級(jí)
  • AI 流式、列表渲染 = 低優(yōu)先級(jí)

高優(yōu)任務(wù)可以直接打斷低優(yōu)任務(wù),丟棄現(xiàn)有進(jìn)度,優(yōu)先執(zhí)行。

4. 那 5ms 到底是什么?

是 React 的協(xié)作式時(shí)間片上限,避免長(zhǎng)時(shí)間霸占主線程。
它不是“隨時(shí)中斷”,只是主動(dòng)讓出。

真正“隨時(shí)中斷”靠的是:
優(yōu)先級(jí)插隊(duì) + 丟棄 WIP 樹(shù)。

5. 總結(jié)對(duì)比表

特性Vue 節(jié)流+時(shí)間切片React useTransition
不阻塞輸入??
可中斷渲染?(DOM 不可中斷)?(內(nèi)存 Fiber 可中斷)
優(yōu)先級(jí)插隊(duì)??
自動(dòng)丟棄過(guò)時(shí)更新??
框架侵入無(wú)強(qiáng)依賴 React
實(shí)現(xiàn)成本
流式體驗(yàn)極佳極致

八、最終結(jié)論

  1. Vue 沒(méi)有并發(fā)渲染架構(gòu),無(wú)法真正中斷 DOM 更新。
  2. 但通過(guò)節(jié)流 + 時(shí)間切片,已經(jīng)可以實(shí)現(xiàn)接近 React 并發(fā)的流暢體驗(yàn)。
  3. React 可中斷的核心是:Fiber + 雙緩存 + 優(yōu)先級(jí)調(diào)度,不是 5ms 時(shí)間片。
  4. 本文方案是 Vue AI 流式輸出的生產(chǎn)級(jí)最佳實(shí)踐,簡(jiǎn)單、穩(wěn)定、可直接上線。

以上就是Vue3實(shí)現(xiàn)AI流式打字機(jī)的完整解決方案的詳細(xì)內(nèi)容,更多關(guān)于Vue3實(shí)現(xiàn)AI流式打字機(jī)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 五分鐘教你使用vue-cli3創(chuàng)建項(xiàng)目(新手入門)

    五分鐘教你使用vue-cli3創(chuàng)建項(xiàng)目(新手入門)

    本文主要介紹了五分鐘教你使用vue-cli3創(chuàng)建項(xiàng)目,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-09-09
  • 前端Vue數(shù)據(jù)不更新問(wèn)題的深入分析與解決方案

    前端Vue數(shù)據(jù)不更新問(wèn)題的深入分析與解決方案

    在前端開(kāi)發(fā)中,Vue.js 是一個(gè)非常流行的 JavaScript 框架,它以其簡(jiǎn)潔的語(yǔ)法和強(qiáng)大的響應(yīng)式系統(tǒng)而聞名,然而,盡管 Vue 的響應(yīng)式系統(tǒng)非常強(qiáng)大,但在實(shí)際開(kāi)發(fā)中,開(kāi)發(fā)者仍然可能會(huì)遇到數(shù)據(jù)不更新的問(wèn)題,本文將深入探討 Vue 數(shù)據(jù)不更新的常見(jiàn)原因,并提供詳細(xì)的解決方案
    2025-03-03
  • vue+Echart實(shí)現(xiàn)立體柱狀圖

    vue+Echart實(shí)現(xiàn)立體柱狀圖

    這篇文章主要為大家詳細(xì)介紹了vue+Echart實(shí)現(xiàn)立體柱狀圖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-04-04
  • vue獲取DOM元素并設(shè)置屬性的兩種實(shí)現(xiàn)方法

    vue獲取DOM元素并設(shè)置屬性的兩種實(shí)現(xiàn)方法

    下面小編就為大家?guī)?lái)一篇vue獲取DOM元素并設(shè)置屬性的兩種實(shí)現(xiàn)方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-09-09
  • Element實(shí)現(xiàn)動(dòng)態(tài)增加多個(gè)輸入框并校驗(yàn)

    Element實(shí)現(xiàn)動(dòng)態(tài)增加多個(gè)輸入框并校驗(yàn)

    本文主要介紹了Element實(shí)現(xiàn)動(dòng)態(tài)增加多個(gè)輸入框并校驗(yàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-07-07
  • Electron 如何調(diào)用本地模塊的方法

    Electron 如何調(diào)用本地模塊的方法

    這篇文章主要介紹了Electron 如何調(diào)用本地模塊的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2019-02-02
  • vue-table實(shí)現(xiàn)添加和刪除

    vue-table實(shí)現(xiàn)添加和刪除

    這篇文章主要為大家詳細(xì)介紹了vue-table實(shí)現(xiàn)添加和刪除,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-05-05
  • vue配置@路徑實(shí)現(xiàn)方式

    vue配置@路徑實(shí)現(xiàn)方式

    文章介紹了在Vue項(xiàng)目中使用path模塊的步驟,首先檢查項(xiàng)目中是否已經(jīng)存在node_modules文件夾中的path模塊,如果不存在則需要通過(guò)安裝命令進(jìn)行安裝,其次,在項(xiàng)目的vue.config.js文件中進(jìn)行相應(yīng)的配置,最后,詳細(xì)說(shuō)明了如何使用path模塊
    2025-11-11
  • Element的el-tree控件后臺(tái)數(shù)據(jù)結(jié)構(gòu)的生成以及方法的抽取

    Element的el-tree控件后臺(tái)數(shù)據(jù)結(jié)構(gòu)的生成以及方法的抽取

    這篇文章主要介紹了Element的el-tree控件后臺(tái)數(shù)據(jù)結(jié)構(gòu)的生成以及方法的抽取,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-03-03
  • Vue組件通信的幾種實(shí)現(xiàn)方法

    Vue組件通信的幾種實(shí)現(xiàn)方法

    這篇文章主要介紹了Vue組件通信的幾種實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04

最新評(píng)論

兴化市| 长丰县| 宜春市| 松阳县| 光山县| 曲麻莱县| 湘乡市| 囊谦县| 怀安县| 泌阳县| 南平市| 灵川县| 墨竹工卡县| 武威市| 凤台县| 佛学| 奉化市| 梅河口市| 望城县| 武宁县| 建始县| 四会市| 竹北市| 喀喇沁旗| 乌苏市| 民乐县| 临桂县| 扶沟县| 晋中市| 盐源县| 漳州市| 博乐市| 桐柏县| 宣城市| 息烽县| 建德市| 儋州市| 肃南| 开远市| 康平县| 怀集县|