普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月24日首页

国投白银LOF:自2026年2月25日10:30复牌

2026年2月24日 17:15
2月24日,国投白银LOF公告,本基金将于2026年2月25日开市起至当日10:30停牌,自2026年2月25日10:30复牌,停牌期间本基金赎回业务照常办理。若本基金2026年2月25日二级市场交易价格溢价幅度未有效回落,本基金有权通过向深圳证券交易所申请盘中临时停牌、延长停牌时间等措施,向市场警示风险,具体以届时公告为准。(每经网)

宇树发布四足机器人Unitree As2

2026年2月24日 17:07
宇树科技在微博发文宣布,宇树发布四足机器人Unitree As2,具备90N.m峰值扭矩,空载续航超4小时,IP54防雨水,负载15kg,续航超13km,开放二次开发生态。(e公司)

春节期间独立开发者从 0 到 1:呼吸训练 iOS App 的工程化落地

作者 SameX
2026年2月24日 16:33

项目:呼吸视界(iOS 已上架)
技术栈:SwiftUI + SwiftData + StoreKit 2 + WidgetKit + ActivityKit

各位新年快乐,春节期间体验到了人挤人,车挤车,闲来无事撸了个一直想做的APP,分享点技术心得大家共勉。

1. 架构目标:把“训练体验”和“增长闭环”同时做出来

这个项目不是只做一个呼吸动画,而是做一条完整链路:

  • 训练引擎:稳定跑节奏(吸气/停顿/呼气)
  • 多感官反馈:视觉 + 音频 + 触觉一致
  • 习惯闭环:课程进度、训练记录、分享卡片
  • 增长入口:提醒、Widget、Live Activity、深链
  • 商业化:订阅、恢复购买、权益门控

核心分层:

  • 状态中枢:breathing-iOS/breathing/Domain/AppStore.swift
  • 页面编排:breathing-iOS/breathing/UI/RootView.swift
  • 能力引擎:breathing-iOS/breathing/Engines/*
  • 数据模型:breathing-iOS/breathing/Data/*
  • 外部触达:breathing-iOS/breathingWidget/* + BreathingLiveActivityManager

2. 单一状态中枢:AppStore 统一收口

AppStore@MainActor + ObservableObject 统一管理业务状态,避免“每个页面自己存一份状态”。

@MainActor
final class AppStore: ObservableObject {
    @Published var activeMode: BreathingMode
    @Published var activeDuration: Int
    @Published var isPro: Bool
    @Published var settings: AppSettings
    @Published var soundEnabled: Bool
    @Published var soundscapeId: String

    let breathingEngine: BreathingEngine
    let hapticsEngine: HapticsEngine
    private let soundscapePlayer = SoundscapePlayer()
    private let liveActivityManager = BreathingLiveActivityManager()
}

同时把订阅商品 ID 固定在内部,避免散落字符串:

private enum ProProductID {
    static let monthly = "com.xun.breathing.pro.monthly"
    static let yearly = "com.xun.breathing.pro.yearly"
    static let all = [monthly, yearly]
}

收益:UI 层只绑定状态,不再承担复杂业务判断;后续加模式/加权益不会牵一发而动全身。


3. 训练引擎:状态机 + 双 Task 保证节奏稳定

BreathingEngine 的关键是“阶段推进”和“总时长倒计时”分离:

@MainActor
final class BreathingEngine: ObservableObject {
    @Published private(set) var phase: BreathPhase = .ready
    @Published private(set) var isPlaying: Bool = false
    @Published private(set) var timeRemaining: Int

    private var cycleTask: Task<Void, Never>?
    private var countdownTask: Task<Void, Never>?
    private var sessionId = UUID()
}

启动时并行两条异步任务:

func start() {
    guard !isPlaying else { return }
    isPlaying = true
    sessionId = UUID()
    timeRemaining = duration

    runCountdown(sessionId: sessionId)
    switch courseType {
    case .standard:
        runBreathingLoop(sessionId: sessionId)
    case .wimHof(let config):
        runWimHofSession(sessionId: sessionId, config: config)
    }
}

倒计时任务只做一件事:

private func runCountdown(sessionId: UUID) {
    countdownTask?.cancel()
    countdownTask = Task { [weak self] in
        guard let self else { return }
        while !Task.isCancelled {
            try? await Task.sleep(nanoseconds: 1_000_000_000)
            guard sessionId == self.sessionId, self.isPlaying else { return }
            self.timeRemaining = max(0, self.timeRemaining - 1)
            if self.timeRemaining <= 0 {
                self.completeSession()
                return
            }
        }
    }
}

收益:暂停/恢复/切模式时行为稳定,不会出现“相位跳变”或“倒计时错乱”。


4. 音景引擎:缓存 + 淡入淡出,解决听感跳变

音频引擎里最关键是三点:

  • bufferCache:避免每次重新解码 mp3
  • fadeIn/fadeOut:切换音景不突兀
  • updatePlayback:统一播放入口(按 isPlaying/isEnabled 决策)
func updatePlayback(isPlaying: Bool, isEnabled: Bool, soundscapeId: String) {
    guard isEnabled, isPlaying else {
        stop()
        return
    }
    play(soundscapeId)
}

private func loadBuffer(for soundscape: Soundscape) -> AVAudioPCMBuffer? {
    if let cached = bufferCache[soundscape.id] { return cached }
    guard let url = Bundle.main.url(forResource: soundscape.fileName, withExtension: "mp3") else { return nil }
    do {
        let file = try AVAudioFile(forReading: url)
        let frameCount = AVAudioFrameCount(file.length)
        guard let buffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: frameCount) else { return nil }
        try file.read(into: buffer)
        bufferCache[soundscape.id] = buffer
        return buffer
    } catch {
        return nil
    }
}

停止时先淡出再停引擎:

fade(to: 0, duration: fadeOutDuration) { [weak self] in
    self?.stopNow(resetSession: resetSession)
}

5. 通知提醒:权限、频率、撤销一体化

提醒模块用 UNUserNotificationCenter,重点是“配置即覆盖”而不是“叠加创建”。

func configure(enabled: Bool, minutes: Int, frequency: ReminderFrequency) async -> Bool {
    if !enabled {
        cancel()
        return true
    }
    let allowed = await requestAuthorizationIfNeeded()
    guard allowed else {
        cancel()
        return false
    }
    schedule(minutes: minutes, frequency: frequency)
    return true
}

按周频次时生成固定 ID,方便后续精确取消:

let id = "\(weekdayPrefix)\(weekday)"
let request = UNNotificationRequest(identifier: id, content: content, trigger: trigger)
center.add(request, withCompletionHandler: nil)

6. Live Activity:状态去重,避免无效刷新

Live Activity 不是“每帧都更新”,而是先比对状态,只有变化才推送:

func update(state: BreathingLiveActivityAttributes.ContentState) {
    guard #available(iOS 16.1, *) else { return }
    guard let activity else { return }
    guard state != lastState else { return }
    lastState = state
    Task {
        let content = ActivityContent(state: state, staleDate: nil)
        await activity.update(content)
    }
}

收益:减少无意义更新,降低系统开销。


7. 数据闭环:训练记录 + 课程进度

7.1 会话记录模型(SwiftData)

@Model
final class SessionRecord {
    var id: UUID
    var timestamp: Date
    var modeId: String
    var courseId: String?
    var programId: String?
    var programDay: Int?
    var duration: Int
    var preCheckin: String?
    var postCheckin: String?
}

preCheckin/postCheckin 让“训练前后变化”可追踪,这是后续留存和转化分析的基础字段。

7.2 课程进度推进

static func nextDayIndex(program: BreathingProgram, record: ProgramProgressRecord?) -> Int? {
    let completed = Set(record?.completedDays ?? [])
    for index in program.plan.indices {
        if !completed.contains(index) {
            return index
        }
    }
    return nil
}

这个实现很朴素,但稳定,且便于后续做“断点继续”。


8. Widget 深链:缩短回流路径

Widget 直接绑定深链,用户从桌面可一跳进入训练:

private let quickURL = URL(string: "breathing://start?type=quick")!
private let emergencyURL = URL(string: "breathing://start?type=emergency")!

这比“打开 App -> 选模式 -> 开始”少至少 2 步,对高频场景(焦虑急救/会前调整)很关键。


9. 订阅链路:StoreKit 2 的最小闭环

关键流程:拉商品 -> 发起购买 -> 校验交易 -> finish -> 刷新权益。

func purchaseSelectedProduct() async {
    guard let product = selectedProduct else { return }
    let result = try await product.purchase()
    switch result {
    case .success(let verification):
        let transaction = try checkVerified(verification)
        await transaction.finish()
        await refreshSubscriptionStatus()
    case .pending, .userCancelled:
        break
    @unknown default:
        break
    }
}

恢复购买也单独兜底:

try await StoreKit.AppStore.sync()
await refreshSubscriptionStatus()

10. 工程复盘:最值得复用的 4 个点

  1. 状态收口AppStore 统一管理跨页面状态。
  2. 节奏分治:阶段循环和倒计时分为两条 Task。
  3. 增长内建:提醒/Widget/Live Activity 不是后补功能,而是留存系统。
  4. 数据先行:从第一天就保留训练前后字段,后续分析成本最低。

后记:APP已经上架,某书上反响还不错,赚钱是次要的,主要产品有人用,技术有积累就很开心啦。 体验链接:apps.apple.com/cn/app/%E5%… PS:要兑换码好说,哈哈~

7e175ace-ca50-4f8f-8f0f-fbc0a82eecd7.jpg

cbebea05-dd13-4128-bf5e-06d7dce991d5.jpg

恒指收跌1.82%,恒生科技指数跌2.13%

2026年2月24日 16:14
36氪获悉,恒指收跌1.82%,恒生科技指数跌2.13%;石油及天然气、咨询科技器材、半导体板块领涨,域高国际控股涨15.68%,普天通信集团涨46.07%,兆易创新涨11.91%;软件服务、保险、药品及生物科技板块跌幅居前,中国信息科技跌25.53%,亚洲金融跌0.21%,MIRXES-B跌21.67%。

Anthropic员工股份出售预计将达到60亿美元

2026年2月24日 16:00
据一位知情人士透露, Anthropic公司周一启动了一项股票出售计划,面向在该公司工作至少12个月的现任和前任员工。该知情人士还表示,此次股票出售基于公司最新的3800亿美元估值。 据报道,现任和前任员工将有机会出售总额高达60亿美元的股票。知情人士透露,最终规模取决于参与出售的员工人数以及他们各自计划出售的股票数量。 本月初,Anthropic宣布已完成300亿美元的融资,由新加坡GIC公司和Coatue领投。本轮融资的其他共同领投方包括DE Shaw Ventures、Dragoneer、Founders Fund、Iconiq和MGX。(新浪财经)

周大福3月又要涨价,部分门店已收到相关通知

2026年2月24日 15:47
近日,市场传出周大福春节后将对黄金产品调价的消息,此次调价或于3月中旬正式启动,目前部分门店已收到相关通知,涨价重点集中在一口价产品,预计涨幅或为15%-30%,但具体调价细节及执行时间仍以到店价格标签调整为准。2月24日,北京京西大悦城周大福门店销售确认了涨价消息,该销售表示:“涨价时间在3月10日左右。一条克重在29.88克的五帝钱金手链涨价前53800元,即将涨价到71800元,目前是一货难求。”据此计算,该条手链涨幅超33%,涨价后克均价已超2400元。(界面)

多款eVTOL航空器、人形机器人亮相武汉

2026年2月24日 15:45
湖北自主研制的多款eVTOL(电动垂直起降飞行器)航空器以及人形机器人2月24日在“全省加快建成中部地区崛起的重要战略支点推进大会”上亮相,集中呈现湖北低空经济、人工智能等产业的最新研发成果。 (新华社)

开工第一天,别让AI写的代码触发3.2f封号。

作者 iOS研究院
2026年2月24日 14:17

背景

今天是农历正月初八,春节后的第一个工作日。后台有粉丝留言,迎来的开年的第一记重磅打击3.2f待终止通知。

踩线原因也是老生常谈了,严查分类之隐藏功能问题

中英对照.png

老iOSer对于这种情况已经是见怪不怪了,很多时候并非开发者想做某些Sao操作,实属无奈的多。毕竟,有业务苹果不能正面允许,不得已就采用这种上有政策下有对策的打法

原因分析

通过进一步沟通,层层抽丝剥茧。终于定位到踩到隐藏功能的导火索,在AI加持的情况下使用了非公开的API获取业务层面需要的功能权限。从业务的角度来看功能确实实现了,从苹果监管的角度来看调用了越权的API属性。通过键值对的方式Hook数据结果。

实话讲AI背大锅,对于很多跨行的开发者来说,为了满足公司的开发需求保住饭碗使用AI的方式本身没有问题。关键的问题在于,无法Review AI所编写的代码是否合规

所以,AI本质是一把双刃剑,在提高开发效率的同时,也需要额外考虑风控问题。

隐藏功能

隐藏功能的前身是苹果开发者指南中的-2.3.1条款。

主要意在通过一些动态下发的方式,直接或间接干预苹果审核所看到的内容。将符合苹果审核的内容作为A面,顺利通过审核,提高审核通过率。【俗称的AB面,也叫马甲包】

随着AppStore审核规则的加强,对于隐藏功能的判定不仅仅只是单纯的功能切换,而是上升到更为全面的元数据以及概念层面。

简单来说:

少做不做挂羊头卖狗肉的事情,苹果的算法比开发者想象中更加强大

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# 如何主动提防苹果3.2f的进攻,自查防御手册(代码篇)

# 如何主动提防苹果3.2f的进攻,自查防御手册(ASO篇)

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

从 Recoil 的兴衰看前端状态管理的技术选型

作者 evle
2026年2月24日 16:13

从 Recoil 的兴衰看前端状态管理的技术选型

2023 年底,Meta 官方宣布 Recoil 进入“维护模式”,从它的兴衰历程中,我们看到了什么?

Recoil 的发展历程

2020 年:Recoil 横空出世

2020 年的一个下午,我正在 Twitter 上刷着动态,突然刷到一条让我想打开电脑蠢蠢欲动的动态 "Facebook 发布了 Recoil,一个实验性的状态管理库",状态管理在 React 生态里面一直是个工程负担,无论是团队角度从复杂工程的状态管理意识培养还是从开发者体验(DX)角度来看,React 状态管理一直像在等一个 “救世主”。

"Recoil 是一个实验性的状态管理库,为 React 提供了更好的状态管理体验。"

上面这是 Recoil 的宣传语,如果是2020年在写前端的同学肯定很熟悉当时的背景

当时的背景

  • Redux 虽然强大,但样板代码太多
  • Context API 性能问题明显,所有消费者都会重渲染
  • 社区渴望一个更简单、更现代的状态管理方案

Recoil 的核心优势

  1. 解决了 Context 的性能问题
    我只是想切换主题,为什么用户信息组件也要重渲染?应用越来越慢,每次状态变化都要重渲染几十个组件让我不得不拆分出非常多的 Context,有没有一种更简单的方式只订阅需要的状态。

  2. 比 Redux 更简单,减少了样板代码
    我只是想写一个计数器,为什么要写三个文件?action、reducer、dispatch...这些概念为什么这么抽象?这将层层传递(漏传) Props 的痛苦转变为一种新的痛苦。 有没有简单的状态管理方案?

  3. 原子化的状态管理理念
    工作台应用的复杂度完全取决于状态管理的复杂度,实际的业务逻辑并没有什么复杂的,反而我们在“技术”上花费大量时间建立开发者信心,这种 ROI 是经不起推敲的。 比如更新一个用户信息,我们 dispatch 一个 UPDATE_USER 类型的参数, 这时候心智负担是 A组件会重新渲染吗? B组件不应该渲染,但是渲染了。 C组件我也不知道是否会重新渲染。

  4. 官方背书
    既然是官方出的,那开发者生态自然会觉得这个方案非常可靠, 并且 TypeScript 支持优秀,我们下意识里面觉得是时候改变我们项目了。

2021-2022 年:快速成长期

Recoil 被广泛采用,成为 React 状态管理的热门选择之一。当时的成功案例和教程大量的涌现在互联网上 “Meta 内部项目开始使用”,"某知名开源项目也集成了 Recoil",生态的发展可是如火如荼,出现了 Recoil DevTools,与 React Router、React Query 等库集成良好。

在当时我们团队内部,我也开始推广 AtomSelector API 的使用,并对负责的项目进行状态管理改造。当时我比喻 Atom 就是一个保险柜:

  • 任何需要用钱的人都可以打开它
  • 任何人都可以往里面放钱或取钱
  • 保险箱里的钱变了,所有知道这个保险箱的人都会收到通知

假设我存了 100块到保险柜里

import { atom } from 'recoil';

// 创建一个 Atom,就像创建一个保险箱
const moneyState = atom({
  key: 'moneyState',      // 保险箱的名字(必须唯一)
  default: 100,           // 保险箱里的初始钱数
});

那么其他组件想交互我这个保险柜的状态像喝水一样简单

import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';

// 在组件中使用
function Wallet() {
  // 1. 读取和写入(既能看钱,也能存钱取钱)
  const [money, setMoney] = useRecoilState(moneyState);
  
  // 2. 只读取(只能看钱,不能改)
  const money = useRecoilValue(moneyState);
  
  // 3. 只写入(只能改钱,不能看)
  const setMoney = useSetRecoilState(moneyState);
  
  return (
    <div>
      <p>我的钱:{money}元</p>
      <button onClick={() => setMoney(money + 10)}>存10元</button>
      <button onClick={() => setMoney(money - 10)}>取10元</button>
    </div>
  );
}

当然我们也可以使用与保险柜配套的智能计算器 selector 来与保险柜交互,他会自动管理依赖关系,举个例子,今天大A跌了,我们取出来的钱要 x0.8, 我们如果使用普通函数实现的话,组件渲染每次都会新建这个函数,每次都会重新计算,这会损耗性能。如果使用原生useCallback API 的话引入了手动管理依赖的心智负担。

import { selector } from 'recoil';

const doubleMoneyState = selector({
  key: 'newMoneyState',  // 计算器的名字
  get: ({ get }) => {          // 计算逻辑
    const money = get(moneyState);  // 从保险箱里读取钱数
    return money * 0.8;               // 跌了!
  },
});

那重复计算场景,根据多个状态计算场景,过滤或者排序状态场景都可以轻松通过 selector 解决了。但在真实的实际使用过程中,发现 Recoil 的学习曲线并不是那么平滑,虽然 atom 和 selector 可以让新人2天内上手,但是 atomFamily、selectorFamily 的使用负担,selector越写越复杂导致的性能问题,以及没有彻底改造异步状态为 waitForAll,而是通过 Promise.all 的 JS API 来组合管理异步状态, 都让我感觉这次改造不如“预期” 。

Recoil 一直标记为 “试验性” ,刚开始我可能只是觉得 新项目嘛,API 可能随时变更,高速迭代,一直朝着最好的方向发展,但是渐渐的发现。企业级应用很难陪跑试验性的项目,没有人愿意为技术的升级买单。

2023 年:宣布进入维护模式

Recoil 团队宣布不再积极开发新功能,进入维护模式。

官方声明

"Recoil 已经进入维护模式,我们将继续修复关键 bug,但不会开发新功能。我们建议用户考虑其他状态管理方案。"

从官方公开资料与社区的状态来看,我觉得 Recoil 的衰落有3个原因:

  1. 资源有限

    • Meta 团队资源有限,无法同时维护多个状态管理方案
    • 优先级调整,资源分配到其他更重要项目(AI时代的趋势)
    • 维护一个"实验性"库的成本效益比不高
  2. 竞争激烈

    • Zustand、Jotai(Recoil原作者) 等新方案更轻量、更简单
    • 这些方案提供了类似的功能,但学习曲线更平缓
    • 社区开始转向更活跃的方案
  3. 需求变化

    • 前端技术栈快速演进,Recoil 的设计可能不再是最优解
    • 新的范式(如 Signals)开始兴起
    • 社区对状态管理的需求发生了变化

我从开始的震惊,转变为理解,也在社区开始讨论迁移方案,也在翻阅大量迁移指南和对比实践。

这次技术选型的经验

不要盲目追求"官方"方案

其实很多项目选择 Recoil 的一个重要原因是:它是 Meta 官方方案

  • "Recoil 是 Facebook 官方的,肯定可靠"
  • "Meta 的技术团队很厉害,他们的方案一定最好"
  • "官方方案 = 最佳方案"

但是回过头我们才发现

  • 官方 ≠ 最适合我们的项目
  • 官方方案也可能被放弃
  • 官方方案的学习曲线可能更陡峭
  • 官方方案的更新频率可能不如社区方案

Recoil 在后期面临的一个重要问题是:社区反馈响应不够及时。 提交了 Issue,但几周都没有回复,新功能的 Roadmap 始终没有看到,可持续性不够。

虽然切换到 Recoil 产生的技术收益不如预期,并且完全掉到另一个坑里(停止维护),但是团队通过这次迁移,强化和实践了 Recoil 可复制的理念。 比如每个状态都有自己的 R&R, 避免不必要的渲染。即使在后面迁移到 Zustand 我们的状态关系依然没有大的变化,状态之间的依赖关系清晰,并且可以独立测试每个状态。以及将一些老代码的 Context 也做了原子化拆分。

状态管理的技术趋势

3.1 简约主义:少即是多

当前最明显的趋势是追求简约。Zustand 的下载量超过了 Recoil 和 Jotai 的总和,大小却只有几KB,这正是与社区开发者共情 “这么简单的东西,我要写那么复杂吗?”,2018 年 统治者地位的 Redux 写个计数器demo 4个文件几十行代码,现如今。Zustand 3行代码,1个文件,开发1天内就能上手 API,更少的代码意味着更少的出错机会,团队反馈在 PeerReveiw 时的信心也增加了许多。Zustand 它让 80% 的场景变得简单,同时让 20% 的复杂场景仍然可行。

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

function Counter() {
  const { count, increment } = useStore();
  return <button onClick={increment}>{count}</button>;
}

3.2 按需选择

社区不再追求"大一统"解决方案,就像工具箱里有锤子、螺丝刀、扳手,我们不会用锤子去拧螺丝。而是根据场景选择:

场景 推荐方案 理由
小型应用 React Context + Hooks 无需额外依赖
中型应用 Zustand 简单高效
大型企业应用 Redux Toolkit 生态成熟,工具完善
复杂状态依赖 Jotai 原子化设计
响应式需求 MobX / Valtio 自动追踪依赖

3.3 渐进式平滑迁移

在实际开发中,我听过很多“XXX技术好,我们要拥抱新技术!”,除去迁移风险,开发交付的核心是业务价值,重构过去稳定的模块在业务上也许完全没有收益, 就算以前写的很垃圾,经过这么多涂涂改改它也很稳定。 那我们的技术洁癖应该是渐进式迁移, 我们引入新方案与旧方案并存,新的业务享受着新技术的便利性,老的方案享受着无变更的稳定性。

那渐进式迁移的前提是什么? 迁移复杂度评估。目前主流的前端状态管理库似乎都意识到了这些工程难题:

  • 概念差异越大,迁移成本越高。
  • 代码结构差异越大,重构工作量越大。
  • 依赖的中间件、工具越多,迁移越复杂。

从而像充电器一样在做类似的“标准”。我们从 Recoil 迁移到 Zustand 的复杂度很低,因为两者的概念类似。

后话

技术会过时,但理念是:永恒的。

贵州茅台在毕节成立销售公司,注册资本3000万

2026年2月24日 15:30
据企业信息查询平台消息,2月13日,毕节茅台酒销售有限公司成立,法定代表人为李强清,注册资本3000万人民币,经营范围包括酒类经营、食品销售、道路货物运输等,由贵州茅台(600519)旗下贵州茅台酒销售有限公司全资持股。(界面)

今年春节假期新能源汽车出行创新高

2026年2月24日 15:15
据国家能源局公众号消息,通过对纳入国家充电设施监测服务平台的5.33万台高速公路充电桩进行统计分析,自腊月二十八(2月15日)至正月初七(2月23日),高速公路电动汽车充电次数共计602.10万次,充电量达到14976.75万千瓦时,日均充电量1664.08万千瓦时,较去年春节日均增长52.01%,创历史新高。(每经网)

A股三大指数集体收涨,MLCC、玻纤、石油石化等概念走强

2026年2月24日 15:03
36氪获悉,A股三大指数集体收涨,沪指涨0.87%,深成指涨1.36%,创业板指涨0.99%;贵金属、石油石化、玻璃玻纤板块领涨,晓程科技涨15.19%,科力股份涨26.03%,国际复材涨19398%;影视院线、传媒、旅游及景区板块跌幅居前,光线传媒跌19.99%,幸福蓝海跌18.99%,长白山跌5.40%。

小米申请小米智能存储商标

2026年2月24日 14:42
据企业信息查询平台消息,近期,小米科技有限责任公司申请注册多枚“小米智能存储”商标,国际分类包括科学仪器、通讯服务、网站服务等,当前商标状态均为等待实质审查。小米科技有限责任公司成立于2010年3月,法定代表人为雷军,注册资本18.5亿元人民币,经营范围含技术进出口、通讯设备销售、日用杂品销售、日用百货销售、日用家电零售、家具销售等,由雷军、黎万强等共同持股。(每经网)

Vue 3 从基础到组合式 API 全解析

作者 赵_叶紫
2026年2月24日 14:40

目录


1.1 基础概念

MVVM 模式

MVVM 由三部分组成:Model(模型)、View(视图)、ViewModel(视图模型)。

graph LR
    subgraph View_Layer["🖥️ View 层"]
        direction TB
        V1["📄 HTML 模板 + CSS 样式"]
        V3["👆 用户交互事件<br/>click / input / submit"]
    end

    subgraph ViewModel_Layer["⚙️ ViewModel 层"]
        direction TB
        VM1["📦 响应式数据<br/>data / ref / reactive"]
        VM2["🔄 计算属性<br/>computed"]
        VM3["👁️ 侦听器<br/>watch"]
        VM4["🔗 生命周期钩子<br/>mounted / updated"]
        VM5["🛠️ 方法<br/>methods"]
    end

    subgraph Model_Layer["🗄️ Model 层"]
        direction TB
        M1["🌐 API 请求<br/>axios / fetch"]
        M3["💡 业务逻辑<br/>数据处理 / 校验 / 数据模型"]
        M4["🏪 状态管理<br/>Vuex / Pinia"]
    end

    %% View → ViewModel
    V3 -- "① 用户操作触发" --> VM5
    V1 -- "② v-model 双向绑定" --> VM1

    %% ViewModel 内部依赖
    VM1 -- "③-a 依赖数据变化<br/>触发重新计算" --> VM2
    VM1 -- "③-b 依赖数据变化<br/>触发侦听器" --> VM3
    VM4 -- "③-c 生命周期触发<br/>初始化加载等" --> VM5

    %% ViewModel → View
    VM1 -- "④ 数据驱动视图更新" --> V1
    VM2 -- "⑤ 计算结果渲染到模板" --> V1

    %% ViewModel → Model
    VM5 -- "⑥ 调用 API" --> M1
    VM3 -- "⑦ 监听变化触发业务逻辑" --> M3

    %% Model → ViewModel
    M1 -- "⑧ 返回数据 → 写入响应式变量" --> VM1
    M4 -- "⑨ 状态变更通知 → 写入响应式变量" --> VM1

    %% 自动触发渲染
    M1 -. "⑧→④ 自动触发视图渲染 🔄" .-> V1
    M4 -. "⑨→④ 自动触发视图渲染 🔄" .-> V1

    style View_Layer fill:#E3F2FD,stroke:#1565C0,stroke-width:3px
    style ViewModel_Layer fill:#FFF3E0,stroke:#E65100,stroke-width:3px
    style Model_Layer fill:#E8F5E9,stroke:#2E7D32,stroke-width:3px

    style V1 fill:#BBDEFB,stroke:#1565C0,stroke-width:2px
    style V3 fill:#BBDEFB,stroke:#1565C0,stroke-width:2px
    style VM1 fill:#FFE0B2,stroke:#E65100,stroke-width:2px
    style VM2 fill:#FFE0B2,stroke:#E65100,stroke-width:2px
    style VM3 fill:#FFE0B2,stroke:#E65100,stroke-width:2px
    style VM4 fill:#FFE0B2,stroke:#E65100,stroke-width:2px
    style VM5 fill:#FFE0B2,stroke:#E65100,stroke-width:2px
    style M1 fill:#C8E6C9,stroke:#2E7D32,stroke-width:2px
    style M3 fill:#C8E6C9,stroke:#2E7D32,stroke-width:2px
    style M4 fill:#C8E6C9,stroke:#2E7D32,stroke-width:2px

    linkStyle 0 stroke:#D32F2F,stroke-width:2.5px
    linkStyle 1 stroke:#D32F2F,stroke-width:2.5px
    linkStyle 2 stroke:#FF6F00,stroke-width:2.5px
    linkStyle 3 stroke:#FF6F00,stroke-width:2.5px
    linkStyle 4 stroke:#FF6F00,stroke-width:2.5px
    linkStyle 5 stroke:#1565C0,stroke-width:2.5px
    linkStyle 6 stroke:#1565C0,stroke-width:2.5px
    linkStyle 7 stroke:#FF6F00,stroke-width:2.5px
    linkStyle 8 stroke:#FF6F00,stroke-width:2.5px
    linkStyle 9 stroke:#2E7D32,stroke-width:2.5px
    linkStyle 10 stroke:#2E7D32,stroke-width:2.5px
    linkStyle 11 stroke:#9C27B0,stroke-width:2px,stroke-dasharray:5
    linkStyle 12 stroke:#9C27B0,stroke-width:2px,stroke-dasharray:5

三层职责与 Vue 中的映射:

缩写 全称 职责 Vue 中的对应
M Model(模型) 应用数据与业务逻辑。不关心数据如何展示,只负责存储和处理 reactive()/ref() 声明的响应式状态、API 请求返回的数据、Pinia store
V View(视图) 用户看到的界面。不包含业务逻辑,只负责声明式地描述 UI 结构 <template> 中的 HTML 模板、最终渲染出的真实 DOM
VM ViewModel(视图模型) M 与 V 之间的桥梁。监听 Model 变化自动更新 View,捕获 View 上的用户交互反向更新 Model Vue 组件实例本身——编译器将模板转为渲染函数,响应式系统追踪依赖并触发更新

流程总结:

整个 MVVM 的运转可以概括为一个闭环:

  1. 用户交互驱动(V → VM): 用户在 View 层触发事件(click、input 等),通过 v-model 或事件绑定将操作传递给 ViewModel 层的方法或响应式变量。
  2. ViewModel 内部联动(VM 内部): 响应式数据变化后,computed 自动重新计算派生值,watch 触发副作用逻辑,生命周期钩子在适当时机执行。
  3. 业务处理(VM → M): ViewModel 调用 Model 层的 API 请求或业务逻辑函数,完成数据的增删改查。
  4. 数据回写(M → VM): Model 层返回结果写入响应式变量,或 Pinia/Vuex 状态变更通知 ViewModel。
  5. 视图自动更新(VM → V): 响应式系统检测到数据变化,自动触发虚拟 DOM diff,最小化更新真实 DOM,用户看到最新界面。

核心价值: 开发者只需关注数据(M)模板(V),中间的同步、diff、DOM 操作全部由 Vue 的 ViewModel 层自动完成。v-model:modelValue + @update:modelValue 的编译时语法糖,本质仍由 VM 层协调,Vue 3 整体是单向数据流 + 双向绑定语法糖的设计。


1.2 项目创建(Vite)

基于原生 ES Module,毫秒级冷启动,HMR 不随项目规模变慢。

# 推荐使用 create-vue(Vue 官方脚手架,底层基于 Vite)
npm create vue@latest

典型项目结构:

my-project/
├── public/                  # 静态资源(不经过构建)
├── src/
│   ├── assets/              # 需构建处理的资源(图片、样式)
│   ├── components/          # 通用组件
│   ├── composables/         # 组合式函数
│   ├── router/              # 路由配置
│   ├── stores/              # Pinia 状态管理
│   ├── views/               # 页面级组件
│   ├── App.vue
│   └── main.ts
├── index.html               # 入口 HTML(Vite 以此为入口)
├── vite.config.ts
├── tsconfig.json
└── package.json

1.3 模板语法

插值与绑定

<template>
  <!-- 文本插值 -->
  <span>{{ message }}</span>

  <!-- 属性绑定(v-bind 简写 :) -->
  <img :src="imgUrl" :alt="title" />

  <!-- 动态绑定多个属性 -->
  <div v-bind="attrs"></div>
  <!-- 等价于 <div :id="attrs.id" :class="attrs.class" /> -->

  <!-- 事件绑定(v-on 简写 @) -->
  <button @click="submit">提交</button>
  <input @keyup.enter="search" />         <!-- 按键修饰符 -->
  <form @submit.prevent="save" />          <!-- 阻止默认行为 -->
</template>

条件渲染

<template>
  <!-- v-if:条件为 false 时 DOM 不存在(适合不频繁切换) -->
  <div v-if="status === 'loading'">加载中</div>
  <div v-else-if="status === 'error'">出错了</div>
  <div v-else>{{ data }}</div>

  <!-- v-show:始终渲染 DOM,通过 display 切换(适合频繁切换) -->
  <div v-show="visible">我一直在 DOM 中</div>
</template>
指令 DOM 行为 初始开销 切换开销 适用场景
v-if 销毁/重建 低(不渲染) 条件很少变化
v-show display: none 高(始终渲染) 频繁切换显示

列表渲染

<template>
  <!-- 数组遍历 -->
  <li v-for="(item, index) in list" :key="item.id">
    {{ index }}. {{ item.name }}
  </li>

  <!-- 对象遍历 -->
  <div v-for="(value, key) in obj" :key="key">
    {{ key }}: {{ value }}
  </div>

  <!-- v-for + v-if 不能同级使用,需用 <template> 包裹 -->
  <template v-for="item in list" :key="item.id">
    <li v-if="item.active">{{ item.name }}</li>
  </template>
</template>

key 的作用: 帮助 Vue 的 diff 算法识别节点身份,复用和重排已有元素而非重新创建。务必使用唯一业务 ID,避免用 index(排序/删除时会导致错误复用)。


2. 组件开发

组件是 Vue 的核心抽象单元——将 UI 拆分为独立、可复用的模块,每个组件封装自己的模板、逻辑和样式,通过明确的接口(props/emits)进行通信。

2.1 组件基础

组件定义与注册

Vue 3 推荐使用单文件组件(SFC) + <script setup> 语法,编译器自动处理注册,无需手动声明。

<!-- MyButton.vue — 单文件组件 -->
<template>
  <button :class="type" @click="emit('click', $event)">
    <slot />
  </button>
</template>

<script setup lang="ts">
defineProps<{ type?: 'primary' | 'default' }>()
const emit = defineEmits<{ click: [e: MouseEvent] }>()
</script>

<style scoped>
.primary { background: #409eff; color: #fff; }
</style>

使用方式:<script setup> 中导入即可直接在模板使用,无需注册。

<template>
  <MyButton type="primary" @click="save">保存</MyButton>
</template>

<script setup lang="ts">
import MyButton from './MyButton.vue'
</script>

SFC 的价值: 一个 .vue 文件 = 模板 + 逻辑 + 样式,scoped 实现样式隔离,<script setup> 减少样板代码,编译器自动优化。


2.2 组件通信

Vue 组件间通信方式按场景选择,核心原则:props 向下,events 向上,跨层用 provide/inject

Props(父 → 子)

父组件通过属性向子组件传递数据,子组件只读不可修改。

<!-- Child.vue -->
<template>
  <h2>{{ title }} ({{ count }})</h2>
</template>

<script setup lang="ts">
const props = withDefaults(defineProps<{
  title: string
  count?: number
}>(), {
  count: 0   // 类型声明的 props 通过 withDefaults 设置默认值
})
</script>
<!-- Parent.vue -->
<Child title="订单" :count="orderCount" />

Emits(子 → 父)

子组件通过事件通知父组件,保持单向数据流。

<!-- Child.vue -->
<template>
  <button @click="remove(item.id)">删除</button>
</template>

<script setup lang="ts">
const emit = defineEmits<{
  update: [value: string]
  delete: [id: number]
}>()

function remove(id: number) {
  emit('delete', id)   // 触发事件,父组件通过 @delete 监听
}
</script>
<!-- Parent.vue -->
<Child @update="handleUpdate" @delete="handleDelete" />

v-model(双向绑定语法糖)

v-model 本质是 :modelValue + @update:modelValue 的简写,支持多个 v-model。

<!-- SearchInput.vue -->
<template>
  <input v-model="keyword" />
  <select v-model="status">
    <option value="all">全部</option>
    <option value="active">启用</option>
  </select>
</template>

<script setup lang="ts">
const keyword = defineModel<string>()           // 默认 v-model
const status = defineModel<string>('status')    // v-model:status
</script>
<!-- Parent.vue — 语法糖写法 -->
<SearchInput v-model="keyword" v-model:status="currentStatus" />

上面的 v-model:status 等价于展开写法:

<!-- Parent.vue — 展开写法(与上方完全等价) -->
<SearchInput
  v-model="keyword"
  :status="currentStatus"
  @update:status="currentStatus = $event"
/>

v-model:status 编译后就是 :status + @update:statusdefineModel('status') 内部帮你处理了 props 接收和 emit 触发。

Provide / Inject(跨层级)

祖先组件提供数据,任意后代组件注入,避免 props 逐层透传。

<!-- 祖先组件 -->
<script setup lang="ts">
import { provide, ref } from 'vue'

const theme = ref<'light' | 'dark'>('light')
provide('theme', theme)       // key-value 形式提供
</script>
<!-- 任意深度的后代组件 -->
<template>
  <div :class="theme">当前主题:{{ theme }}</div>
</template>

<script setup lang="ts">
import { inject } from 'vue'
import type { Ref } from 'vue'

const theme = inject<Ref<'light' | 'dark'>>('theme')  // 注入
</script>

使用 InjectionKey 实现类型安全(推荐):

字符串 key 容易拼错且无类型推导,推荐抽取 InjectionKey 常量:

// keys.ts — 统一管理 injection key
import type { InjectionKey, Ref } from 'vue'

export const ThemeKey: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme')
// 祖先组件中 provide
import { ThemeKey } from './keys'
provide(ThemeKey, theme)      // TS 自动校验 value 类型

// 后代组件中 inject
import { ThemeKey } from './keys'
const theme = inject(ThemeKey) // 自动推导为 Ref<'light' | 'dark'> | undefined

适用场景: 主题、国际化、全局配置等需要跨多层传递但不适合放 Pinia 的数据。

Ref(父访问子实例)

通过模板 ref 获取子组件实例,直接调用子组件暴露的方法或属性。

defineExpose 的作用:<script setup> 中,组件内部的变量和方法默认对外不可见(与 Options API 不同)。必须通过 defineExpose 显式声明哪些内容允许父组件通过 ref 访问。未暴露的内容,父组件拿到 ref 后也无法调用。

<!-- Child.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
const secret = ref('隐藏数据')     // 未暴露,父组件无法访问
function reset() { count.value = 0 }

// 只有 count 和 reset 对外可见,secret 外部不可访问
defineExpose({ count, reset })
</script>
<!-- Parent.vue -->
<template>
  <Child ref="childRef" />
  <button @click="resetChild">重置子组件</button>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref<InstanceType<typeof Child>>()

function resetChild() {
  childRef.value?.reset()   // 调用子组件通过 defineExpose 暴露的 reset 方法
}
</script>

注意: 模板 ref 访问破坏了组件封装性,仅在表单校验、弹窗控制等必要场景使用。


2.3 插槽

插槽让父组件向子组件内部注入模板片段,实现布局和内容的解耦。

默认插槽

子组件用 <slot /> 占位,父组件传入内容替换。

<!-- Card.vue -->
<template>
  <div class="card">
    <slot />   <!-- 父组件传入的内容渲染在这里 -->
  </div>
</template>
<Card>
  <p>这段内容会替换 slot 占位</p>
</Card>

具名插槽

多个插槽通过 name 区分,父组件用 v-slot:name(简写 #name)指定。

<!-- Layout.vue -->
<template>
  <header><slot name="header" /></header>
  <main><slot /></main>
  <footer><slot name="footer" /></footer>
</template>
<Layout>
  <template #header>
    <h1>页面标题</h1>
  </template>

  <p>默认插槽内容(main 区域)</p>

  <template #footer>
    <span>© 2026</span>
  </template>
</Layout>

作用域插槽

子组件通过 slot 向父组件回传数据,父组件拿到数据后自定义渲染。

<!-- DataList.vue -->
<script setup lang="ts">
defineProps<{ items: { id: number; name: string }[] }>()
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="item.id" />  <!-- 回传数据 -->
    </li>
  </ul>
</template>
<!-- 父组件自定义每一行的渲染方式 -->
<DataList :items="list">
  <template #default="{ item, index }">
    <span>{{ index }}. {{ item.name }}</span>
    <button @click="remove(item.id)">删除</button>
  </template>
</DataList>

作用域插槽的价值: 子组件负责数据遍历和逻辑,父组件负责 UI 呈现,实现逻辑与视图的分离。常见于表格列自定义、列表项渲染等场景。


2.4 动态组件

<component :is>

根据变量动态切换渲染的组件,适用于 Tab 切换、多表单步骤等场景。

<template>
  <button v-for="tab in tabs" :key="tab.label" @click="current = tab.comp">
    {{ tab.label }}
  </button>
  <component :is="current" />
</template>

<script setup lang="ts">
import { shallowRef } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'
import TabC from './TabC.vue'

const tabs = [
  { label: '基本信息', comp: TabA },
  { label: '详细配置', comp: TabB },
  { label: '操作日志', comp: TabC },
]
const current = shallowRef(tabs[0].comp)  // shallowRef 避免深度代理组件对象
</script>

<keep-alive>

缓存被切走的组件实例,切回时保留状态(表单输入、滚动位置等),避免重新创建和销毁。

<keep-alive :include="['TabA', 'TabB']" :max="5">
  <component :is="current" />
</keep-alive>
属性 说明
include 只缓存匹配的组件(名称或正则)
exclude 排除不缓存的组件
max 最大缓存实例数,超出时销毁最久未使用的(LRU)

keep-alive 缓存的组件可使用两个专属生命周期:

<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 组件从缓存中被激活(切回)时触发,可用于刷新数据
})

onDeactivated(() => {
  // 组件被缓存(切走)时触发,可用于清理定时器
})
</script>

典型场景: 后台管理的多 Tab 页面切换——用户在 Tab A 填了一半表单,切到 Tab B 再切回来,数据不丢失。

defineAsyncComponent(异步组件)

将组件的 JS 代码从主包中分离,用到时才加载,减少首屏体积。Vite 构建时会自动将其拆为独立 chunk。

import { defineAsyncComponent } from 'vue'

// 基本用法:传入返回 import() 的工厂函数
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))

// 完整配置:加载状态、超时、错误处理
const HeavyChart = defineAsyncComponent({
  loader: () => import('./HeavyChart.vue'),
  loadingComponent: LoadingSpinner,   // 加载中显示的组件
  errorComponent: ErrorBlock,         // 加载失败显示的组件
  delay: 200,                         // 延迟 200ms 后才显示 loading(避免闪烁)
  timeout: 10000,                     // 超过 10s 视为超时,显示 errorComponent
})
<!-- 在模板中像普通组件一样使用,Vue 自动处理懒加载 -->
<template>
  <HeavyChart v-if="showChart" :data="chartData" />
</template>

适用场景: 大型图表、富文本编辑器、PDF 预览等体积较大且非首屏必需的组件。与路由懒加载(() => import('./views/xxx.vue'))原理相同,区别在于异步组件是组件级别的按需加载。


2.5 Teleport

将组件模板的一部分渲染到DOM 树的其他位置(如 body),解决弹窗/浮层被父组件 overflow: hiddenz-index 影响的问题。

<template>
  <button @click="visible = true">打开弹窗</button>

  <!-- 内容渲染到 body 下,而非当前组件 DOM 内 -->
  <Teleport to="body">
    <div v-if="visible" class="modal-overlay">
      <div class="modal">
        <p>弹窗内容</p>
        <button @click="visible = false">关闭</button>
      </div>
    </div>
  </Teleport>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(false)
</script>
属性 说明
to CSS 选择器或 DOM 元素,指定渲染目标(如 "body""#modal-root"
disabled true 时禁用传送,内容回到组件原位

逻辑上仍属于当前组件(props/emits/provide 照常工作),只是 DOM 位置变了。


2.6 自定义指令

封装对 DOM 的底层操作为可复用指令,命名 v-xxx

// directives/vFocus.ts
import type { Directive } from 'vue'

export const vFocus: Directive = {
  mounted(el: HTMLElement) {
    el.focus()   // 元素挂载后自动聚焦
  }
}
<template>
  <input v-focus />
</template>

<script setup lang="ts">
import { vFocus } from '@/directives/vFocus'
</script>

指令生命周期钩子:

钩子 触发时机
created 元素属性/事件绑定前
beforeMount 插入 DOM 前
mounted 插入 DOM 后 ✅ 最常用
beforeUpdate 组件更新前
updated 组件更新后
beforeUnmount 卸载前
unmounted 卸载后

带参数的实际示例(权限指令):

// directives/vPermission.ts
import type { Directive } from 'vue'

export const vPermission: Directive<HTMLElement, string> = {
  mounted(el, binding) {
    // binding.value 就是 v-permission="'admin'" 中的 'admin'
    const userRole = getUserRole()
    if (userRole !== binding.value) {
      el.parentNode?.removeChild(el)  // 无权限则移除元素
    }
  }
}
<button v-permission="'admin'">仅管理员可见</button>

3. Composition API

Composition API 是 Vue 3 的核心编程范式,以函数为基本组织单位,替代 Options API 的 data/methods/computed/watch 分散写法,使相关逻辑聚合在一起,便于复用和维护。

3.1 响应式 API

ref

包装任意类型为响应式数据。JS/TS 中通过 .value 读写,模板中自动解包。包装对象时内部调用 reactive 实现深层响应

import { ref } from 'vue'

const count = ref(0)                    // 基本类型
const user = ref<User | null>(null)     // 对象类型,支持泛型

count.value++                           // JS 中需要 .value

// 嵌套对象同样响应式(内部自动 reactive)
const config = ref({ theme: { color: 'blue' } })
config.value.theme.color = 'red'        // ✅ 视图更新
config.value = { theme: { color: 'green' } }  // ✅ 整体替换也响应式

// 数组同样深层响应式
const list = ref([{ id: 1, name: '张三' }, { id: 2, name: '李四' }])
list.value.push({ id: 3, name: '王五' })       // ✅ 新增元素,视图更新
list.value[0].name = '赵六'                     // ✅ 修改元素属性,视图更新
list.value = list.value.filter(i => i.id !== 2) // ✅ 整体替换,视图更新

自动解包规则 & 注意事项:

场景 需要 .value 说明
模板 {{ count }} 自动解包
JS/TS 代码 count.value++
嵌入 reactive 对象 reactive({ count }).count++
放入数组 / Map reactive([ref(1)])[0].value
解构 .value 丢失响应性,需 toRefs() 转换

reactive

将对象转为深层响应式代理,访问属性无需 .value不能用于基本类型,且不能整体替换(会丢失响应性)。

import { reactive } from 'vue'

const form = reactive({
  name: '',
  age: 0,
  address: { city: '', zip: '' }  // 嵌套对象也是响应式
})

form.name = '张三'             // 直接赋值,无需 .value
form.address.city = '深圳'     // 深层属性也是响应式

ref vs reactive 选择:

场景 推荐 原因
基本类型(string / number / boolean) ref reactive 不支持基本类型
可能被整体替换的对象 ref reactive 重新赋值会丢失响应性
表单等字段固定的复杂对象 reactive 无需 .value,代码更简洁
composable 函数返回值 ref 解构时不丢失响应性

computed

基于响应式依赖自动缓存的派生值,依赖不变则不重新计算。

import { ref, computed } from 'vue'

const price = ref(100)
const quantity = ref(3)

// 只读计算属性
const total = computed(() => price.value * quantity.value)

// 可写计算属性(少用)
const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (val: string) => {
    const [first, last] = val.split(' ')
    firstName.value = first
    lastName.value = last ?? ''
  }
})

与方法的区别: computed 有缓存,多次访问只在依赖变化时重新计算;方法每次调用都重新执行。

watch

监听特定响应式数据,变化时执行回调。适合需要旧值对比有条件执行的场景。

import { ref, watch } from 'vue'

const keyword = ref('')

// 监听单个 ref
watch(keyword, (newVal, oldVal) => {
  console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`)
})

// 监听多个源
watch([keyword, page], ([newKeyword, newPage], [oldKeyword, oldPage]) => {
  fetchList(newKeyword, newPage)
})

// 监听 reactive 对象的某个属性(需用 getter 函数)
const form = reactive({ name: '', age: 0 })
watch(
  () => form.name,
  (newName) => { validate(newName) }
)

// 常用选项
watch(keyword, handler, {
  immediate: true,  // 创建时立即执行一次
  deep: true,       // 深层监听(reactive 对象默认深层,ref 对象需手动开启)
  flush: 'post',    // 在 DOM 更新后执行回调(默认 'pre')
})

watchEffect

自动追踪回调中用到的所有响应式依赖,不需要指定监听源。适合"用了什么就监听什么"的场景。

import { ref, watchEffect } from 'vue'

const keyword = ref('')
const page = ref(1)

// 回调中访问了 keyword 和 page,两者变化都会重新执行
const stop = watchEffect(() => {
  fetchList(keyword.value, page.value)
})

stop()  // 手动停止监听(组件卸载时自动停止)

watch vs watchEffect 对比:

维度 watch watchEffect
监听源 需显式指定 自动追踪回调中的依赖
旧值访问 (newVal, oldVal) ❌ 无旧值
首次执行 默认不执行(immediate: true 开启) 默认立即执行
适用场景 需要旧值对比、条件触发 依赖多且不需要旧值

nextTick

Vue 的 DOM 更新是异步批量的,修改数据后 DOM 不会立即更新。nextTick 等待 DOM 更新完成后执行回调。

import { ref, nextTick } from 'vue'

const show = ref(false)

async function expand() {
  show.value = true
  // 此时 DOM 尚未更新,拿不到新元素
  await nextTick()
  // DOM 已更新,可安全操作
  document.querySelector('.detail')?.scrollIntoView()
}

响应式工具函数

函数 作用 典型场景
toRef(obj, key) 将 reactive 对象的单个属性转为 ref 传递单个属性给 composable
toRefs(obj) 将 reactive 对象的所有属性转为 ref 解构 reactive 不丢失响应性
toRaw(proxy) 返回代理的原始对象 传给第三方库(避免代理副作用)
shallowRef(val) 只有 .value 替换时触发更新,深层属性变化不触发 大型对象 / 组件引用
shallowReactive(obj) 只有顶层属性变化触发更新 扁平配置对象
markRaw(obj) 标记对象永不被代理 第三方类实例(echarts、地图等)
import { reactive, toRefs, toRaw, shallowRef, markRaw } from 'vue'

// toRefs:解构不丢失响应性
const state = reactive({ name: '张三', age: 20 })
const { name, age } = toRefs(state)   // name、age 都是 Ref
name.value = '李四'                    // state.name 同步变化

// shallowRef:大型对象只在整体替换时触发更新
const tableData = shallowRef<Row[]>([])
tableData.value[0].name = '新名字'     // ❌ 不触发更新
tableData.value = [...tableData.value] // ✅ 整体替换才触发

// markRaw:排除不需要响应式的对象
const chart = markRaw(echarts.init(el))

3.2 依赖注入(provide / inject)

已在 2.2 组件通信 — Provide / Inject 中详细介绍,包含基本用法和 InjectionKey 类型安全写法。

核心要点回顾:

  • provide(key, value) 在祖先组件提供数据
  • inject(key) 在任意后代组件注入
  • 推荐使用 InjectionKey<T> 常量管理 key,实现自动类型推导
  • 适用于主题、国际化等跨层级共享数据的场景

3.3 生命周期钩子

Composition API 中通过 onXxx 函数注册生命周期回调,对应组件从创建到销毁的各个阶段。

graph TD
    A["setup()"] --> B["onBeforeMount"]
    B --> C["onMounted<br/>DOM 已挂载,可访问 DOM / 发请求"]
    C --> D["onBeforeUpdate"]
    D --> E["onUpdated<br/>DOM 已更新"]
    E --> D
    C --> F["onBeforeUnmount"]
    F --> G["onUnmounted<br/>组件已销毁,清理副作用"]
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from 'vue'

// setup 本身等价于 beforeCreate + created,无需对应钩子

onMounted(() => {
  // DOM 已渲染,适合:获取 DOM 引用、发起初始请求、初始化第三方库
  initChart()
  fetchData()
})

onUpdated(() => {
  // 响应式数据变化导致 DOM 更新后触发
  // 注意:避免在此修改响应式数据,可能导致无限循环
})

onBeforeUnmount(() => {
  // 组件即将销毁,适合:清除定时器、取消订阅、销毁第三方库实例
  clearInterval(timer)
  chart?.dispose()
})

Options API 与 Composition API 钩子映射:

Options API Composition API 说明
beforeCreate setup() 本身 setup 在所有 Options API 钩子之前执行
created setup() 本身 响应式数据已就绪,但 DOM 未挂载
beforeMount onBeforeMount DOM 挂载前
mounted onMounted DOM 已挂载 ✅
beforeUpdate onBeforeUpdate 数据变化,DOM 更新前
updated onUpdated DOM 已更新
beforeUnmount onBeforeUnmount 组件销毁前
unmounted onUnmounted 组件已销毁

常用原则: 初始化请求放 onMounted(而非 setup),清理工作放 onBeforeUnmount。同一钩子可多次调用,按注册顺序执行。


3.4 组合式函数(Composables)

相关联的响应式状态 + 逻辑提取为独立函数,实现跨组件复用。命名约定以 use 开头。

// composables/useFetch.ts
import { ref, type Ref } from 'vue'

interface UseFetchReturn<T> {
  data: Ref<T | null>
  error: Ref<string>
  isFetching: Ref<boolean>
  execute: () => Promise<void>
}

export function useFetch<T>(url: string | Ref<string>): UseFetchReturn<T> {
  const data = ref<T | null>(null) as Ref<T | null>
  const error = ref('')
  const isFetching = ref(false)

  async function execute() {
    isFetching.value = true
    error.value = ''
    try {
      const resolvedUrl = typeof url === 'string' ? url : url.value
      const res = await fetch(resolvedUrl)
      data.value = await res.json()
    } catch (e: any) {
      error.value = e.message
    } finally {
      isFetching.value = false
    }
  }

  execute()   // 创建时自动执行一次

  return { data, error, isFetching, execute }
}
<!-- 在组件中使用 -->
<template>
  <div v-if="isFetching">加载中...</div>
  <div v-else-if="error">{{ error }}</div>
  <ul v-else>
    <li v-for="item in data" :key="item.id">{{ item.name }}</li>
  </ul>
</template>

<script setup lang="ts">
import { useFetch } from '@/composables/useFetch'

const { data, error, isFetching } = useFetch<Item[]>('/api/items')
</script>

Composable 设计原则:

原则 说明
单一职责 一个 composable 只解决一类问题(如请求、分页、表单校验)
返回 ref 返回值使用 ref 而非 reactive,调用方解构时不丢失响应性
命名 useXxx 约定以 use 开头,表明这是一个组合式函数
可接收 ref 参数 参数支持 `string Ref`,提高灵活性

与 Mixin 的对比: Options API 时代用 Mixin 复用逻辑,但存在命名冲突、来源不明、隐式依赖等问题。Composable 通过显式导入和返回值,完全解决了这些问题。


3.5 <script setup> 语法糖

<script setup> 是 Composition API 在 SFC 中的编译时语法糖,编译器自动处理导出、注册、类型推导,减少样板代码。

核心编译宏

编译宏无需导入,编译器自动识别:

作用 示例
defineProps 声明 props defineProps<{ title: string }>()
defineEmits 声明 emits defineEmits<{ change: [value: string] }>()
defineExpose 暴露实例属性/方法 defineExpose({ reset })
defineModel 声明 v-model 双向绑定 defineModel<string>('status')
withDefaults 为类型声明的 props 设置默认值 withDefaults(defineProps<P>(), { count: 0 })
defineOptions 声明组件选项(如 name / inheritAttrs) defineOptions({ name: 'MyComp' })

<script setup> vs 普通 <script>

<!-- ❌ 普通 script:需要手动 return、注册组件 -->
<script lang="ts">
import { ref, defineComponent } from 'vue'
import MyButton from './MyButton.vue'

export default defineComponent({
  components: { MyButton },
  setup() {
    const count = ref(0)
    function increment() { count.value++ }
    return { count, increment }  // 必须手动 return
  }
})
</script>

<!-- ✅ script setup:顶层变量/导入组件自动暴露给模板 -->
<script setup lang="ts">
import { ref } from 'vue'
import MyButton from './MyButton.vue'   // 自动注册,模板中直接用

const count = ref(0)
function increment() { count.value++ }
// 无需 return,顶层声明自动可在模板中使用
</script>

defineOptions 与属性透传(inheritAttrs)

默认情况下,父组件传给子组件的未声明为 props 的属性(如 classstyleiddata-*)会自动透传到子组件的根元素上。通过 defineOptions({ inheritAttrs: false }) 可关闭自动透传,改用 useAttrs() 手动控制。

<!-- BaseInput.vue -->
<template>
  <!-- 关闭自动透传后,attrs 不会自动加到根 div 上 -->
  <div class="input-wrapper">
    <!-- 手动绑定到指定元素 -->
    <input v-bind="attrs" />
  </div>
</template>

<script setup lang="ts">
import { useAttrs } from 'vue'

defineOptions({
  name: 'BaseInput',        // 组件名(keep-alive include 匹配用)
  inheritAttrs: false        // 关闭自动透传
})

const attrs = useAttrs()     // 获取所有透传属性
</script>
<!-- 父组件使用 -->
<template>
  <!-- class、placeholder、@focus 都会透传到 BaseInput 内部的 <input> 上 -->
  <BaseInput class="custom" placeholder="请输入" @focus="onFocus" />
</template>
场景 inheritAttrs 效果
单根元素组件(默认) true attrs 自动添加到根元素
需要将 attrs 绑定到非根元素 false + v-bind="attrs" 手动控制透传目标
多根元素组件 Vue 警告,必须手动 v-bind="$attrs" 指定

湖北:加大集成电路、工业母机、具身智能等新兴产业和未来产业投资布局力度

2026年2月24日 14:39
36氪获悉,据湖北发布,2月24日,农历新春首个工作日,湖北省加快建成中部地区崛起的重要战略支点推进大会举行。省委书记、省人大常委会主任王忠林出席大会并强调,强化内需主导,在转换经济增长动力上取得新进展。积极适应消费趋势新变化,坚持供需两端发力,大力优化消费供给,培育壮大多元化消费新场景,做强“神武峡”“赤黄红”两大文旅主轴,深入推进国际化消费环境建设试点、消费新业态新模式新场景试点,更加有效地提振消费需求、打开消费空间。持续加强水电路网等基础设施投资,加大集成电路、工业母机、具身智能等新兴产业和未来产业投资布局力度,优化房地产投资,激活民间投资,提高投资效益,与消费共同构成拉动内需增长的“双轮驱动”。

REDOTPAY据悉考虑赴美进行10亿美元的IPO

2026年2月24日 14:35
总部位于香港的加密支付金融科技公司RedotPay据悉考虑赴美进行10亿美元的IPO。消息人士称RedotPay可能最早今年在美国上市,可能在IPO中寻求超过40亿美元的估值。(华尔街见闻)
❌
❌