Vue3 + 流式输出学习

  • 🌊 流式输出如何让AI像真人一样"打字"
  • 🔗 Vue3响应式系统如何自动更新界面
  • 🔐 API密钥如何在Vite项目中安全存储
  • 🧩 组件化开发为什么是前端工程化的基石
  • ⚛️ <script setup> 语法糖如何简化组件开发

无论你是刚接触Vue的前端新手,还是想搞懂AI应用落地细节的后端大佬,这篇文章都能让你有所收获。


一、核心功能速览:这个Demo做了什么?

简单来说,这是一个调用DeepSeek大模型API的聊天界面,核心亮点是:

  • 📝 双向数据绑定:输入框内容自动同步到JS变量
  • 🎛️ 流式开关:一键切换"逐字输出"和"一次性返回"模式
  • 响应式更新:数据变化时,界面自动刷新(无需操作DOM)
  • 🧩 组件化封装 :所有代码写在.vue文件中,易于维护

生活类比:如果把获取AI回答比作"接水"------

  • 非流式:等水龙头接满一桶水再端给你(等待时间长,但一次性拿到全部)
  • 流式:直接接根管子到你杯子,水边流边喝(实时看到输出,体验更好)

二、解决什么痛点?为什么需要流式输出?

2.1 传统HTTP请求的"等待困境"

普通的API请求(如RESTful接口)通常是一次性返回完整数据。但对于LLM(大语言模型):

  • 生成一个回答可能需要2-10秒(取决于问题复杂度和模型大小)
  • 用户面对空白屏幕会焦虑(据统计,超过3秒的等待会让40%用户流失)

2.2 流式输出的"打字机"体验

核心优势

  1. ⏱️ 首字节时间(TTFB)极短:用户几乎立即看到第一个字
  2. 😌 心理预期管理:看到AI在"思考并输出",减少焦虑
  3. 🔄 可中断性:如果回答不满意,用户可提前停止(未来可扩展)

三、重难点剖析(核心干货)

🔥 难点1:流式数据解析------ReadableStream完整揭秘

3.1 可选链操作符 ?. 的妙用

javascript

ini 复制代码
const reader = response.body?.getReader();

这行代码看似简单,实则包含了两层关键操作:

第一步:?. 可选链操作符(Optional Chaining)

javascript

javascript 复制代码
// ❌ 传统写法(危险)
const reader = response.body.getReader(); 
// 如果response.body是null或undefined,报错:
// "Cannot read properties of undefined (reading 'getReader')"

// ✅ 使用可选链(安全)
const reader = response.body?.getReader();
// 如果response.body存在 → 调用getReader(),返回读取器
// 如果response.body不存在 → 返回undefined,不会报错

// 等价于:
const reader = (response.body !== null && response.body !== undefined) 
  ? response.body.getReader() 
  : undefined;

核心价值:在不确定对象属性是否存在时,安全地访问深层嵌套属性,避免程序崩溃。

第二步:getReader() 锁定流的含义

getReader()ReadableStream原型上的方法,作用是创建一个流读取器(Reader)并锁定该流

javascript

ini 复制代码
// 获取读取器后,流被锁定
const reader = response.body.getReader();

// 此时其他代码无法再获取读取器
const reader2 = response.body.getReader(); 
// ❌ 报错:TypeError: ReadableStream is locked

为什么需要锁定?

  • 独占访问:保证数据按顺序被消费,不会出现多个读取器竞争
  • 数据完整性:防止同一个chunk被多次读取
  • 资源管理:流读取完毕后,自动释放资源

返回值ReadableStreamDefaultReader对象,提供了read()cancel()等方法。

3.2 response.body 与 ReadableStream 的本质

fetch收到stream:true的响应时,response.body是一个ReadableStream对象,它代表一个可读的字节流。这个流的特点是:

  • 数据是分块(chunk)到达的,不是一次性全部到齐
  • 每个chunk是Uint8Array(二进制数组),不是文本
  • 数据到达顺序是确定的,但到达时间是未知的

javascript

arduino 复制代码
console.log(response.body)  
// 输出: ReadableStream { 
//   locked: false,          // 当前未被锁定
//   ... 
// }
// 它不是一个普通对象,而是一个流式数据源

3.3 reader.read() 的返回值详解

每次调用await reader.read()都会返回一个Promise,解析后的对象包含:

javascript

javascript 复制代码
const result = await reader.read();
console.log(result);
// 输出示例:
// { 
//   value: Uint8Array [100, 97, 116, 97, 58, 32, 123, ...], 
//   done: false 
// }

返回值解析

字段 类型 说明
done boolean true表示流已结束,false表示还有更多数据
value Uint8Array undefined 当前chunk的二进制数据。如果donetruevalueundefined

关键理解

每个chunk可能包含多条消息 ,也可能一条消息被拆成多个chunk。这就是为什么我们需要buffer机制!

3.4 数据转换全流程:从二进制到文字

详细数据转换过程

text

css 复制代码
┌─────────────────────────────────────────────────────────┐
│ 1. 网络传输(二进制字节流)                              │
│    [100, 97, 116, 97, 58, 32, 123, 34, 105, 100, ...] │
│    ↑ Uint8Array(二进制数组)                           │
└────────────────┬────────────────────────────────────────┘
                 │
                 │ decoder.decode(value, { stream: true })
                 ▼
┌─────────────────────────────────────────────────────────┐
│ 2. 解码后(字符串)                                     │
│    'data: {"id":"1","delta":{"content":"你"}}'         │
│    ↑ string(字符串)                                   │
└────────────────┬────────────────────────────────────────┘
                 │
                 │ 字符串操作: trimmed.slice(6)
                 ▼
┌─────────────────────────────────────────────────────────┐
│ 3. 纯 JSON 字符串                                       │
│    '{"id":"1","delta":{"content":"你"}}'               │
│    ↑ 仍然是 string                                      │
└────────────────┬────────────────────────────────────────┘
                 │
                 │ JSON.parse()(解析成对象)
                 ▼
┌─────────────────────────────────────────────────────────┐
│ 4. JavaScript 对象                                      │
│    {                                                   │
│      id: "1",                                          │
│      choices: [{                                       │
│        delta: {                                        │
│          content: "你"                                 │
│        }                                               │
│      }]                                                │
│    }                                                   │
│    ↑ Object(对象)                                     │
└────────────────┬────────────────────────────────────────┘
                 │
                 │ 访问属性:data.choices[0].delta.content
                 ▼
┌─────────────────────────────────────────────────────────┐
│ 5. 最终内容片段                                         │
│    '你'                                                │
│    ↑ string(字符串)                                   │
└─────────────────────────────────────────────────────────┘

3.5 Buffer机制:为什么它如此重要?

Buffer的核心作用 :处理跨chunk的不完整数据

场景演示

javascript

ini 复制代码
// 假设服务器发送了两个chunk
// Chunk 1(部分): 'data: {"id":"1","delta":{'
// Chunk 2(剩余): '"content":"你"}}'

// ❌ 错误做法:直接解析每个chunk
const wrong = (chunk) => {
  const line = chunk.trim();
  if (line.startsWith('data: ')) {
    const json = JSON.parse(line.slice(6)); // 报错!因为JSON不完整
  }
};

// ✅ 正确做法:使用buffer缓存不完整数据
let buffer = '';
const correct = (chunk) => {
  buffer += chunk; // 追加到buffer
  const lines = buffer.split('\n');
  buffer = lines.pop() || ''; // 最后一行可能不完整,保留到下次
  
  for (const line of lines) {
    // 只有完整行才处理
    const trimmed = line.trim();
    if (trimmed.startsWith('data: ')) {
      try {
        const json = JSON.parse(trimmed.slice(6));
        // 处理完整数据
      } catch (e) {
        // 万一解析失败,放回buffer
        buffer = trimmed + '\n' + buffer;
      }
    }
  }
};

Buffer的三大职责

  1. 拼接跨chunk的数据:确保拿到完整的行
  2. 暂存不完整的JSON:等待下一个chunk补全
  3. 处理多字节字符:如中文UTF-8编码可能跨越多个chunk

3.6 TextDecoder的stream参数

javascript

arduino 复制代码
const decoder = new TextDecoder();
buffer += decoder.decode(value, { stream: true });
  • stream: false(默认) :假设当前chunk是完整数据,直接解码
  • stream: true:保留内部状态,正确处理跨chunk的多字节字符

举例 :中文字符"你"的UTF-8编码是[228, 189, 160],如果第一个chunk只包含[228, 189],第二个chunk包含[160]

javascript

css 复制代码
// ❌ 错误:stream:false 会乱码
decoder.decode(new Uint8Array([228, 189]), { stream: false }); 
// 输出: '�'(乱码)

// ✅ 正确:stream:true 会等待后续字节
decoder.decode(new Uint8Array([228, 189]), { stream: true }); 
// 输出: ''(暂不输出,等待完成)
decoder.decode(new Uint8Array([160]), { stream: true }); 
// 输出: '你'(完整字符)

3.7 完整实现代码

javascript

ini 复制代码
// 流式处理核心代码(完整注释版)
const reader = response.body?.getReader();
if (!reader) {
  content.value = '⚠️ 无法读取响应流';
  return;
}

const decoder = new TextDecoder(); // 创建文本解码器
let buffer = '';                  // 缓存不完整的行
let done = false;

while (!done) {
  // 读取一个数据块(chunk)
  const { done: doneReading, value } = await reader.read();
  
  // doneReading: true 表示流已结束,没有更多数据
  // value: Uint8Array 或 undefined(当done为true时)
  if (doneReading) break;
  
  // 关键:将二进制数据转换为文本
  // stream: true 表示可能跨chunk,保留解码器内部状态
  buffer += decoder.decode(value, { stream: true });
  
  // 按换行符分割成多行
  const lines = buffer.split('\n');
  
  // 最后一行可能不完整(如 'data: {"id":"1","delta":{' 被截断)
  // 保留到下一次循环继续拼接
  buffer = lines.pop() || '';
  
  // 处理每一行完整数据
  for (const line of lines) {
    const trimmed = line.trim();
    
    // SSE(Server-Sent Events)格式:每行以 'data: ' 开头
    if (!trimmed.startsWith('data: ')) continue;
    
    // 提取实际数据部分(去掉 'data: ' 前缀)
    const payload = trimmed.slice(6);
    
    // 检查是否结束标志
    if (payload === '[DONE]') {
      done = true;
      break;
    }
    
    try {
      // 解析 JSON 字符串为 JavaScript 对象
      const data = JSON.parse(payload);
      
      // 提取内容片段(根据DeepSeek API响应格式)
      const delta = data.choices[0].delta.content;
      
      if (delta) {
        // 累加内容,Vue响应式自动更新界面
        content.value += delta;
      }
    } catch (error) {
      // 如果JSON不完整(如被截断),放回buffer等待下次拼接
      console.warn('解析失败,等待更多数据:', error);
      buffer = trimmed + '\n' + buffer;
    }
  }
}

🔥 难点2:Vue3的 <script setup> 语法糖

2.1 什么是 <script setup>

<script setup> 是 Vue 3.2 引入的编译时语法糖,用于简化 Composition API 的写法。

对比传统写法

javascript

xml 复制代码
// ❌ 传统写法(Vue 3.0)
<script>
import { ref } from 'vue'
export default {
  setup() {
    const count = ref(0)
    const increment = () => count.value++
    return { count, increment }  // 必须手动返回
  }
}
</script>

// ✅ `<script setup>` 写法(Vue 3.2+)
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
// 无需return,所有顶层绑定自动暴露给模板
</script>

2.2 <script setup> 的核心特点

特性 说明 示例
自动暴露 顶层声明的变量、函数自动暴露给模板 const msg = ref('hello') → 模板直接用{{msg}}
模块化 支持import导入其他模块 import { useRouter } from 'vue-router'
宏函数 编译器提供的特殊函数,在编译时处理 definePropsdefineEmitsdefineExpose
性能优化 编译时优化,减少运行时开销 比普通setup()更高效
类型推断 更好的TypeScript支持 自动推断类型,无需额外声明

2.3 为什么使用 <script setup>

1. 简洁性

javascript

csharp 复制代码
// ❌ 传统写法:冗余的return
export default {
  setup() {
    const question = ref('')
    const stream = ref(false)
    const content = ref('')
    const updata = async () => { /* ... */ }
    return { question, stream, content, updata } // 必须列出所有
  }
}

// ✅ setup语法糖:自动暴露
<script setup>
const question = ref('')
const stream = ref(false)
const content = ref('')
const updata = async () => { /* ... */ }
// 无需return,所有顶层声明自动可用
</script>

2. 组件通信更直观

javascript

xml 复制代码
<script setup>
// 定义props(宏函数,无需导入)
const props = defineProps({
  title: String,
  count: Number
})

// 定义emits
const emit = defineEmits(['update', 'delete'])

// 暴露方法给父组件
defineExpose({
  reset: () => { /* ... */ }
})
</script>

3. 编译时优化

  • <script setup> 在编译时会被转换为标准的setup()函数
  • 编译器会进行静态提升缓存内联事件等优化
  • 最终生成的代码比手写setup()更高效

2.4 与普通 <script> 的区别

关键区别

特性 普通 <script> <script setup>
组件实例 完整的组件选项对象 编译后的setup函数
数据暴露 必须手动return 自动暴露所有顶层声明
组件注册 需要在components中注册 导入即注册
生命周期 所有生命周期钩子 同Composition API
适用场景 需要多个script块、options API Composition API优先

🔥 难点3:Vue3响应式系统------ref vs reactive

3.1 响应式数据绑定原理

数据绑定(Data Binding) 是Vue的核心机制,确保数据状态界面显示 保持同步。关键区别

特性 普通 <script> <script setup>
组件实例 完整的组件选项对象 编译后的setup函数
数据暴露 必须手动return 自动暴露所有顶层声明
组件注册 需要在components中注册 导入即注册
生命周期 所有生命周期钩子 同Composition API
适用场景 需要多个script块、options API Composition API优先

🔥 难点3:Vue3响应式系统------ref vs reactive

3.1 响应式数据绑定原理

数据绑定(Data Binding) 是Vue的核心机制,确保数据状态界面显示保持同步。

两种绑定方式

vue

xml 复制代码
<template>
  <!-- 1. 单向数据绑定:数据 → 界面 -->
  <div>{{ content }}</div>
  
  <!-- 2. 双向数据绑定:数据 ↔ 界面 -->
  <input v-model="question" />
  
  <!-- 3. 属性绑定 -->
  <input :value="question" @input="question = $event.target.value" />
</template>

3.2 ref 与 reactive 的区别

javascript

csharp 复制代码
import { reactive, ref } from 'vue'

// ref:适用于基本类型
const question = ref('讲一个关于中国龙的故事')  // string
const stream = ref(false)                      // boolean
const count = ref(0)                          // number

// reactive:适用于对象/数组
const state = reactive({
  name: 'Vue',
  version: 3,
  features: ['响应式', '组件化']
})

// 访问方式不同
console.log(question.value)  // ref需要.value
console.log(state.name)      // reactive直接访问

对比表格

特性 ref reactive
数据类型 任何类型(内部包装) 对象、数组
访问方式 .value访问 直接访问属性
模板解包 自动解包(无需.value 直接访问
重新赋值 支持整体替换 不支持整体替换
原理 通过RefImpl类包装 通过Proxy代理

3.3 响应式系统的底层原理

核心机制

  1. 依赖收集 :模板中使用了{{content}},Vue会记录"这个组件依赖content"
  2. 派发更新 :当content.value = '新值'时,Vue知道该组件需要重新渲染
  3. 虚拟DOM:通过diff算法最小化DOM操作

注意 :在<script setup>中,ref会自动解包,所以模板中直接写{{content}}而不是{{content.value}}

3.4 为什么表单元素需要双向绑定?

表单元素(如<input>)是双向数据绑定的典型场景:

vue

xml 复制代码
<template>
  <!-- v-model = :value + @input -->
  <input v-model="question" />
  
  <!-- 等价于 -->
  <input :value="question" @input="question = $event.target.value" />
</template>

<script setup>
const question = ref('')  // 响应式数据
// 用户输入 → 自动更新question → 界面同步更新
</script>

单向绑定 vs 双向绑定

特性 单向绑定 双向绑定
数据流向 数据→界面 数据↔界面
适用场景 纯展示 表单输入
Vue语法 {{}}:prop v-model
数据更新 手动触发 自动同步

🔥 难点4:环境变量安全------Vite的.env机制

设计者为什么这么写?

javascript

javascript 复制代码
'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`

安全问题:API密钥绝对不能硬编码在代码中(否则提交到GitHub就泄露了)。

Vite解决方案

  1. 创建.env.local文件(已被.gitignore忽略):

text

ini 复制代码
VITE_DEEPSEEK_API_KEY=sk-xxxxxx
  1. 代码中通过import.meta.env.VITE_XXX访问 3. Vite构建时会将环境变量编译进静态资源 (注意:仅以VITE_开头的变量会被暴露)

避坑指南

错误做法

javascript

arduino 复制代码
// 硬编码密钥(危险!)
const API_KEY = 'sk-1234567890abcdef'

另一个常见错误

javascript

arduino 复制代码
// 在服务端渲染(SSR)中误用
console.log(process.env.VITE_XXX) // Node环境没有import.meta.env

正确做法

javascript

go 复制代码
// 使用Vite提供的环境变量
const key = import.meta.env.VITE_DEEPSEEK_API_KEY
if (!key) {
  console.error('请检查.env.local文件中的VITE_DEEPSEEK_API_KEY')
}

四、最佳实践:新手避坑指南

🚫 坑1:忘记处理空值

javascript

ini 复制代码
// ❌ 错误:如果response.body为null,getReader()会报错
const reader = response.body.getReader();

// ✅ 正确:使用可选链
const reader = response.body?.getReader();
if (!reader) {
  content.value = '请求失败,请重试';
  return;
}

🚫 坑2:流式输出时内容被覆盖

javascript

ini 复制代码
// ❌ 错误:每次更新都重新赋值,会丢失之前的内容
content.value = delta;

// ✅ 正确:追加内容
content.value += delta;

🚫 坑3:忘记处理DONE标志

如果没有检测到[DONE],while循环会一直执行,导致界面卡死。

javascript

ini 复制代码
// ✅ 正确:遇到[DONE]立即退出
if (payload === '[DONE]') {
  done = true;
  break;
}

🚫 坑4:忽略buffer机制

javascript

javascript 复制代码
// ❌ 错误:直接处理每个chunk,可能拿到不完整的数据
for (const line of chunk.split('\n')) {
  JSON.parse(line); // 可能报错!
}

// ✅ 正确:使用buffer缓存不完整数据
let buffer = '';
// ... 参考上面的完整实现

🚫 坑5:API密钥提交到Git

bash

bash 复制代码
# ✅ 正确:确保.gitignore包含
.env.local
.env.*.local

五、面试高频考点

Q1:Vue的响应式原理是什么?ref和reactive的区别?

回答要点

  • Vue2用Object.defineProperty,Vue3用Proxy(更强大,支持数组、新增属性等)
  • ref用于基本类型,通过.value访问;reactive用于对象
  • 两者都能触发视图更新,但ref在模板中自动解包

Q2:response.body.getReader() 和 reader.read() 的原理?

回答要点

  1. response.bodyReadableStream对象,代表一个可读的字节流

  2. getReader() 创建流读取器,锁定流(只能一个读取器访问)

  3. reader.read() 返回Promise,解析为{value, done}

    • valueUint8Array(二进制数据块)
    • doneboolean(流是否结束)
  4. 每次read()返回一个chunk,需要循环调用直到done:true

  5. 数据是二进制格式,需要用TextDecoder解码为文本

Q3:为什么需要buffer机制?如何实现?

回答要点

  1. 原因:网络传输中,一个完整的消息可能被拆分成多个chunk

  2. buffer的作用:缓存不完整的数据,等待下次拼接完整后再处理

  3. 实现策略

    • 将每次解码后的文本追加到buffer
    • \n分割成行数组
    • 最后一行可能不完整,放回buffer
    • 只处理完整的行
  4. 异常处理 :如果JSON.parse失败,说明数据仍不完整,放回buffer

Q4:TextDecoder的stream参数作用?

回答要点

  • stream:true告诉解码器:数据可能不完整,保留内部状态
  • 正确处理多字节字符(如中文UTF-8编码可能是3个字节)
  • 避免乱码(如"你"被拆成两个chunk时,stream:true能正确组合)

Q5:<script setup> 和普通 <script> 的区别?

回答要点

  1. 暴露方式<script setup>自动暴露顶层声明,普通script需要手动return
  2. 编译优化<script setup>是编译时转换,性能更好
  3. 组件注册<script setup>导入即注册,普通script需在components中注册
  4. 适用场景<script setup>推荐用于Composition API,普通script用于Options API或特殊需求
  5. 宏函数<script setup>支持definePropsdefineEmits等编译宏vue
xml 复制代码
<template>
  <!-- v-model = :value + @input -->
  <input v-model="question" />
  
  <!-- 等价于 -->
  <input :value="question" @input="question = $event.target.value" />
</template>

<script setup>
const question = ref('')  // 响应式数据
// 用户输入 → 自动更新question → 界面同步更新
</script>

单向绑定 vs 双向绑定

特性 单向绑定 双向绑定
数据流向 数据→界面 数据↔界面
适用场景 纯展示 表单输入
Vue语法 {{}}:prop v-model
数据更新 手动触发 自动同步

🔥 难点4:环境变量安全------Vite的.env机制

设计者为什么这么写?

javascript

javascript 复制代码
'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`

安全问题:API密钥绝对不能硬编码在代码中(否则提交到GitHub就泄露了)。

Vite解决方案

  1. 创建.env.local文件(已被.gitignore忽略):

text

ini 复制代码
VITE_DEEPSEEK_API_KEY=sk-xxxxxx
  1. 代码中通过import.meta.env.VITE_XXX访问 3. Vite构建时会将环境变量编译进静态资源 (注意:仅以VITE_开头的变量会被暴露)

避坑指南

错误做法

javascript

arduino 复制代码
// 硬编码密钥(危险!)
const API_KEY = 'sk-1234567890abcdef'

另一个常见错误

javascript

arduino 复制代码
// 在服务端渲染(SSR)中误用
console.log(process.env.VITE_XXX) // Node环境没有import.meta.env

正确做法

javascript

go 复制代码
// 使用Vite提供的环境变量
const key = import.meta.env.VITE_DEEPSEEK_API_KEY
if (!key) {
  console.error('请检查.env.local文件中的VITE_DEEPSEEK_API_KEY')
}

四、最佳实践:新手避坑指南

🚫 坑1:忘记处理空值

javascript

ini 复制代码
// ❌ 错误:如果response.body为null,getReader()会报错
const reader = response.body.getReader();

// ✅ 正确:使用可选链
const reader = response.body?.getReader();
if (!reader) {
  content.value = '请求失败,请重试';
  return;
}

🚫 坑2:流式输出时内容被覆盖

javascript

ini 复制代码
// ❌ 错误:每次更新都重新赋值,会丢失之前的内容
content.value = delta;

// ✅ 正确:追加内容
content.value += delta;

🚫 坑3:忘记处理DONE标志

如果没有检测到[DONE],while循环会一直执行,导致界面卡死。

javascript

ini 复制代码
// ✅ 正确:遇到[DONE]立即退出
if (payload === '[DONE]') {
  done = true;
  break;
}

🚫 坑4:忽略buffer机制

javascript

javascript 复制代码
// ❌ 错误:直接处理每个chunk,可能拿到不完整的数据
for (const line of chunk.split('\n')) {
  JSON.parse(line); // 可能报错!
}

// ✅ 正确:使用buffer缓存不完整数据
let buffer = '';
// ... 参考上面的完整实现

🚫 坑5:API密钥提交到Git

bash

bash 复制代码
# ✅ 正确:确保.gitignore包含
.env.local
.env.*.local

五、面试高频考点

Q1:Vue的响应式原理是什么?ref和reactive的区别?

回答要点

  • Vue2用Object.defineProperty,Vue3用Proxy(更强大,支持数组、新增属性等)
  • ref用于基本类型,通过.value访问;reactive用于对象
  • 两者都能触发视图更新,但ref在模板中自动解包

Q2:response.body.getReader() 和 reader.read() 的原理?

回答要点

  1. response.bodyReadableStream对象,代表一个可读的字节流

  2. getReader() 创建流读取器,锁定流(只能一个读取器访问)

  3. reader.read() 返回Promise,解析为{value, done}

    • valueUint8Array(二进制数据块)
    • doneboolean(流是否结束)
  4. 每次read()返回一个chunk,需要循环调用直到done:true

  5. 数据是二进制格式,需要用TextDecoder解码为文本

Q3:为什么需要buffer机制?如何实现?

回答要点

  1. 原因:网络传输中,一个完整的消息可能被拆分成多个chunk

  2. buffer的作用:缓存不完整的数据,等待下次拼接完整后再处理

  3. 实现策略

    • 将每次解码后的文本追加到buffer
    • \n分割成行数组
    • 最后一行可能不完整,放回buffer
    • 只处理完整的行
  4. 异常处理 :如果JSON.parse失败,说明数据仍不完整,放回buffer

Q4:TextDecoder的stream参数作用?

回答要点

  • stream:true告诉解码器:数据可能不完整,保留内部状态
  • 正确处理多字节字符(如中文UTF-8编码可能是3个字节)
  • 避免乱码(如"你"被拆成两个chunk时,stream:true能正确组合)

Q5:<script setup> 和普通 <script> 的区别?

回答要点

  1. 暴露方式<script setup>自动暴露顶层声明,普通script需要手动return
  2. 编译优化<script setup>是编译时转换,性能更好
  3. 组件注册<script setup>导入即注册,普通script需在components中注册
  4. 适用场景<script setup>推荐用于Composition API,普通script用于Options API或特殊需求
  5. 宏函数<script setup>支持definePropsdefineEmits等编译宏
相关推荐
努力成为AK大王1 小时前
CSS 入门完整笔记
前端·css
微笑挖矿2 小时前
大模型流式渲染:为什么"逐字显示"远比你想的复杂
前端
weedsfly2 小时前
前端开发中的装饰器模式——给函数和组件“加壳”的艺术
前端·javascript·面试
zhedream2 小时前
那个从没改过的页面,为什么突然坏了?
前端·vue.js
工业HMI实战笔记2 小时前
HMI性能优化技巧:告别卡顿,打造流畅体验
前端·ui·性能优化·自动化·交互
QN1幻化引擎2 小时前
Gravity-Anchored Cognitive Field Architecture: The DalinX V8/V10 Implementation
java·前端·算法
光影少年2 小时前
react的View/Text/Image/ScrollView 常用组件注意事项
开发语言·前端·javascript·react.js·前端框架
yangmu32032 小时前
前端性能优化实战:我是如何将首屏加载时间缩短 40% 的?
前端·性能优化
IT_陈寒2 小时前
JavaScript的隐式类型转换又双叒坑了我一次!
前端·人工智能·后端