阅读视图

发现新文章,点击刷新页面。

A股三大指数开盘涨跌不一,中油资本涨停

36氪获悉,A股三大指数开盘涨跌不一,沪指低开0.08%,深成指高开0.07%,创业板指高开0.41%;多元金融、发电设备、重型机械领涨,中油资本涨停,振江股份涨超5%,中船科技涨超2%;贵金属、电脑硬件、休闲用品跌幅居前,盛达资源、绿联科技跌超2%,星辉娱乐跌超1%。

小米汽车:新一代SU7将于3月19日正式上市

36氪获悉,据小米汽车消息,新一代SU7将于3月19日19:00正式上市。小米春季新品发布会还有两款新品:全新高性能超轻薄本,Xiaomi Book Pro 14;全能运动长续航手表,Xiaomi Watch S5。

两位清华校友创业:获谷歌、英伟达系投资人押注,要把机器人数据采集成本降50倍丨36氪首发

作者丨欧雪

编辑丨袁斯来

硬氪获悉,面向机器人与空间智能领域的数据基础设施服务商——Ropedia完成了千万美金级种子轮融资。本轮融资由多位来自谷歌,英伟达,亚马逊的北美天使投资人和亚洲头部美元基金联合投资,深渡资本担任长期独家财务顾问。资金将主要用于核心技术团队扩建、现有产品量产交付,以及市场的持续拓展。

Ropedia于2025年下半年在新加坡成立,致力于为机器人、空间、物理智能等领域提供新一代数据采集与解决方案。

Ropedia由三位联合创始人创立:CEO陈昭熹博士(清华大学本科,南洋理工大学博士,曾在Meta参与光学动捕数据体系搭建)、CTO洪方舟博士(清华大学本科,南洋理工大学博士,曾在Meta从事第一人称多模态智能研究)和首席科学家刘子纬教授(南洋理工大学副教授,计算机视觉领域知名学者,谷歌学术引用超9万次)。此外,南洋理工大学校长讲席教授、CVPR 2026程序大会主席吕健勤担任科学委员会主席。

据陈昭熹介绍,未来十年,智能将打破屏幕的桎梏,来到真实物理世界。但是,就像二维生物无法理解三维生物,通用物理智能无法从低维的互联网数据中获得,而是从三维世界中的交互体验中习得。

与传统意义上的数据采集公司或硬件公司不同,Ropedia旨在构建面向机器人与空间智能的物理世界数据基础设施,通过低门槛采集设备获取真实世界多模态信号,结合自研空间基础模型,将原始数据转化为可直接进入训练与评测流程的数据产品。

当AI开始从数字世界走向物理世界,行业对数据的要求也在发生变化。相比过去以视频和图文对为主的数据形式,机器人与空间智能模型越来越需要具备真实物理尺度、动态交互过程、人体与物体关系、场景结构和任务语义的高质量数据。

但这类数据长期存在两个难题:一是采集成本高,通常依赖昂贵设备和复杂部署;二是即便采到了原始信号,距离真正可用于训练的结构化数据仍然隔着很长一条链路。

因此,在技术路径上,Ropedia选择了一条不同于传统数据公司的路线:用算法能力反向定义采集能力,用模型能力反向降低硬件门槛。

在洪方舟看来,未来行业竞争的关键,不是谁能采到更多原始素材,而是谁能以更低成本、更高效率、更接近训练目标的方式,把现实世界稳定转化为模型可学习的数据资产。

基于这一思路,Ropedia推出了头戴式便携采集系统HOMIE。该系统以轻量化硬件作为入口,采集第一视角下的人体运动、场景变化、物体交互等多模态信号,并结合自研4D重建与对齐算法,恢复带有真实尺度信息的动态世界表示。相较于传统单纯的视频数据,这类数据能够更完整地保留人与环境、人与物体之间的交互过程,更接近机器人学习和评测所需的输入形式。目前,该产品已实现量产,并开始批量交付。

Ropedia 4D人类体验数据可视化(图源/企业)

在洪方舟看来,硬件只是入口,真正的壁垒来自其背后的模型与数据管线能力。更强的4D重建与结构化标注能力,意味着公司可以使用成本更低、部署更灵活的采集设备进入真实场景;而持续积累的真实任务数据,又会反过来提升模型精度、对齐能力和交付效率。

相比单纯依赖硬件迭代的数据采集模式,这种“设备+模型+数据管线”的组合,也使Ropedia更像一家底层基础设施公司,而非单点设备供应商。

Ropedia核心数据产品的矩阵维度(图源/企业)

商业化层面,Ropedia从成立初期便将北美作为核心市场之一。一方面,北美聚集了大量前沿机器人公司、空间智能团队和相关研究机构,对高质量物理世界数据的需求更早出现,也更为明确;另一方面,公司以新加坡作为全球化运营枢纽,在供应链组织、跨境协作、合规交付和国际研发合作上具备一定优势。

目前,Ropedia客户覆盖北美十多家头部具身智能和空间智能公司,公司现阶段已形成“采集设备+数据服务+标准化交付”的综合模式,服务对象涵盖机器人、空间智能及相关研究团队。

谈及未来规划,Ropedia表示,短期内公司将继续夯实数据生产与交付管线,进一步强化质量、成本与效率优势。中期则瞄准4D物理数据相关标准的推动与定义。更长期来看,公司希望构建围绕物理智能的数据基础设施网络,服务更多机器人、空间智能与现实世界AI应用场景。

v-once和v-memo完全指南:告别不必要的渲染,让应用飞起来

前言

在日常开发中,我们可能遇到过这样的情况:写了一个 Vue 应用,数据量稍微大一点,页面就开始卡顿;用户只是点击了一个按钮,整个页面都要重新渲染;明明大部分内容都没变,却感觉应用像“老了十岁”一样慢。这是为什么呢?

Vue 的响应式系统很智能,但它也有“过度反应”的时候。就像我们只是拍了拍桌子,整个办公室的人都站起来看看发生了什么——这显然是一种浪费。

v-oncev-memo 就是来解决这个问题的。它们像两个聪明的“保安”,告诉 Vue:“这部分内容不用每次都检查,它没变” 和 “这部分内容只有在特定条件变化时才需要检查”。

本文将从最基础的概念讲起,用最通俗的语言,配合完整的代码示例,帮助我们彻底掌握这两个性能优化神器。

为什么要关注不必要的渲染

从一个简单的例子开始

我们先来看一个简单的例子:

<template>
  <div>
    <!-- 动态内容:会变化 -->
    <h2>当前计数:{{ count }}</h2>
    <button @click="count++">点我增加</button>
    
    <!-- 静态内容:永远不会变 -->
    <footer>
      <p>© 2026 我的公司. 版权所有</p>
      <p>联系方式:contact@example.com</p>
      <p>地址:xxx</p>
    </footer>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

这段代码看起来没什么,但实际上会发生了什么呢?

每次点击按钮是,count 都会变化,整个组件都会重新渲染。包括那个 永远不会变 的页脚。

虽然 Vue 的虚拟 DOM 会最终发现页脚没变,不会更新真实的 DOM,但这个过程仍然需要:

  • 执行渲染函数
  • 创建新的虚拟 DOM
  • 和旧的虚拟 DOM 进行对比
  • 确认没有变化,跳过更新

这就像我们每天早上去公司,尽管保安每天都会看到我们,但他们仍然每天都要重新核对我们的身份信息,这是一种不必要的浪费。

Vue 的默认更新机制

响应式数据变化
    ↓
组件重新渲染函数执行
    ↓
生成新的虚拟 DOM 树
    ↓
与旧虚拟 DOM 进行 diff 比较
    ↓
计算出需要更新的真实 DOM
    ↓
执行 DOM 更新

不必要的渲染有多"贵"?

我们先看一段数据:

组件规模 一次不必要的渲染耗时 每天10万次操作 额外开销
小型组件(50个节点) 0.5ms 50,000ms 50秒
中型组件(200个节点) 2ms 200,000ms 3.3分钟
大型组件(1000个节点) 10ms 1,000,000ms 16.7分钟

想象一下,用户每天要多等十几分钟,就因为应用在“瞎忙活”。

什么是不必要的渲染?

简单来说就是:渲染的结果和上一次 完全一样,但过程却重复执行了。

// 这是一个"不必要的渲染"的典型案例
const App = {
  template: `
    <div>
      <!-- 这部分每次都会重新计算,但结果永远一样 -->
      <div>{{ getStaticData() }}</div>
      
      <!-- 这部分确实需要更新 -->
      <div>{{ dynamicData }}</div>
    </div>
  `,
  
  methods: {
    getStaticData() {
      console.log('我被调用了!') // 其实只需要调用一次
      return '永远不变的内容'
    }
  }
}

问题:即使大部分内容没变,渲染函数仍会执行,虚拟 DOM 树仍会创建,diff 算法仍需遍历。

v-once:一次渲染,终身躺平

v-once 是什么?

v-once 是 Vue 提供的一个指令,它的作用就像它的名字一样:只渲染一次。之后无论数据怎么变化,这部分内容都不会再更新。

用生活化的比喻理解v-once

想象一下,我们正在装修房子:

  • 普通渲染:每天都要重新粉刷一遍墙壁,尽管颜色没变
  • v-once 渲染:装修一次,以后再也不动它

v-once 的基本用法

<template>
  <div>
    <!-- 普通内容:每次count变化都会更新 -->
    <p>当前计数:{{ count }}</p>
    
    <!-- v-once内容:只渲染一次,之后永远不变 -->
    <p v-once>初始计数:{{ count }}</p>
    
    <button @click="count++">增加计数</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

运行效果

  • 首次加载:两个都显示“0”
  • 点击按钮:上面变成“1”,下面还是“0”
  • 继续点击:上面一直变,下面永远是“0”

v-once的工作原理

让我们用流程图来理解:

首次渲染
    ↓
遇到 v-once 指令
    ↓
正常渲染内容
    ↓
将生成的虚拟DOM缓存起来
    ↓
打上"静态标记"
    ↓
─────────────────
    ↓
后续更新时
    ↓
遇到 v-once 标记
    ↓
直接返回缓存的虚拟DOM
    ↓
跳过所有更新逻辑

v-once 的实现机制

// 简化版的 v-once 实现原理
function processOnceNode(vnode) {
  if (vnode.shapeFlag & ShapeFlags.COMPONENT_ONCE) {
    // 如果是组件,标记为静态组件
    vnode.isStatic = true
    return vnode
  }
  
  // 如果是元素,创建静态节点
  const staticNode = createStaticVNode(
    vnode.children,
    vnode.props
  )
  
  // 后续更新直接返回缓存的静态节点
  return staticNode
}

v-once 的适用场景

场景一:页脚版权信息等纯静态内容

<!-- 页脚版权信息,永远不变 -->
<footer v-once>
  <p>© 2026 我的公司. All rights reserved.</p>
  <p>ICP备案号:xxxxx</p>
  <div class="contact">
    <p>邮箱:contact@example.com</p>
    <p>电话:400-123-4567</p>
  </div>
</footer>

场景二:一次性初始数据

<template>
  <div class="user-profile">
    <!-- 用户 ID 只在创建时显示,后续不变 -->
    <div v-once class="user-meta">
      <span>用户ID:{{ userId }}</span>
      <span>注册时间:{{ registerDate }}</span>
      <span>会员等级:{{ initialLevel }}</span>
    </div>
    
    <!-- 动态更新的内容 -->
    <div class="user-points">
      当前积分:{{ points }}
      <button @click="points++">签到</button>
    </div>
  </div>
</template>

场景三:复杂的静态组件

<template>
  <div class="dashboard">
    <!-- 左侧:帮助文档组件,完全静态,只需加载一次 -->
    <HelpDocumentation v-once class="sidebar" />
    
    <!-- 右侧:动态更新的内容 -->
    <div class="main-content">
      <DashboardCharts :data="liveData" />
      <RealTimeLogs :logs="systemLogs" />
    </div>
  </div>
</template>

场景四:与 v-for 配合优化列表

<template>
  <div class="data-table">
    <!-- 表格头部完全静态 -->
    <div v-once class="table-header">
      <div class="col">姓名</div>
      <div class="col">年龄</div>
      <div class="col">部门</div>
      <div class="col">操作</div>
    </div>
    
    <!-- 动态列表项 -->
    <div v-for="item in list" :key="item.id" class="table-row">
      <div class="col">{{ item.name }}</div>
      <div class="col">{{ item.age }}</div>
      <div class="col">{{ item.department }}</div>
      <div class="col">
        <button @click="edit(item.id)">编辑</button>
      </div>
    </div>
  </div>
</template>

v-once 的使用注意事项

注意事项 说明 示例
失去响应性 v-once 内的所有数据绑定都变成静态,不再响应更新 <div v-once>{{ count }}</div> 永远不会更新
子树全静态 v-once 作用于元素时,其所有子元素也变为静态 整个组件树都会静态化
避免滥用 只在真正不需要更新的地方使用,否则会导致数据和视图不一致 动态内容不能用 v-once
组件中使用 组件上加 v-once,整个组件只会渲染一次 <ComplexChart v-once />

v-once 性能收益实测

测试环境

  • 页面包含 200 个静态节点
  • 每秒触发 10 次更新
  • 运行 60 秒
指标 未优化 使用 v-once 提升
渲染函数调用次数 60,000 次 600 次 99%
虚拟 DOM 创建 60,000 次 600 次 99%
内存分配 850MB 85MB 90%
CPU 使用率 65% 8% 88%
平均帧率 45fps 60fps 33%

v-memo:有条件地记忆渲染

为什么要 v-memo?

v-once 虽然好,但它的缺点也很明显:要么永远更新,要么永远不更新。现实开发中,我们经常遇到这样的情况:

  • 列表项的大部分内容稳定,但少数字段会变
  • 组件的大部分数据不变,但需要响应某些特定变化

这时候就需要 v-memo 了。

v-memo 是什么?

v-memo 是 Vue 3.2+ 引入的新指令,它可以接受一个依赖数组,只有当数组中的值变化时,才会重新渲染。

用生活化的比喻理解 v-memo

想象一下,我们在公司里:

  • 普通员工:领导一喊,所有人都站起来(不管是不是叫自己)
  • v-memo 员工:只有听到自己名字才站起来

v-memo的基本用法

<template>
  <div 
    v-for="item in items" 
    :key="item.id"
    v-memo="[item.id, item.price, item.stock]"
  >
    <!-- 只有当 item.id、item.price 或 item.stock 变化时才重新渲染 -->
    <h3>{{ item.name }}</h3>
    <p>价格:{{ item.price }}</p>
    <p>库存:{{ item.stock }}</p>
    <button @click="toggleFavorite(item.id)">
      {{ item.isFavorite ? '取消收藏' : '收藏' }}
    </button>
  </div>
</template>

v-memo的工作原理

让我们用流程图来理解:

首次渲染
    ↓
计算依赖数组的值
    ↓
缓存这些值和生成的虚拟DOM
    ↓
─────────────────
    ↓
后续更新触发
    ↓
重新计算依赖数组的新值
    ↓
和缓存的值比较
    ↓
有变化?→ 是 → 重新渲染,更新缓存
    ↓       
    否
    ↓
直接返回缓存的虚拟DOM
    ↓
跳过所有更新逻辑

v-memo 工作机制的三阶段

1. 依赖收集阶段

  • 编译时解析依赖数组
  • 建立响应式依赖图谱
  • 为每个节点创建 memo 缓存

2. 缓存对比阶段

  • 重新渲染前计算依赖数组的新值
  • 与缓存的上次值进行浅比较
  • 若未变化 → 直接复用缓存的 VNode 树
  • 若已变化 → 重新生成 VNode 并更新缓存

3. 虚拟 DOM 跳过

  • 完全跳过该节点的 diff 计算
  • 不触发子树的渲染函数
  • 直接复用真实 DOM

v-memo的实战场景

场景一:超大规模商品列表

想象一个电商网站的商品列表,有1万件商品:

<template>
  <div class="product-list">
    <div 
      v-for="product in products" 
      :key="product.id"
      v-memo="[
        product.id, 
        product.price, 
        product.stock, 
        product.isFavorite
      ]"
      class="product-item"
    >
      <img :src="product.image" :alt="product.name" />
      <h3>{{ product.name }}</h3>
      <p class="price">¥{{ product.price }}</p>
      <p class="stock">库存: {{ product.stock }}件</p>
      <p class="sales">销量: {{ product.sales }}件</p>
      <p class="rating">评分: {{ product.rating }}分</p>
      <button 
        @click="toggleFavorite(product.id)"
        :class="{ active: product.isFavorite }"
      >
        {{ product.isFavorite ? '已收藏' : '收藏' }}
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

// 生成1万件商品
const products = ref(
  Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `商品 ${i}`,
    price: Math.floor(Math.random() * 1000),
    stock: Math.floor(Math.random() * 100),
    sales: Math.floor(Math.random() * 1000),
    rating: (Math.random() * 5).toFixed(1),
    image: `https://picsum.photos/200/150?random=${i}`,
    isFavorite: false
  }))
)

function toggleFavorite(id) {
  const product = products.value.find(p => p.id === id)
  product.isFavorite = !product.isFavorite
  // ✅ 只有被点击的那一项会重新渲染
}
</script>

优化效果:

  • 用户点击收藏时,只有被点击的商品重新渲染
  • 后台更新价格时,只有价格变化的商品重新渲染
  • 其他 9999 件商品完全不动

场景二:复杂计算缓存

<template>
  <div class="dashboard">
    <!-- 只有当原始数据或用户设置变化时才重新计算 -->
    <div 
      class="dashboard-content"
      v-memo="[rawData.version, userSettings.theme]"
    >
      <DashboardHeader />
      
      <!-- 这里的数据需要复杂计算 -->
      <DataVisualization :data="processedData" />
      <StatsCards :stats="computedStats" />
      <ActivityChart :chart-data="chartData" />
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const rawData = ref(fetchData()) // 10MB的原始数据
const userSettings = ref({ theme: 'light', language: 'zh' })

// 复杂计算:处理10MB数据
const processedData = computed(() => {
  console.log('正在处理数据...') // 我们希望这个不要频繁执行
  return rawData.value.map(item => ({
    ...item,
    processed: heavyComputation(item)
  }))
})

// 当用户切换主题时,不应该重新计算processedData
// 但上面的v-memo确保了这一点:只有rawData.version或userSettings.theme变化时才重新渲染
</script>

场景三:聊天消息列表

<template>
  <div class="chat-messages">
    <div 
      v-for="msg in messages" 
      :key="msg.id"
      v-memo="[msg.id, msg.content, msg.timestamp, msg.isRead]"
      class="message"
      :class="{ 'message-self': msg.senderId === currentUserId }"
    >
      <img :src="msg.avatar" class="avatar" />
      <div class="content">
        <div class="sender">{{ msg.senderName }}</div>
        <div class="text">{{ msg.content }}</div>
        <div class="time">{{ formatTime(msg.timestamp) }}</div>
      </div>
      <div class="status">
        <span v-if="msg.isRead">已读</span>
        <span v-else-if="msg.isSending">发送中...</span>
        <span v-else-if="msg.isFailed">发送失败</span>
      </div>
    </div>
  </div>
</template>

<script setup>
const messages = ref([])

// 新消息到来时,只有新消息会渲染
// 已读状态变化时,只有那条消息会更新
// 其他消息完全不动
</script>

场景四:选中状态高亮

<template>
  <div class="image-gallery">
    <div 
      v-for="image in images" 
      :key="image.id"
      v-memo="[selectedId === image.id]"
      class="image-item"
      :class="{ selected: selectedId === image.id }"
      @click="selectedId = image.id"
    >
      <img :src="image.thumbnail" :alt="image.title" />
      <div class="overlay">
        <h4>{{ image.title }}</h4>
        <button @click.stop="download(image.id)">下载</button>
      </div>
    </div>
  </div>
</template>

<script setup>
const selectedId = ref(null)

// 点击时,只有之前选中的和当前选中的两个图片会重新渲染
// 其他9998张图片完全不动
</script>

v-memo 依赖项选择的黄金法则

  • 精准包含:只放那些真正会影响渲染的字段
  • 避免冗余:不要把整个对象放进去
  • 稳定依赖:不要用 Date.now() 这种每次都变的值
  • 版本控制:复杂对象可以用版本号

选择决策树

graph TD
    Start[遇到一个组件/元素] --> Question1{内容永远不变吗?}
    Question1 -->|是| A[用 v-once]
    Question1 -->|否| Question2{是长列表?<br>(>500项)}
    
    Question2 -->|否| B[暂时不需要优化]
    Question2 -->|是| Question3{更新频率高吗?}
    
    Question3 -->|低| C[保持现状]
    Question3 -->|高| Question4{能否精确控制更新?}
    
    Question4 -->|否| D[考虑虚拟滚动]
    Question4 -->|是| E[用 v-memo 精确优化]

v-once vs v-memo,如何选择?

特性对比表

对比维度 v-once v-memo
适用版本 Vue 2+ Vue 3.2+
更新策略 永不更新 条件更新
依赖声明 显式数组
学习难度 ⭐⭐⭐
适用场景 纯静态内容 大部分稳定的动态内容
代码侵入性

组合使用示例

<template>
  <div class="app">
    <!-- 1. 完全静态的头部 -->
    <header v-once>
      <AppLogo />
      <AppTitle />
      <NavigationMenu />
    </header>
    
    <!-- 2. 动态列表,但有条件更新 -->
    <div class="content">
      <div 
        v-for="item in items" 
        :key="item.id"
        v-memo="[item.id, item.updatedAt]"
      >
        <!-- 2.1 每个列表项内部的静态部分 -->
        <div v-once class="item-static">
          <img :src="item.avatar" />
          <span>ID: {{ item.id }}</span>
        </div>
        
        <!-- 2.2 每个列表项内部的动态部分 -->
        <div class="item-dynamic">
          <h3>{{ item.title }}</h3>
          <p>{{ item.content }}</p>
          <span>点赞: {{ item.likes }}</span>
        </div>
      </div>
    </div>
    
    <!-- 3. 完全静态的页脚 -->
    <footer v-once>
      <Copyright />
      <ContactInfo />
    </footer>
  </div>
</template>

性能收益对比

场景 优化前 v-once v-memo
静态页脚 每次更新都渲染 0次更新 不适用
收藏按钮点击 整个列表重绘 不适用 只更新单个项
价格批量更新 整个列表重绘 不适用 只更新价格变化项
列表项1000条 120ms 不适用 35ms

常见陷阱与解决方案

v-memo 依赖遗漏

<!-- ❌ 错误:遗漏了关键依赖 -->
<div 
  v-for="item in items"
  v-memo="[item.id]"
>
  {{ item.name }}  <!-- 当name变化时,这里不会更新! -->
  <span :class="{ active: item.isActive }">
    {{ item.status }}
  </span>
</div>

<!-- ✅ 正确:包含所有依赖 -->
<div 
  v-for="item in items"
  v-memo="[item.id, item.name, item.isActive, item.status]"
>
  {{ item.name }}
  <span :class="{ active: item.isActive }">
    {{ item.status }}
  </span>
</div>

在错误的位置使用 v-memo

<!-- ❌ 错误:在父容器上使用v-memo -->
<ul v-memo="[items.length]">
  <li v-for="item in items" :key="item.id">
    {{ item.name }}
  </li>
</ul>
<!-- 结果:items.length不变时,整个列表都不更新 -->
<!-- 但item.name变化时也不会更新! -->

<!-- ✅ 正确:在v-for的项上使用 -->
<ul>
  <li 
    v-for="item in items" 
    :key="item.id"
    v-memo="[item.id, item.name]"
  >
    {{ item.name }}
  </li>
</ul>

滥用v-once导致bug

<!-- ❌ 错误:动态内容用了v-once -->
<div v-once>
  <h3>当前用户:{{ username }}</h3>  <!-- 永远不会更新! -->
  <button @click="logout">退出登录</button>
</div>

<!-- ✅ 正确:只静态化真正静态的部分 -->
<div>
  <h3>当前用户:{{ username }}</h3>  <!-- 动态 -->
  <div v-once>操作面板</div>  <!-- 静态 -->
  <button @click="logout">退出登录</button>  <!-- 动态 -->
</div>

最佳实践清单

什么时候用 v-once?

  • 版权信息、页脚
  • 表格表头
  • 静态导航菜单
  • 一次性初始数据
  • 复杂的静态组件(帮助文档、使用说明)

什么时候用 v-memo?

  • 超长列表(>500项)
  • 高频更新的区域隔离
  • 选中状态切换
  • 复杂计算的缓存
  • 聊天消息列表

优化检查清单

  • v-memo 的依赖数组包含了所有影响渲染的字段
  • 避免在 v-memo 中使用 Date.now()Math.random()
  • v-memo 正确放在 v-for 的项上,而不是父容器
  • v-once 只用于真正静态的内容
  • 组合使用时逻辑清晰
  • 用性能工具验证了优化效果

性能优化的哲学

  1. 优化不是炫技:用数据和用户体感说话
  2. 适度原则:不是所有地方都需要优化
  3. 持续演进:性能优化是过程,不是终点
  4. 量化的力量:没有数据的优化是盲目的

结语

v-oncev-memo 是 Vue 提供的两个强大的优化工具,但它们不是银弹。真正的性能优化,是在理解业务场景的基础上,选择合适的技术,验证优化效果,持续改进的过程。让该更新的更新,该躺平的躺平,这才是 Vue 性能优化的真谛!

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

半导体又迎涨价潮:消息称联电、世界先进、力积电等晶圆代工厂涨价

据台湾经济日报,继存储芯片、封装之后,半导体产业链或迎来新一波涨价潮。成熟制程晶圆代工厂联电、世界先进、力积电等最快4月起调升报价,幅度最高达一成甚至更多。其中,联电不回应市场涨价传言,不过该公司此前提到,目前订价环境“确实较先前有利”;世界先进涨价函显示,拟自2026年4月起调整代工价格,但没有透露本次涨幅,公司未回应涨价;力积电则证实,本季起已陆续涨价,主要调整毛利率较低的产品线。此外,成熟制程大宗客户,以驱动IC为首的IC设计厂因成本上扬,也规划涨价。(财联社)

“纽邦生物”完成超亿元C轮融资

36氪获悉,“纽邦生物”近日宣布完成超亿元C轮融资。本轮融资由明熙资本领投,光谷产投跟投,老股东毅达资本、华熙元祐基金持续加注。融资资金将主要用于新技术平台的搭建、产能扩建以及全球商业化版图的深化。

中国人民银行副行长陆磊出席美洲开发银行集团2026年年会

36氪获悉,据央行网站,2026年3月13日至14日,中国人民银行副行长陆磊出席在巴拉圭举行的美洲开发银行集团2026年年会。参会各方就美洲开发银行集团改革进展、拉美地区经济金融形势等议题进行交流。会议期间,陆磊与美洲开发银行行长伊兰·戈德法恩等举行会谈,就加强双边合作等议题交换了意见。

2026 年前端 Agent 框架选型:Mastra 与 LangChain 该怎么选

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优化和协作功能的实现等核心难点。

如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。

如果你对 AI全栈 感兴趣,也欢迎添加我微信,我拉你进交流群

2026 年的前端圈卷出了新高度,AI Agent 已是各类 Web 应用的标配。官网智能客服、内部任务助手、产品内的搜索与推荐,都绕不开一件事:用哪个框架把大模型和工具串起来。不少团队会在 LangChain.js 和 Mastra 之间反复纠结,架构评审时也常为此争论。

两者没有绝对优劣,差别主要在"设计哲学"和"业务场景"的匹配度。Mastra 像为前端量身定制的厨师刀,刀刃顺手、切菜切肉都轻松;LangChain.js(尤其是 LangGraph.js)则像重型瑞士军刀,刀锯镊子开瓶器齐全,能应付各种复杂场景,代价是重量和复杂度都更高。下文从 2026 年技术生态出发做一次对比,并配上代码与图示,方便你理清思路、少走弯路。

为什么前端选型会卡在这两个框架上

前端接大模型、做 Agent,本质是三件事:把用户输入送给 LLM、根据输出决定下一步(是否调工具、是否多轮对话)、再把结果还给用户。不同框架在这三条链路上的抽象程度和侧重点差异很大。一类把"循环调工具、拼消息"全包在内部,对外只暴露"发消息、拿回复",你几乎不用关心内部调了几轮工具;另一类把节点、边、状态都暴露给你,自己搭图,灵活性高,但概念和代码量都上去,得先建立"图"的思维才能写得顺手。

LangChain 从 Python 生态长出来,后有 langchain-js,再后来复杂编排催生了 LangGraph,面向任意语言和部署环境的通用编排,概念多、集成广,前端只是消费端之一。文档里会反复出现 Runnables、LCEL、RunnableSequence、RunnablePassthrough 以及各种 @langchain/xxx 包,学习路径会先经过"什么是 Runnable、reducer、checkpointer"这一串概念。Mastra 则从 TypeScript 和现代前端框架出发,默认你在用 Next.js、Nuxt 等全栈框架,API 和类型系统都围着前端习惯转,包名和概念更收敛,文档集中在"在 React、Server Actions 里怎么用",很少逼你先学一整套编排术语。

选型归根结底就两点:团队和产品更接近"通用 AI 编排"还是"前端优先的轻量 Agent"。前者偏向后端或全栈做复杂系统,愿意为灵活性和生态付学习成本;后者偏向前端或小团队在现有 Web 应用里快速接一层智能,希望少概念、少依赖、快上线。

如下图所示。

20260314114857

两种哲学一目了然:一侧是前端优先的轻量体验,一侧是通用编排与生态。

Mastra 的定位与优势

Mastra 从诞生起就面向 TypeScript 和现代前端框架(Next.js、Nuxt 等),针对前端痛点,主打开发者体验(DX)。

TypeScript 原生与类型安全

端到端类型推导做得很好:Agent 的输入、输出和工具调用参数在 IDE 里都有完整类型提示和自动补全,不必手写类型转换或 as 断言。工具用 zod 或 TypeScript 类型定义入参,框架自动生成模型可用的 schema 并做运行时校验。例如在 createTool 里写 inputSchema: z.object({ location: z.string() }),调用时入参即被推断为 { location: string },返回值与 outputSchema 对齐,和现有基于 zod 的表单校验、API 契约也容易打通。

轻量且贴合全栈框架

部署在 Vercel、Cloudflare Workers 等 Serverless 或 Edge 上时,Mastra 的冷启动和边缘兼容性通常更好。没有 LangChain 那套 Runnables、LCEL 等抽象层,依赖树干净,打包体积可控,不必为"跑通一个带工具的 Agent"拉满 @langchain/core@langchain/openai@langchain/langgraph 等一长串包。在 Next.js 的 Server Action、Route Handler 里直接调 Mastra Agent,心智负担小,和现有数据流(表单、状态、API)易对齐,也方便和 React Server Components、流式 SSR 配合。

心智负担低

API 贴近前端数据流直觉:发一段消息、拿一段回复、必要时调几个工具。Mastra 把 LLM 调度、工具解析和流式输出包起来,用简单异步函数或 React 友好接口暴露,不必理解"图、节点、条件边、reducer",会写 createToolnew Agent、会调 generate 或流式方法就能跑通,适合作为团队第一个 Agent 项目的起点。

适合的场景小结

Mastra 特别适合这几类情况:

  • Agent 主要是 Web 应用的辅助功能(智能搜索、客服助手、简单数据总结或表单建议),且深度绑定 Next.js、React 生态。
  • 团队以前端或全栈为主,不想引入过重后台架构,希望快速迭代上线,同时要类型安全和良好调试体验。
  • 对依赖体积、冷启动和 Edge 兼容性敏感,不想为用不到的能力背上整座 LangChain 生态。

LangChain.js 与 LangGraph 的定位与优势

到 2026 年,单纯用 LangChain 搞复杂 Agent 已不够用,实际在评估的往往是 LangGraph.js,它是处理复杂、有状态、多 Agent 协作时的常用方案。

生态系统覆盖广

冷门向量库、大模型厂商、各种外部 API,LangChain 生态里大多已有现成集成。Pinecone、Weaviate、Qdrant、Chroma、自建 REST、OpenAI、Anthropic、Cohere、国产大模型,以及 Tavily、SerpAPI 等,多数有官方或社区的 @langchain/xxx 包。公司内有老旧系统、私有模型或特定协议时,也容易在现有集成上做薄封装,复用 LangChain 的 Runnable、消息格式和工具约定,快速对接大量外部依赖时能省下不少适配和调试时间。

状态机与图逻辑(LangGraph)

需要"循环思考、多路分支、人类介入(Human-in-the-loop)"的复杂工作流时,LangGraph 的图架构能精确控制节点流转。节点是处理单元(一次 LLM 调用、工具执行或人工审核),边是状态转移(固定边或条件边)。状态可持久化到 checkpointer,刷新或断线重连后从断点继续,适合多轮任务和多人协作,也是 Mastra 目前不直接提供的部分。

过度抽象的代价

学习曲线陡:Runnables、Chains、Tools、Nodes、Edges、Annotation、reducer、checkpointer 等概念交织,新手易迷路。实现"用户问一句、模型调一次工具再回答"这种简单功能,也要先理解状态结构、写 agent 与 tools 节点、配条件边和普通边再 compile,代码量明显多于"Agent 配置 + 一次 generate"。报错常来自链式调用的某一层,堆栈里是 LangChain 内部的 Runnable 名,前端背景的开发者需要时间习惯"从图的角度想问题"。LangGraph 的 TypeScript 类型虽完整,但状态是运行时用 Annotation 和 reducer 拼出来的,和 Mastra 那种"工具入参即 zod schema、一眼能看出类型"的体验比,心智负担更大。

适合的场景小结

LangChain、LangGraph 更适合这几类情况:

  • 核心业务就是复杂 AI 系统:多 Agent 协作、长时运行异步任务、或需精准控制"思考中断与恢复"。
  • 集成需求多且杂,要接内部老旧系统或非常小众的向量库、模型接口。
  • 要对底层 Prompt、重试、记忆(Memory)做深度定制,甚至改框架默认行为。

核心能力对比

用一张表概括两个方向在关键维度上的差异,细节在前后文展开。

维度 Mastra LangChain.js / LangGraph
设计核心 极致 DX、原生 TS、轻量化 复杂编排、状态管理、大生态
学习曲线 平缓,熟悉 TS 即可快速上手 陡峭,需理解大量框架专属概念
调试体验 堆栈清晰,贴合前端习惯 多层抽象,报错有时难以定位
多 Agent 支持,更适合简单链式交互 极强,循环与状态打断控制完善
生态与集成 精选集成,覆盖主流工具 海量集成,几乎覆盖常见基础设施
依赖与体积 包少、体积小,Edge 友好 多包组合,体积与冷启动略大

Mastra 通常只需 @mastra/core 加模型适配(如 OpenAI),LangChain 则常需 @langchain/core@langchain/openai(或其它模型包)、@langchain/langgraph,再接向量库或 RAG 还会多几个包,在 Serverless 冷启动和 Edge 里更敏感一些。

如下图所示。

20260314115005

从设计重心到依赖体积,一张图能看清两边差异。下面用两段代码对比同一需求的实现方式,再给出选型决策说明。

用代码感受两种 API 风格

同一需求"做一个能查天气的对话 Agent",在 Mastra 和 LangGraph 里写出来的代码量和抽象层次差很多,看一遍再想选型会直观不少。

Mastra:工具 + Agent 几行搞定

在 Mastra 里用 createTool 定义工具的入参(zod)、描述和执行函数,创建 Agent 时把工具挂上去即可。调用时直接 agent.generate() 或流式接口,不用关心"模型要不要调工具、调完要不要再推理",框架内部处理。

下面示例定义了一个查天气工具和一个使用该工具的 Agent。工具入参用 z.object 声明,execute 的返回值与 outputSchema 一致,整条链路在 IDE 里都有类型推导。示例使用 OpenAI 当前主力模型 gpt-5.4,实际项目里可通过环境变量配置 API Key。

import { createTool } from "@mastra/core/tools";
import { Agent } from "@mastra/core/agent";
import { z } from "zod";

const getWeather = createTool({
  id: "get_weather",
  description: "根据城市名称查询当前天气,适合回答天气相关提问",
  inputSchema: z.object({
    location: z.string().describe("城市名称,如北京、上海"),
  }),
  outputSchema: z.object({ summary: z.string(), temp: z.number().optional() }),
  execute: async ({ location }) => {
    // 实际项目里这里调和风、OpenWeather 等 API
    return { summary: `${location} 晴`, temp: 22 };
  },
});

const weatherAgent = new Agent({
  id: "weather-agent",
  name: "天气助手",
  instructions: "你是天气助手,用 get_weather 查天气并简洁回复用户。",
  model: "openai/gpt-5.4",
  tools: { getWeather },
});

// 在 Next.js Route Handler 或 Server Action 里直接调用
const result = await weatherAgent.generate("北京今天天气怎么样?");
console.log(result.text);

在 Next.js 的 Route Handler 里暴露成 API 时,导入 weatherAgent,对请求体里的消息调 generate 或流式方法即可,不必再写状态机或图。

LangGraph:显式建图与状态

在 LangGraph 里,要先定义状态结构(例如消息列表)、再定义"agent"节点(调用模型、可能产生 tool_calls)和"tools"节点(执行工具并返回 ToolMessage),最后用边把节点串起来,并加上"是否继续调工具"的条件边。模型用 LangChain 的 ChatOpenAI 接 OpenAI 最新模型,工具用 bindTools 绑定,循环由图的拓扑自然形成。

下面这段示例用 StateGraph 定义了一个单 Agent、带一个天气工具的最小图。状态里只有 messages,agent 节点读最后一条用户消息并调用模型,若返回 tool_calls 则路由到 tools 节点,执行完再回到 agent,直到模型不再调工具为止。可与上面 Mastra 示例对照,体会"图"和"状态"的显式写法。

import { StateGraph, Annotation, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { HumanMessage, AIMessage, BaseMessage } from "@langchain/core/messages";
import { ToolNode } from "@langchain/langgraph/prebuilt";

const model = new ChatOpenAI({
  model: "gpt-5.4",
  apiKey: process.env.OPENAI_API_KEY,
});

const getWeather = tool(
  async (input: { location: string }) => `${input.location} 晴,22℃`,
  {
    name: "get_weather",
    description: "根据城市名称查询当前天气",
    schema: z.object({ location: z.string() }),
  },
);

const modelWithTools = model.bindTools([getWeather]);
const toolNode = new ToolNode([getWeather]);

const AgentState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (left, right) => left.concat(right),
    default: () => [],
  }),
});

async function agentNode(state: typeof AgentState.State) {
  const response = await modelWithTools.invoke(state.messages);
  return { messages: [response as AIMessage] };
}

function shouldContinue(state: typeof AgentState.State): "tools" | "end" {
  const last = state.messages[state.messages.length - 1] as AIMessage;
  return last.tool_calls?.length ? "tools" : "end";
}

const graph = new StateGraph(AgentState)
  .addNode("agent", agentNode)
  .addNode("tools", toolNode)
  .addEdge("tools", "agent")
  .addConditionalEdges("agent", shouldContinue, { tools: "tools", end: END })
  .compile();

const result = await graph.invoke({
  messages: [new HumanMessage("北京今天天气怎么样?")],
});
console.log(result.messages[result.messages.length - 1]);

同样实现"用户问天气、模型调工具、再回复":Mastra 是"Agent + tools 配置 + 一次 generate",LangGraph 是"状态注解 + 两节点 + 条件边 + compile"。前者适合快速落地和前端集成,后者适合加人工审核、多 Agent 分支、断点续跑等复杂控制。示例中 LangGraph 使用 gpt-5.4,API Key 建议从环境变量 OPENAI_API_KEY 读取。

选型决策思路

可以按"产品形态、团队基因、集成与定制需求"三条线问自己,再对照上文对比。决策主线就一条:先看 Agent 是"应用的核心"还是"应用里的辅助能力"。核心场景(多 Agent、长任务、状态中断与恢复、大量冷门集成或深度定制)更倾向 LangGraph;辅助能力(Next/React 为主、快速迭代、极重 TypeScript 与 DX)更倾向 Mastra。不必二选一,也可以简单对话用 Mastra、复杂管线用 LangGraph,按模块边界拆。

如下图所示。

20260314115134

从"核心还是辅助"出发,到倾向 LangGraph 或 Mastra(或两者组合)的决策路径。

更偏向选 Mastra 的情况

  • 产品形态上,Agent 主要作为 Web 应用的辅助功能(智能搜索、客服助手、简单数据总结等),且深度绑定 Next.js、React 生态。
  • 团队以前端、全栈为主,不想引入过重的后台架构,希望快速迭代、快速上线。
  • 你非常看重 TypeScript 的类型安全和开发体验,对臃肿依赖和难以排查的报错比较排斥。

更偏向选 LangChain / LangGraph 的情况

  • 产品形态上,核心业务就是一个复杂的 AI 系统,例如多 Agent 协作、长时间运行的异步任务、或需要精准控制思考中断与恢复。
  • 集成需求多且杂,需要连接内部各种老旧系统,或使用非常小众的向量数据库、模型接口。
  • 需要对底层 Prompt、重试、Memory 等做深度定制,甚至改动框架默认行为。

结合业务场景做更细的取舍

光看框架特性不够,最终要落到"这个 Agent 具体负责什么"上。下面四类典型场景方便对号入座,每类对应不同复杂度和集成需求,选错框架要么大材小用,要么后期自己造轮子。

如下图所示。

20260314115312

四类场景与推荐方向的对应关系。

场景一:官网或产品里的智能客服、搜索建议

用户在一页里问几句,要即时、简洁的回复,必要时查文档或知识库。流程短、状态简单,不需要多 Agent 博弈或断点续跑,前端发一条消息、收一条(或流式)回复,至多一两轮工具调用。这类需求 Mastra 的轻量 API 和 TypeScript 体验很顺手,一个 Agent 配几个工具、在 Route Handler 里调 generate 就能上线;用 LangGraph 容易杀鸡用牛刀,要先建图、理解条件边和状态,对只想做一个会查文档的客服的团队来说性价比不高。

场景二:内部工具里的"多步任务助手"

例如用自然语言帮用户订会议室、填工单、查数据并生成报告。步骤多,有时要人工确认或回退(如"是否确认提交工单"),状态要在多轮请求间保持,甚至支持"离开页面再回来从断点继续"。这类需求用 LangGraph 的状态图和 checkpointer 更自然:节点对应步骤或人工介入,边上挂条件判断,状态持久化后刷新或重连都能恢复。用 Mastra 也能做,但分支和人工介入一多,就得自己维护"当前步骤、待确认项、历史结果",等于在业务层再造状态机,不如直接用图建模,让框架负责持久化。

场景三:多 Agent 协作(检索、生成、审核等分工)

多角色各司其职,之间有固定或动态调用关系,甚至要循环几轮才产出结果。这类编排是 LangGraph 的强项;Mastra 更适合"一个主 Agent 调若干工具"的链式交互,多 Agent 的路由和状态共享要自己写胶水代码。

场景四:向量库、模型、外部 API 集成种类多

公司内有自建向量库、多种大模型和第三方 API,希望用同一套抽象管住"检索、调用、解析"。LangChain 的集成生态在这里优势明显:Pinecone、Weaviate、Qdrant、自建 REST、各类 LLM 与 RAG 预制链和图,大多有现成包。Mastra 偏向精选常用组合,技术栈若较"非主流"(内网模型、私有协议、冷门向量库),可能要自己写适配层,把外部能力包成 Mastra 能识别的工具或模型接口。要权衡多写的适配代码是否被 Mastra 的 DX 和轻量部署抵消;若集成种类还会持续增加,直接上 LangChain 生态往往更省事。

常见误区与落地注意点

选型时容易踩的坑和落地前值得想清楚的几点,简单归纳如下。

不必纠结的两点。第一,没有"用了 Mastra 就不能用 LangChain"这回事,两者可共存,例如边缘或 BFF 用 Mastra 做轻量对话,后台用 LangGraph 做复杂管线,用 HTTP 或消息队列打通。第二,没有"LangGraph 一定比 Mastra 重"的绝对结论,重的是你要维护的图与状态逻辑;若你只需要一张简单 agent-tools 图,编译后运行时开销可接受,主要是上手成本高。

需要提前想清楚的两点。一是"先简单后复杂"时,若判断半年内会演进到多 Agent 或人机协同,可早点把复杂子流程用 LangGraph 建模,哪怕先只实现单 Agent,图结构也为后续加节点留好位置,避免以后在 Mastra 里手写状态机再迁一轮。二是"先复杂后简化"时,若团队普遍抱怨 LangChain 报错难查、概念太多,可把"单轮或短对话"抽成独立服务,用 Mastra 重写,接口不变、前端无感,逐步降维护成本。

最后,无论选哪边,都建议一开始就把"输入输出契约"(请求体格式、流式 SSE、错误码)定好,并用 TypeScript 类型或 OpenAPI 描述出来,以后换实现、做 A/B 或拆服务时,前端和网关都不必大动。

混合使用与迁移成本

不少团队会折中:简单、面向用户的 Agent 用 Mastra,部署在前端或边缘;复杂、长链路、多 Agent 的管线放后端,用 LangGraph 或 LangChain 实现,通过 API 暴露。这样既保住前端侧的开发体验和性能,又在需要复杂编排时用上 LangChain 生态。

若一开始选了 Mastra,后面业务演进到必须上状态图、多 Agent,可以只把"复杂子流程"迁到 LangGraph,用 HTTP 或消息队列和现有 Mastra Agent 对接,不必全盘重写。例如前端仍用 Mastra 做即时问答,把多步审批、长任务编排单独做成 LangGraph 服务,Mastra 在需要时调该服务 API。反过来,若一开始用 LangChain 搭了简单客服,发现维护成本高、报错难排查,可以把单轮或短对话抽成独立服务,用 Mastra 重写,逐步迁移。关键是想清楚边界(按功能、按请求路径、按团队 ownership 都行),按边界拆模块,而不是非此即彼。迁移时优先保证输入输出契约稳定(统一 JSON 请求体、流式 SSE 格式),前端或网关就不必大改。

总结与下一步

Mastra 和 LangChain(LangGraph)代表两种设计哲学:前者为前端和 TypeScript 优化,追求轻量和 DX;后者面向通用 AI 编排和复杂状态,追求生态和表达能力。没有谁一定更好,只看和你的业务场景、团队结构、集成与定制需求是否匹配。

一句话记住选型心法:Agent 是"应用里的辅助能力"、团队偏前端、要快上线,优先看 Mastra;Agent 是"业务核心"、有多 Agent、长任务、人机协同或大量冷门集成,优先看 LangGraph。两者也可组合,按模块边界拆,契约定好即可。

建议先明确两件事:当前要做的 Agent 主要负责什么(辅助功能还是核心 AI 系统),以及半年到一年内会不会出现多 Agent、长任务、复杂集成或深度定制。有了这两个问题的答案,再对照文中的对比表、决策说明和四类场景,选型会清晰很多。若你愿意说一下目前在规划的 Agent 具体负责什么业务、会接哪些系统,可以在此基础上再做一轮更细的技术栈评估和落地方案设计。文中的代码示例使用当前主流的 gpt-5.4,可直接复制后按需改模型名和 API Key 配置。

格力电器:公司暂时没有铝代铜技术的应用计划

36氪获悉,格力电器在互动平台表示,公司多年前已开展铝代铜相关技术研究,并持续关注行业动态与技术进展,在铝材的性能、质量和可靠性不能完全保证的情况下,公司暂时没有铝代铜技术的应用计划。

5 分钟入门 fetch

fetch API 用于发起 http 请求和处理响应。

fetchXMLHttpRequest 的现代替代方案。与使用回调函数的 XMLHttpRequest 不同,fetch 基于 Promise,并且与现代 Web 的一些特性(例如 Service WorkersCORS(跨源资源共享))进行了集成。

async function getData() {
  const url = "https://example.org/products.json";
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }

    const result = await response.json();
    console.log(result);
  } catch (error) {
    // 请求被 reject 时会进入 catch
    console.error(error.message);
  }
}

使用 response.ok 判断 fetch 请求是否成功特别重要,因为当请求返回 404 或 500 时,fetch 返回的 Promise 不会被 reject

fetch 只会在无法发出请求(网络问题、CORS 失败、中止等)时 reject。当 fetch 请求被 reject 时,会被 catch 捕获。

async function testFetch() {
  try {
    const response = await fetch("http://localhost:9999/api/test");
    console.log("响应状态:", response.status);

    if (!response.ok) {
      // 抛出错误,由外层的 catch 处理
      throw new Error(`请求失败,状态码:${response.status}`);
    }

    const data = await response.json();
    console.log("数据:", data);
  } catch (error) {
    // 请求被 reject 后会被 catch 捕获
    console.error("出错了:", error.message);
  }
}

testFetch();

1.png

设置 fetch 的请求方法

fetch 的配置对象中有个 method 属性,专门配置 fetch 的请求方法,比如发起 post 请求,只要把 method 属性设置为 POST 就可以了

const response = await fetch("https://example.org/post", {
  method: "POST",
  // …
});

设置 fetch 的请求体

fetch 配置对象中有个 body 属性,专门用于配置请求体,请求体的作用是向服务端发送数据的,比如 POSTPUT 请求的时候,向服务器传递有用的数据。

const response = await fetch("https://example.org/post", {
  method: "POST",
  body: JSON.stringify({ username: "example" }),
  // …
});

需要注意的是,GET 请求中是不能包含请求体的,如果 GET 请求需要向服务端发送数据的话,则可通过 URL 参数的方式传递。

同时要将传递给 body 配置的原生 JS 对象用 JSON.stringify 包裹,因为 HTTP 协议传输的数据本质上是文本或二进制流,不支持直接传递 JS 对象。否则 JS 对象会自动序列化为 [object Object] 字符串,从而导致数据传递错误:

2.png

设置请求头

fetch 的配置对象中有个 headers 属性,用于设置请求头。

请求头(Request headers) 用于向服务器提供关于请求的信息。例如,在 POST 请求 中,Content-Type 请求头会告诉服务器请求体的数据格式。

const response = await fetch("https://example.org/post", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ username: "example" }),
  // …
});

在 GET 请求中发送数据

GET 请求没有请求体,因此不能像 POST 请求那样通过 body 配置发送数据。GET 请求是通过 URL 查询参数发送数据的。

有三种设置 GET 请求的查询参数

  • 手动拼接 URL

  • 使用 URLSearchParams 构建查询参数

  • 使用 URL 对象操作

手动拼接 URL 示例

const userId = 123;
const page = 2;
fetch(`https://api.example.com/users?id=${userId}&page=${page}`)
  .then((response) => response.json())
  .then((data) => console.log(data));

使用 URLSearchParams 构建查询参数示例

const params = new URLSearchParams({
  id: 123,
  page: 2,
  name: "John Doe", // 特殊字符会被自动编码
});

fetch(`https://api.example.com/users?${params}`)
  .then((response) => response.json())
  .then((data) => console.log(data));

使用 URL 对象操作示例

const url = new URL("https://api.example.com/users");
url.searchParams.append("id", 123);
url.searchParams.append("page", 2);

fetch(url)
  .then((response) => response.json())
  .then((data) => console.log(data));

处理响应体

使用 fetch 发送完请求后,会得到服务器的响应对象 Response,然后数据会在 Response 对象的响应体中。要读取响应体,需要调用 Response 提供的读取方法。

Response 对象提供了 5 种读取响应体的方法:

  • response.json() ,读取 json 数据

  • response.text() ,读取文本数据

  • response.blob() ,读取二进制文件

  • response.arrayBuffer() ,读取二进制数据

  • response.formData() ,读取表单数据

以上 5 种方法都是异步的,并且都返回 Promise

const response = await fetch("/api/data");
const data = await response.json();
const response = await fetch("/api/text");
const text = await response.text();
const response = await fetch("/image.png");
const blob = await response.blob();
// response.arrayBuffer()通常用于底层二进制处理
const response = await fetch("/file.bin");
const buffer = await response.arrayBuffer();
const response = await fetch("/form");
const formData = await response.formData();

参考

Using the Fetch API

栗子前端技术周刊第 120 期 - Vite 8.0、Solid v2.0.0 Beta、TypeScript 6.0 RC...

🌰栗子前端技术周刊第 120 期 (2026.03.09 - 2026.03.15):浏览前端一周最新消息,学习国内外优秀文章,让我们保持对前端的好奇心。

📰 技术资讯

  1. Vite 8.0:Vite 8.0 正式发布,本次版本设计为平滑升级,底层有大量改动:@vitejs/plugin-react v6 不再需要 Babel(由 Oxc 替代)、Rolldown 替换了 Rollup 与 esbuild(Vite 团队由衷感谢 Rollup 与 esbuild 的维护者们,没有他们,Vite 不可能取得今天的成功)、支持 Wasm SSR、浏览器控制台日志转发到终端、性能大幅提升(构建速度提升 10~30 倍,同时保持完整的插件兼容性)。

  2. Solid v2.0.0 Beta:经过漫长的实验阶段,Solid 2.0 的首个测试版正式登场,带来了一流的异步支持:计算函数现在可以返回 Promise 或异步迭代对象。<Suspense> 已被废弃,初始渲染改用 <Loading> 组件;数据变更则迎来内置的 action() 功能,自带乐观更新支持。

  3. TypeScript 6.0 RC:TypeScript 6.0 候选版本(RC)已发布, v6.0 主要是为今年晚些时候推出的基于 Go 语言重构的原生 TypeScript 7.0 铺路,本次对 tsconfig.json 所做的所有必要调整,都能让你为未来版本做好准备。

  4. TC39 会议:在本周的 TC39 会议上,Temporal 提案正式进入 Stage 4 阶段。

📒 技术文章

  1. Seven Years to TypeScript: Migrating 11,000 Files at Patreon:用七年迁移到 TypeScript:Patreon 完成 11,000 个文件的改造 - 这个知名创作者平台曾拥有百万行 JavaScript 代码。尽管新项目接入 TypeScript 进展顺利,但全量改造旧代码仍是一项艰巨任务,这篇复盘文章介绍了其中用到的工具与技术方案。

  2. 用 Three.js 写了一个《我的世界》:本文作者分享用 Three.js 开发《我的世界》风格游戏项目的经历,该项目包含菜单、角色控制、相机等系统,有昼夜循环、挖掘放置等玩法。

  3. Agent Skill 是什么?:文中介绍了一个 Skill 有三层结构,分别是 Metadata、Instruction、Resources,三层各司其职,还提到设计 Skill 要注意粒度和描述精准。

🔧 开发工具

  1. RevoGrid:一款专为海量数据集打造的高性能数据表格控件,支持与 Vue、Angular、React、Svelte 集成,也可直接在原生 JavaScript 中使用
image-20260315083703805
  1. All SVG Icons:来自 200 多个精选图标库的 250,000+ 免费 SVG 图标,界面设计精美、使用简单,内容非常丰富。
image-20260315085901030
  1. Emittery 2.0:简洁、现代化的异步事件发射器。
image-20260315084313277

🚀🚀🚀 以上资讯文章选自常见周刊,如 JavaScript Weekly 等,周刊内容也会不断优化改进,希望你们能够喜欢。

💖 欢迎关注微信公众号:栗子前端

两市融资余额减少126.08亿元

36氪获悉,截至3月13日,上交所融资余额报13351.65亿元,较前一交易日减少42.59亿元;深交所融资余额报12896.67亿元,较前一交易日减少83.49亿元;两市合计26248.32亿元,较前一交易日减少126.08亿元。

兴业证券:家居板块目前处于磨底阶段,估值或进入修复周期

36氪获悉,兴业证券研报称,地产已深度调整,预计后续供需两端均有望出台相关政策进一步发力形成支撑。行业角度,家居板块目前处于磨底阶段,估值或进入修复周期,预计2026年家装总需求压力延续但降幅收敛,2027年下滑压力显著缓解,并保持平稳;此外,较高的股息率构筑充足的安全垫。

聚焦高景气绩优股,长线资金持仓线路图浮现

随着上市公司2025年年报的陆续披露,社保基金、QFI等长线资金的持仓情况随之浮出水面。从机构布局逻辑看,非常看重公司的业绩表现,行业景气度向上、基本面向好的公司获长线资金加码布局。(上证报)
❌