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

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

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

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

一、核心原理

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

二、目錄結構

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

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

1. useTimeSlicedQueue.js — 時間切片調度器

/**
 * 時間切片隊列,模擬 React 并發(fā)更新
 * @param sliceTime 每片執(zhí)行時間,默認 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()
    // 時間切片:避免長時間占用主線程
    while (taskQueue.length > 0) {
      const task = taskQueue.shift()
      task()
      if (performance.now() - start > sliceTime) break
    }
    isScheduling = false
    // 剩余任務繼續(xù)調度
    if (taskQueue.length > 0) schedule()
  }

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

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

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

  return {
    addTask,
    clearQueue
  }
}

2. useSseParser.js — SSE 解析器

/**
 * SSE 流式解析,處理分包/粘包
 * @param onChunk 解析完成回調
 */
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) {
      // 分包導致不完整 JSON,忽略
    }
  }

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

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

  return {
    feed,
    flush,
    clearParser
  }
}

四、Vue3 對話組件(業(yè)務層)

<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="輸入問題..."
      />
      <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)
// 時間切片(低優(yōu)先級更新)
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 = ''
  // 插入對話
  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(`請求錯誤 ${res.status}`)
    if (!res.body) throw new Error('當前環(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 解析回調
function onChunkResult({ data, done }) {
  if (done) return
  const content = data?.content || data?.delta?.content || ''
  if (!content) return
  fullText += content
  updateContentView(fullText)
}
// 時間切片更新視圖(不阻塞輸入)
function updateContentView(text) {
  addTask(() => {
    if (aiMsgIndex >= 0) {
      msgList.value[aiMsgIndex].content = text
    }
    nextTick(scrollToBottom)
  })
}
// 停止生成
function stopGenerate() {
  controller?.abort()
  reader?.cancel().catch(() => {})
}
// 自動滾動到底部
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>

五、核心亮點

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

六、面試/問答亮點

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

七、重點對比:Vue 方案 VS React useTransition

1. 兩者體驗差距

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

2. 核心原理差異

Vue(本文方案)

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

React useTransition

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

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

靠三大底層設計:

(1)Fiber 鏈表

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

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

(2)雙緩存 WIP 樹

  • Current Tree:頁面真實 DOM 樹
  • WorkInProgress Tree:內存中計算的新樹

所有 diff 都在內存進行,可隨時扔掉,不影響界面。

(3)優(yōu)先級調度(Lane 模型)

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

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

4. 那 5ms 到底是什么?

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

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

5. 總結對比表

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

八、最終結論

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

以上就是Vue3實現(xiàn)AI流式打字機的完整解決方案的詳細內容,更多關于Vue3實現(xiàn)AI流式打字機的資料請關注腳本之家其它相關文章!

相關文章

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

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

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

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

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

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

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

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

    下面小編就為大家?guī)硪黄獀ue獲取DOM元素并設置屬性的兩種實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-09-09
  • Element實現(xiàn)動態(tài)增加多個輸入框并校驗

    Element實現(xiàn)動態(tài)增加多個輸入框并校驗

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

    Electron 如何調用本地模塊的方法

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

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

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

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

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

    Element的el-tree控件后臺數(shù)據(jù)結構的生成以及方法的抽取

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

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

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

最新評論

德江县| 土默特左旗| 霸州市| 乌拉特前旗| 上饶市| 海兴县| 广昌县| 罗定市| 杭州市| 蓝田县| 亳州市| 全州县| 阿拉尔市| 中牟县| 斗六市| 南乐县| 革吉县| 梅河口市| 岳池县| 丰台区| 铜鼓县| 县级市| 磐安县| 平武县| 淳化县| 耿马| 阿鲁科尔沁旗| 遂平县| 鹤庆县| 库尔勒市| 客服| 霍邱县| 新绛县| 瓦房店市| 泉州市| 嘉兴市| 肇源县| 哈巴河县| 南丹县| 郧西县| 峨山|