普通视图

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

国家外汇局:截至2月末我国外汇储备规模为34278亿美元,环比上升0.85%

2026年3月7日 11:26
国家外汇管理局统计数据显示,截至2026年2月末,我国外汇储备规模为34278亿美元,较1月末上升287亿美元,升幅为0.85%。 2026年2月,受主要经济体宏观经济数据、货币政策及预期等因素影响,美元指数上涨,全球主要金融资产价格涨跌互现。汇率折算和资产价格变化等因素综合作用,当月外汇储备规模上升。我国经济稳中有进、向新向优发展,长期向好的支撑条件和基本趋势没有改变,有利于外汇储备规模保持基本稳定。

2025年全年、2026年春节假期国内出游人次与花费均创历史新高

2026年3月7日 11:21
十四届全国人大四次会议今天举行民生主题记者会。文化和旅游部部长孙业礼在会上介绍,今年春节9天假期,国内旅游人次5.96亿、花费超过8000亿元,创下了历史新高。2025年,国内居民出游人次超65亿、同比增长16%以上,花费6.3万亿元、同比增长9.5%,也创造了历史新高。(央视新闻)

人力资源社会保障部部长:正研究措施发挥人工智能创造新岗位和赋能传统岗位作用

2026年3月7日 11:15
人力资源社会保障部部长王晓萍3月7日在十四届全国人大四次会议民生主题记者会上表示,人力资源社会保障部正在研究相关措施,积极发挥人工智能在创造新岗位和赋能传统岗位方面的作用,推动实现技术进步与民生改善相协调的包容性发展。(新华社)

我国高等教育毛入学率超60% “双一流”高校扩招3.8万人

2026年3月7日 11:08
十四届全国人大四次会议今天举行民生主题记者会。教育部部长怀进鹏在会上介绍,当前,我国举办着世界上规模最大且有质量的教育。在高等教育方面,“十四五”期间累计向社会输送5500万人才,毛入学率从十八大前的不到30%到现在超过60%,提高一倍多。近两年通过优质本科扩容,“双一流”高校扩招3.8万人。此外,这五年还新增普通本科高校18所、职业本科70所,增加本科招生70万;推进国际合作办学,新设540多个本科以上中外合作办学机构和项目,新增学位供给35万。(央视新闻)

青岛:发布首批OPC AI工具包清单

2026年3月7日 11:04
3月6日,青岛人工智能产业协会发布青岛市OPC(一人公司)AI工具包(第一批)清单。首批清单涵盖13款工具,工具包聚焦单人创业高频刚需,集成内容生成、数据处理、效率办公、智能客服等核心能力,能够为创业者提供从想法到落地的全链条AI赋能。

中信证券:继续看好存储需求超预期 预计行业供不应求将持续至27H1

2026年3月7日 10:43
3月7日,中信证券研报指出,2月以来,随铠侠业绩及指引超预期、NAND一季度合约价上调、以及国内模组厂商佰维存储发布1-2月业绩公告超预期,进一步验证了存储行业景气度维持高位。中信证券继续看好存储需求超预期,预计行业供不应求将持续至27H1,核心推荐短期业绩爆发性强的存储模组公司、存储原厂及贴近原厂的设计公司。(每经网)

Flutter GetX 深入浅出详解

作者 忆江南
2026年3月7日 10:33

一、GetX 是什么?

GetX 是 Flutter 生态中的一个 全家桶式框架,它不只是一个状态管理方案,而是把状态管理、路由导航、依赖注入三件事打包在了一起。

很多人第一次接触 GetX 的感受是:"怎么什么都能做?" —— 这既是它的优势,也是它的争议所在。

GetX 的三大核心模块

GetX
 ├── 状态管理(State Management)  ── 替代 setState / Provider / Bloc
 ├── 路由管理(Route Management)  ── 替代 Navigator 系列 API
 └── 依赖注入(Dependency Injection)── 替代 Provider / get_it

为什么这么多人用?

一个字:简单

用 Bloc 写一个计数器功能,你需要:Event 类 + State 类 + Bloc 类 + BlocProvider + BlocBuilder,至少 4~5 个文件。

用 GetX 写同样的功能:一个 Controller 类 + Obx(() => Text()),两行搞定。


二、状态管理

2.1 两种响应式风格

GetX 提供了两种状态管理方式,理解它们的区别是用好 GetX 的第一步。

简单状态管理(GetBuilder)

  • 手动调用 update() 通知刷新
  • 类似 setState(),但作用域更小
  • 性能好,适合不需要频繁自动响应的场景

工作原理很朴素:Controller 内部维护一个监听者列表,调用 update() 时遍历通知所有 GetBuilder Widget 重建。本质就是一个 手动版的观察者模式

响应式状态管理(Obx)

  • 变量用 .obs 标记,变化时自动触发 UI 刷新
  • 不需要手动调用 update()
  • 类似 Vue 的响应式、MobX 的 observable
// 声明
var count = 0.obs;

// 修改(自动触发 UI 刷新)
count.value++;

// UI 监听
Obx(() => Text('${controller.count}'))

2.2 .obs 的底层原理

.obs 是 GetX 最核心的魔法。要理解它,需要拆解三个问题:

问题一:.obs 做了什么?

当你写 var count = 0.obs,实际上是把一个普通的 int 包装成了一个 RxInt 对象。这个 Rx 对象内部持有:

  • 真正的值(_value
  • 一个监听者列表(Stream

它本质上就是一个 带通知能力的值容器

问题二:修改值时发生了什么?

当你写 count.value++Rx 对象的 set value 被触发。在 setter 内部:

  1. 更新 _value
  2. 通过 Stream 广播一个"值变了"的事件
  3. 所有订阅了这个 Stream 的监听者收到通知

问题三:Obx 怎么知道要监听哪些变量?

这是 GetX 最巧妙的设计。Obx 并不需要你手动告诉它"我依赖了哪些变量",它是 自动收集依赖 的。

原理分三步:

  1. Obx 在首次 build 时,先打开一个"全局监听开关"
  2. 执行你传入的 builder 函数(比如 () => Text('${count.value}')
  3. count.value 的 getter 被调用时,Rx 对象检测到"监听开关"是打开的,就把自己注册到 Obx 的依赖列表中
  4. builder 执行完毕,关闭"监听开关"

之后任何被收集到的 Rx 变量发生变化,Obx 就会自动重建。

Obx 首次 build
  → 开启"依赖收集模式"
  → 执行 builder: () => Text('${count.value}')
    → count.value 的 getter 被调用
    → count(Rx 对象)发现正在收集依赖,把自己注册进去
  → 关闭"依赖收集模式"
  → 依赖收集完成:[count]

之后 count.value++ 触发
  → Rx 广播变化
  → Obx 收到通知,重新执行 builder

这个机制和 Vue 3 的 watchEffect、MobX 的 autorun 原理几乎一模一样 —— 基于 getter 劫持的自动依赖收集

2.3 GetBuilder 的底层原理

相比之下,GetBuilder 的原理简单得多:

  1. GetBuilderinitState 时,把自己注册到 Controller 的监听者列表
  2. Controller 调用 update(),遍历列表,调用每个 GetBuildersetState
  3. GetBuilderdispose 时,从列表中移除自己

没有 Stream、没有依赖收集,就是最朴素的 观察者模式 + setState

2.4 两种方式怎么选?

场景 推荐 原因
表单页面、简单列表 GetBuilder 手动控制,性能开销最小
数据频繁联动、多变量交叉依赖 Obx + .obs 自动依赖收集,代码更简洁
超大列表、高性能场景 GetBuilder + update([id]) 可以精确控制刷新范围

三、依赖注入

3.1 是什么?

GetX 内置了一套依赖注入系统,核心 API 就两个:

  • Get.put(Controller()) —— 注册
  • Get.find<Controller>() —— 获取

你可以把它理解成一个 全局的"服务柜台":先把东西放进去,需要的时候按类型取出来。

3.2 底层原理

GetX 内部维护了一个 全局的 Map<String, Object>,key 是类型名(或类型名 + tag),value 是实例。

全局容器(简化理解):
{
  "HomeController": HomeController 实例,
  "UserService": UserService 实例,
  "ApiClient_v2": ApiClient 实例(带 tag)
}

Get.put() 就是往 Map 里写,Get.find() 就是从 Map 里读。

3.3 四种注册方式的区别

方式 何时创建 何时销毁 适用场景
Get.put() 立即创建 手动或路由关闭时 页面 Controller
Get.lazyPut() 首次 find 同上 可能用不到的依赖
Get.putAsync() 立即创建(支持异步) 同上 需要异步初始化的服务
Get.create() 每次 find 都新建 不自动销毁 每次需要新实例的场景

3.4 SmartManagement:自动内存管理

GetX 最被低估的能力之一。它有一套 智能内存管理机制,可以在路由关闭时自动销毁关联的 Controller。

三种模式:

  • full(默认):不被任何路由或 Widget 使用的 Controller 自动销毁
  • onlyBuilder:只有通过 GetBuilder / GetX 使用的 Controller 才自动管理
  • keepFactory:销毁实例但保留工厂函数,下次 find 时重新创建

这解决了 Flutter 状态管理中一个常见的痛点:谁来负责销毁 Controller? 在 Provider/Bloc 中你需要手动处理,GetX 帮你自动化了。


四、路由管理

4.1 为什么要替换 Flutter 原生路由?

Flutter 原生路由的痛点:

  • 跳转需要 context,在非 Widget 层(Service、Controller)中很难拿到
  • 传参和接收返回值写法繁琐
  • 路由动画自定义复杂

GetX 的路由通过全局 NavigatorKey 持有 Navigator 的引用,所以 不需要 context 就能跳转。

4.2 底层原理

GetX 路由的核心做了两件事:

第一:全局 NavigatorKey

GetX 在 GetMaterialApp 初始化时,创建了一个全局的 GlobalKey<NavigatorState>,保存在静态变量中。之后所有路由操作都通过这个 key 拿到 Navigator,不再依赖 context。

第二:路由与依赖注入联动

这是 GetX 路由最独特的地方。当你用 Get.to(HomePage()) 跳转时:

  1. 创建一个路由条目
  2. 如果 HomePage 关联了 Controller(通过 GetBuilderBindings),自动 put 进依赖容器
  3. 当路由 pop 时,自动 delete 关联的 Controller
Get.to(HomePage())
  → 创建路由
  → 自动注册 HomeController(如果有 Binding)
  → 用户在 HomePage 操作...
  → Get.back()
  → 路由 pop
  → 自动销毁 HomeController
  → 内存释放

这就形成了一个 路由驱动的生命周期管理:Controller 的生死和页面的进出自动绑定。

4.3 Bindings:依赖与路由的桥梁

Bindings 是连接路由和依赖注入的纽带。它定义了"进入某个页面时需要准备哪些依赖"。

你可以把它类比为 iOS 的 viewDidLoad —— 页面加载时做初始化工作,页面销毁时自动清理。

4.4 中间件(Middleware)

GetX 路由支持中间件,可以在路由跳转前/后插入逻辑:

  • 登录拦截:未登录自动跳转登录页
  • 权限检查:没有权限的页面拒绝访问
  • 埋点:自动记录页面访问

中间件按优先级执行,可以中断跳转(返回 null 表示拦截),和 Web 框架的中间件概念一致。


五、GetX 的其他能力

GetX 是个全家桶,除了三大核心模块,还打包了很多实用工具:

能力 说明
国际化(i18n) 'hello'.tr 即可翻译,动态切换语言
主题切换 Get.changeTheme() 一行切换深色/浅色
网络请求 GetConnect 封装了 HTTP 客户端
本地存储 GetStorage 类似 SharedPreferences 但更快
响应式表单验证 配合 .obs 做实时校验
Snackbar / Dialog / BottomSheet 不需要 context 的全局弹窗
Worker ever / debounce / interval 等响应式工具

Worker 机制

Worker 是 GetX 响应式系统中很实用的工具,用于对 .obs 变量的变化做 节流、防抖、一次性监听 等处理:

Worker 行为
ever(count, callback) 每次变化都执行
once(count, callback) 只在第一次变化时执行
debounce(count, callback) 停止变化后一段时间才执行(搜索场景)
interval(count, callback) 变化期间按固定间隔执行(节流)

底层实现就是对 Rx 的 Stream 做了 listen / first / debounceTime / throttle 等 Dart Stream 操作的封装。


六、GetX 的底层架构总结

把所有模块串起来,GetX 的底层可以概括为三个核心机制:

1. Rx + Stream:响应式引擎

.obs 变量(Rx 对象)
  └── 内部持有 Stream
        └── Obx / Worker 订阅 Stream
              └── 变量变化 → Stream 广播 → 订阅者响应

这是 Dart 语言自带的 Stream 机制,GetX 没有发明新东西,只是在 Stream 之上做了 语法糖封装.obsObxever 等),降低了使用门槛。

2. 全局 Map:依赖注入容器

静态 Map<String, InstanceInfo>
  └── Get.put() 写入
  └── Get.find() 读取
  └── Get.delete() 删除
  └── SmartManagement 自动清理

没有复杂的 IoC 容器,就是一个 Map。简单直接。

3. 全局 NavigatorKey:脱离 context 的路由

GetMaterialApp 初始化 → 持有全局 NavigatorKey
  └── Get.to() / Get.back() → 通过 Key 拿到 Navigator → 执行路由操作
  └── 路由变化 → 触发 Bindings → 联动依赖注入的创建/销毁

七、GetX 的争议

赞成派观点

  • 开发效率极高:原型开发、中小项目飞速
  • 学习曲线平缓:API 直觉化,新手友好
  • 全家桶一站式:不用在多个库之间做选型和协调

反对派观点

  • 过度封装:把 Flutter 的很多设计理念(如 BuildContext、InheritedWidget)绕过了,新手可能对 Flutter 本身理解不深
  • 隐式行为多:自动依赖收集、自动销毁,出了问题难以调试
  • 大型项目维护难:全局状态 + 隐式依赖,随着项目变大,依赖关系会变得不透明
  • 和 Flutter 官方方向渐行渐远:Flutter 团队推崇的是 Riverpod / Provider 思路

客观建议

项目类型 推荐度 建议
个人项目 / Demo 强烈推荐 快速出活
中小型商业项目 推荐 配合良好的分层架构使用
大型团队协作项目 谨慎 建议考虑 Riverpod / Bloc,或严格约束 GetX 的使用范围
学习 Flutter 阶段 不推荐先学 先理解 Flutter 原生机制,再用 GetX 提效

八、GetX vs 其他状态管理方案

维度 GetX Provider Riverpod Bloc
学习成本
模板代码量 极少
依赖 context 不需要 需要 不需要 需要
内置路由
内置依赖注入 自身就是 DI 自身就是 DI 无(需配合)
可测试性
官方推荐 是(早期) 是(现在) 社区主流
适合规模 小中型 中型 中大型 大型

九、一句话总结

GetX 的哲学是 "约定优于配置,简单优于正确"。它牺牲了一些架构上的严谨性,换来了极致的开发效率。理解它的底层原理(Rx Stream + 全局 Map + 全局 NavigatorKey),你就能用好它,也知道它的边界在哪里。

我国科学家研发出人体热发电新型材料

2026年3月7日 10:25
把人变成“行走的充电宝”,有望从科幻走进现实。现在,科学家盯上了我们身上产生的多余热量,研发出一种特殊的塑料热电薄膜材料,可将热能转化为电能,给随身的电子设备充电,其核心指标创造了同类材料的最新世界纪录。这一成果昨天(6日)在国际学术期刊《科学》发表。(央视新闻)

iOS 可视化埋点与无痕埋点详解

作者 忆江南
2026年3月7日 10:21

一、为什么需要不同的埋点方式?

最早大家都是 手动写代码埋点:在每个按钮点击、页面出现的地方,手动调用 track("事件名")。这种方式最精确,但有两个痛点:

  1. 每加一个埋点就要改代码、发版,周期太长
  2. 埋点需求爆炸式增长,开发根本忙不过来

于是业界开始思考:能不能让机器自动采集?能不能让运营自己配置?

这就催生了三种埋点方式的演进:

手动代码埋点 → 无痕埋点(全自动) → 可视化埋点(半自动)
维度 代码埋点 无痕埋点 可视化埋点
谁来埋 开发 机器自动 运营圈选
需要发版吗 需要 不需要 不需要
能带业务参数吗 能(商品ID、金额等) 不能 有限支持
数据量 按需 巨大 按需
精确度 最高 最低 中等

二、无痕埋点(全埋点)

一句话理解

不写任何埋点代码,SDK 自动采集用户的所有操作。

原理:偷梁换柱

iOS 有个强大的 Runtime 机制叫 Method Swizzling —— 可以在运行时把系统方法的实现"偷偷换掉"。

举个例子,iOS 中所有按钮点击最终都会走 UIControlsendAction:to:forEvent: 方法。SDK 做的事情就是:

原本的调用链:
  用户点击按钮 → sendAction → 执行业务逻辑

Swizzle 之后:
  用户点击按钮 → SDK 拦截,记录"谁在哪个页面点了什么" → 再调用原始 sendAction → 执行业务逻辑

业务方完全无感知,SDK 悄悄在中间插了一层数据采集。

SDK 需要 Hook 哪些地方?

拦截点 能采集到什么
UIControl 的点击事件 按钮、开关、滑块等操作
UITableView 的 Cell 点击 列表项点击
UIViewController 的页面出现 页面浏览量(PV)
UIGestureRecognizer 手势操作

核心难题:怎么标识"点的是哪个按钮"?

SDK 需要给每个 UI 元素生成一个 唯一标识(ViewPath),方式是沿着 View 层级往上爬,记录每一层的类名和位置:

UIWindow / UINavigationController / 首页VC / UIView / UITableView / 第3个Cell / 购买按钮

转化成路径就是:

UIWindow[0]/UINavigationController[0]/HomeVC[0]/UIView[0]/UITableView[0]/Cell[3]/UIButton[0]

这就像是给每个按钮一个"门牌号"。

致命缺陷

  1. 门牌号不稳定:UI 稍微改一下层级(比如在按钮外面多套一层 View),路径就变了,之前的数据就对不上了

  2. 只知道行为,不知道内容:SDK 能告诉你"用户点了第3个 Cell 里的按钮",但不知道那个 Cell 显示的是什么商品、多少钱

  3. 数据量爆炸:用户每一次点击、每一次滑动都会上报,90% 的数据可能没人看


三、可视化埋点

一句话理解

在无痕埋点的基础上,加了一个"后台圈选"的功能。运营在后台看到 App 截图,用鼠标点选要追踪的元素,SDK 只上报被选中的事件。

工作流程

第一步:App 和后台建立 WebSocket 连接

第二步:App 截图 + View 树结构 → 发给后台
       (后台能看到 App 当前界面的"透视图")

第三步:运营在后台的截图上点击"立即购买"按钮
       → 后台自动识别出这个按钮的 ViewPath
       → 运营给它命名为 "click_buy_button"

第四步:后台把配置下发给 SDK
       { viewPath: "xxx", eventName: "click_buy_button" }

第五步:SDK 在 Hook 点拦截事件时,拿当前元素的 ViewPath 去配置表里匹配
       → 匹配到了才上报,匹配不到就忽略

和无痕埋点的本质区别

无痕埋点:先采集所有数据 → 后期在数据平台筛选(先采后筛)
可视化埋点:先配置要采什么 → 只采集配置过的(先筛后采)

这就像是:

  • 无痕埋点 = 装了 360 度全景摄像头,24 小时录像,需要的时候回看
  • 可视化埋点 = 在关键位置装定向摄像头,只拍你关心的区域

优势

  • 运营自助:不需要开发介入,运营在后台圈选即可生效
  • 动态生效:配置下发后立即生效,不需要发版
  • 数据可控:只采集被圈选的事件,数据量小

局限

  • 依赖 ViewPath 稳定性:和无痕埋点一样,如果 UI 层级变了,之前圈选的配置就失效了
  • 无法携带复杂业务参数:你能圈选"购买按钮被点击",但很难自动带上"买的是哪个商品"
  • WebView / H5 页面支持复杂:需要额外注入 JS SDK

四、实战中怎么选?

成熟的 App 不会只用一种,而是 混合使用

场景 推荐方式 原因
页面 PV、App 启动/退出 无痕埋点 标准化行为,不需要业务参数
运营活动按钮、Tab 切换 可视化埋点 需求变化快,运营自助配置
支付、注册、加购、分享 代码埋点 需要精确的业务参数(金额、商品ID)

一个简单的原则:

数据越重要、越需要业务参数的事件,越应该用代码埋点;越通用、越标准化的行为,越适合自动采集。


五、ViewPath 稳定性:所有自动埋点方案的阿喀琉斯之踵

ViewPath 不稳定是无痕埋点和可视化埋点最大的技术挑战。业界的应对思路:

  1. 用 accessibilityIdentifier 做锚点:给关键元素设置固定 ID,优先用 ID 而不是层级位置来标识
  2. 模糊匹配:不要求路径完全一致,允许中间层级有增减,只要首尾和关键节点匹配度达到 80% 就算命中
  3. 哈希指纹:结合元素类型、文本内容、相对位置等多维度信息生成指纹,不完全依赖层级路径

六、SwiftUI 时代的新挑战

传统方案依赖 UIKit 的 View 层级树,但 SwiftUI 的渲染机制完全不同 —— 开发者写的 Button 和实际渲染出来的 View 层级之间没有稳定的对应关系。

目前的解决方向:

  • 利用 SwiftUI 的 ViewModifier 机制,做类似 .tracked("buy_button") 的声明式埋点
  • 借助 Accessibility Tree(辅助功能树)作为更稳定的元素标识来源
  • 通过 SwiftUI Introspect 获取底层 UIKit View 做桥接

这个领域还在发展中,还没有像 UIKit 时代那样成熟的方案。


七、隐私合规

  • 截图上传时必须对密码框、身份证号等敏感区域做 模糊处理
  • 不采集键盘输入内容
  • 需要在隐私政策中明确告知用户数据采集范围
  • 遵循 GDPR / 中国个人信息保护法
  • SDK 必须提供关闭开关

八、业界参考

平台 特点
GrowingIO 国内可视化埋点先驱,圈选体验好
神策 Sensors Analytics 全埋点 + 可视化 + 代码埋点全覆盖,iOS SDK 开源
Mixpanel 可视化埋点 + 代码埋点,国际主流
Heap 全埋点理念的代表,"Capture Everything"
Firebase Analytics Google 出品,自动事件 + 自定义事件

谷歌为CEO提供了一份6.92亿美元的新薪酬方案

2026年3月7日 10:17
谷歌已将桑达尔・皮查伊未来三年的潜在总薪酬提高至 6.92 亿美元,使其成为全球薪酬最高的首席执行官之一。该公司周五表示,皮查伊薪酬方案的主要部分为绩效股票单位(PSUs),目标价值 1.26 亿美元,平均分为两批发放。这些绩效股票单位的价值将根据母公司 Alphabet 的总股东回报率与标普 100 指数成分股的对比情况确定:若 Alphabet 业绩显著领先,最高可发放目标价值的两倍,即 2.52 亿美元;若业绩落后,则可能分文不得。(新浪财经)

Vue 3 Composition API深度解析:构建可复用逻辑的终极方案

作者 bluceli
2026年3月7日 10:13

引言

Vue 3的Composition API是Vue框架最重大的更新之一,它提供了一种全新的组件逻辑组织方式。与传统的Options API相比,Composition API让我们能够更灵活地组织和复用代码逻辑。本文将深入探讨Vue 3 Composition API的8大核心特性,帮助你掌握这个构建可复用逻辑的终极方案。

setup函数基础

1. setup函数的基本使用

setup函数是Composition API的入口点,它在组件创建之前执行。

import { ref, reactive } from 'vue';

export default {
  setup() {
    // 定义响应式数据
    const count = ref(0);
    const user = reactive({
      name: 'Vue 3',
      version: '3.0'
    });

    // 定义方法
    const increment = () => {
      count.value++;
    };

    // 返回给模板使用
    return {
      count,
      user,
      increment
    };
  }
};

2. setup函数的参数

setup函数接收两个参数:props和context。

export default {
  props: {
    title: String,
    initialCount: {
      type: Number,
      default: 0
    }
  },
  setup(props, context) {
    // props是响应式的,不能解构
    console.log(props.title);
    
    // context包含attrs、slots、emit等
    const { attrs, slots, emit } = context;
    
    // 触发事件
    const handleClick = () => {
      emit('update', props.initialCount + 1);
    };
    
    return { handleClick };
  }
};

响应式API详解

3. ref与reactive的选择

ref和reactive是创建响应式数据的两种方式,各有适用场景。

import { ref, reactive, toRefs } from 'vue';

// ref - 适合基本类型和单一对象
const count = ref(0);
const message = ref('Hello');

// 访问ref的值需要.value
console.log(count.value);
count.value++;

// reactive - 适合复杂对象
const state = reactive({
  count: 0,
  user: {
    name: 'Vue',
    age: 3
  }
});

// 访问reactive的值不需要.value
console.log(state.count);
state.count++;

// 在模板中自动解包,不需要.value
// <template>
//   <div>{{ count }}</div>
//   <div>{{ state.count }}</div>
// </template>

4. toRefs的使用

当需要从reactive对象中解构属性时,使用toRefs保持响应性。

import { reactive, toRefs } from 'vue';

export default {
  setup() {
    const state = reactive({
      count: 0,
      name: 'Vue 3',
      isActive: true
    });

    // 不推荐 - 失去响应性
    // const { count, name } = state;

    // 推荐 - 使用toRefs保持响应性
    const { count, name, isActive } = toRefs(state);

    const increment = () => {
      count.value++;
    };

    return {
      count,
      name,
      isActive,
      increment
    };
  }
};

计算属性与侦听器

5. computed计算属性

computed用于创建计算属性,支持getter和setter。

import { ref, computed } from 'vue';

export default {
  setup() {
    const firstName = ref('John');
    const lastName = ref('Doe');

    // 只读计算属性
    const fullName = computed(() => {
      return firstName.value + ' ' + lastName.value;
    });

    // 可写计算属性
    const writableFullName = computed({
      get() {
        return firstName.value + ' ' + lastName.value;
      },
      set(value) {
        const [first, last] = value.split(' ');
        firstName.value = first;
        lastName.value = last;
      }
    });

    return {
      firstName,
      lastName,
      fullName,
      writableFullName
    };
  }
};

6. watch与watchEffect

watch和watchEffect用于侦听数据变化。

import { ref, reactive, watch, watchEffect } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const user = reactive({
      name: 'Vue',
      age: 3
    });

    // watchEffect - 自动追踪依赖
    watchEffect(() => {
      console.log(`Count is: ${count.value}`);
      console.log(`User is: ${user.name}`);
    });

    // watch - 显式指定侦听源
    watch(count, (newValue, oldValue) => {
      console.log(`Count changed from ${oldValue} to ${newValue}`);
    });

    // 侦听多个源
    watch([count, () => user.name], ([newCount, newName], [oldCount, oldName]) => {
      console.log(`Count: ${oldCount} -> ${newCount}, Name: ${oldName} -> ${newName}`);
    });

    // watch的配置选项
    watch(
      () => user.name,
      (newValue) => {
        console.log(`Name changed to: ${newValue}`);
      },
      {
        immediate: true,  // 立即执行
        deep: true        // 深度侦听
      }
    );

    return { count, user };
  }
};

生命周期钩子

7. 生命周期钩子的使用

Composition API中的生命周期钩子以on开头。

import { 
  onMounted, 
  onUpdated, 
  onUnmounted,
  onBeforeMount,
  onBeforeUpdate,
  onBeforeUnmount
} from 'vue';

export default {
  setup() {
    onBeforeMount(() => {
      console.log('组件挂载前');
    });

    onMounted(() => {
      console.log('组件已挂载');
      // 可以在这里访问DOM
    });

    onBeforeUpdate(() => {
      console.log('组件更新前');
    });

    onUpdated(() => {
      console.log('组件已更新');
    });

    onBeforeUnmount(() => {
      console.log('组件卸载前');
    });

    onUnmounted(() => {
      console.log('组件已卸载');
      // 清理工作
    });

    return {};
  }
};

自定义组合函数

8. 创建可复用的逻辑

自定义组合函数是Composition API的核心优势,让我们能够提取和复用逻辑。

// useCounter.js - 计数器逻辑
import { ref, computed } from 'vue';

export function useCounter(initialValue = 0) {
  const count = ref(initialValue);

  const increment = () => {
    count.value++;
  };

  const decrement = () => {
    count.value--;
  };

  const reset = () => {
    count.value = initialValue;
  };

  const double = computed(() => count.value * 2);

  return {
    count,
    increment,
    decrement,
    reset,
    double
  };
}

// useMouse.js - 鼠标位置追踪
import { ref, onMounted, onUnmounted } from 'vue';

export function useMouse() {
  const x = ref(0);
  const y = ref(0);

  const update = (event) => {
    x.value = event.pageX;
    y.value = event.pageY;
  };

  onMounted(() => {
    window.addEventListener('mousemove', update);
  });

  onUnmounted(() => {
    window.removeEventListener('mousemove', update);
  });

  return { x, y };
}

// 在组件中使用
import { useCounter, useMouse } from './composables';

export default {
  setup() {
    const { count, increment, decrement, double } = useCounter(10);
    const { x, y } = useMouse();

    return {
      count,
      increment,
      decrement,
      double,
      x,
      y
    };
  }
};

依赖注入

9. provide与inject

provide和inject用于跨组件层级传递数据。

// 父组件
import { provide, ref } from 'vue';

export default {
  setup() {
    const theme = ref('dark');
    const user = ref({
      name: 'Vue User',
      role: 'admin'
    });

    // 提供数据
    provide('theme', theme);
);
    provide('user', user);

    return { theme };
  }
};

// 子组件
import { inject } from 'vue';

export default {
  setup() {
    // 注入数据
    const theme = inject('theme');
    const user = inject('user');

    // 提供默认值
    const config = inject('config', {
      debug: false,
      version: '1.0'
    });

    return { theme, user, config };
  }
};

模板引用

10. 使用ref获取DOM元素

在Composition API中使用ref获取模板引用。

import { ref, onMounted } from 'vue';

export default {
  setup() {
    // 创建模板引用
    const inputRef = ref(null);
    const listRef = ref(null);

    onMounted(() => {
      // 访问DOM元素
      inputRef.value.focus();
      
      // 访问组件实例
      console.log(listRef.value.items);
    });

    const focusInput = () => {
      inputRef.value.focus();
    };

    return {
      inputRef,
      listRef,
      focusInput
    };
  }
};

// 模板中使用
// <template>
//   <input ref="inputRef" />
//   <MyList ref="listRef" />
// </template>

实战案例

11. 表单处理组合函数

// useForm.js
import { ref, reactive } from 'vue';

export function useForm(initialValues, validationRules) {
  const values = reactive({ ...initialValues });
  const errors = reactive({});
  const touched = reactive({});

  const validate = () => {
    let isValid = true;
    
    for (const field in validationRules) {
      const rules = validationRules[field];
      const value = values[field];
      
      for (const rule of rules) {
        if (rule.required && !value) {
          errors[field] = rule.message || '此字段必填';
          isValid = false;
          break;
        }
        
        if (rule.pattern && !rule.pattern.test(value)) {
          errors[field] = rule.message || '格式不正确';
          isValid = false;
          break;
        }
        
        if (rule.validator && !rule.validator(value)) {
          errors[field] = rule.message || '验证失败';
          isValid = false;
          break;
        }
      }
    }
    
    return isValid;
  };

  const handleChange = (field) => (event) => {
    values[field] = event.target.value;
    touched[field] = true;
    
    if (errors[field]) {
      validate();
    }
  };

  const handleBlur = (field) => () => {
    touched[field] = true;
    validate();
  };

  const reset = () => {
    Object.assign(values, initialValues);
    Object.keys(errors).forEach(key => {
      errors[key] = '';
    });
    Object.keys(touched).forEach(key => {
      touched[key] = false;
    });
  };

  const submit = (callback) => () => {
    if (validate()) {
      callback(values);
    }
  };

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    validate,
    reset,
    submit
  };
}

// 使用示例
export default {
  setup() {
    const { values, errors, handleChange, handleBlur, submit } = useForm(
      {
        username: '',
        email: '',
        password: ''
      },
      {
        username: [
          { required: true, message: '用户名必填' },
          { pattern: /^[a-zA-Z0-9_]{3,20}$/, message: '用户名格式不正确' }
        ],
        email: [
          { required: true, message: '邮箱必填' },
          { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式不正确' }
        ],
        password: [
          { required: true, message: '密码必填' },
          { validator: (value) => value.length >= 6, message: '密码至少6位' }
        ]
      }
    );

    const handleSubmit = submit((formData) => {
      console.log('表单提交:', formData);
      // 发送API请求
    });

    return {
      values,
      errors,
      handleChange,
      handleBlur,
      handleSubmit
    };
  }
};

12. 异步数据获取组合函数

// useAsyncData.js
import { ref, onMounted } from 'vue';

export function useAsyncData(fetchFn, options = {}) {
  const {
    immediate = true,
    initialData = null,
    onSuccess,
    onError
  } = options;

  const data = ref(initialData);
  const loading = ref(false);
  const error = ref(null);

  const execute = async (...args) => {
    loading.value = true;
    error.value = null;

    try {
      const result = await fetchFn(...args);
      data.value = result;
      
      if (onSuccess) {
        onSuccess(result);
      }
      
      return result;
    } catch (err) {
      error.value = err;
      
      if (onError) {
        onError(err);
      }
      
      throw err;
    } finally {
      loading.value = false;
    }
  };

  if (immediate) {
    onMounted(execute);
  }

  return {
    data: data,
    loading: loading,
    error: error,
    execute: execute,
    refresh: execute
  };
}

// 使用示例
export default {
  setup() {
    const { data, loading, error, refresh } = useAsyncData(
      async (userId) => {
        const response = await fetch(`/api/users/${userId}`);
        return response.json();
      },
      {
        immediate: true,
        onSuccess: (data) => {
          console.log('数据加载成功:', data);
        },
        onError: (error) => {
          console.error('数据加载失败:', error);
        }
      }
    );

    return {
      data,
      loading,
      error,
      refresh
    };
  }
};

总结

Vue 3 Composition API为我们提供了更强大、更灵活的代码组织方式:

核心优势

  1. 逻辑复用:通过自定义组合函数轻松复用逻辑
  2. 代码组织:相关逻辑可以组织在一起,而不是分散在options中
  3. 类型推断:更好的TypeScript支持
  4. 灵活性:更灵活的代码组织方式

最佳实践

  1. 合理使用ref和reactive:基本类型用ref,复杂对象用reactive
  2. 提取组合函数:将可复用逻辑提取为独立的组合函数
  3. 保持单一职责:每个组合函数只负责一个功能
  4. 善用toRefs:解构reactive对象时使用toRefs保持响应性
  5. 合理使用生命周期:在setup中正确使用生命周期钩子

学习路径

  1. 掌握setup函数和响应式API
  2. 学习computed和watch的使用
  3. 理解生命周期钩子
  4. 实践自定义组合函数
  5. 掌握依赖注入和模板引用

Composition API不仅是一种新的API,更是一种新的思维方式。它让我们能够以更函数式、更模块化的方式组织代码,提高了代码的可维护性和可测试性。开始在你的项目中使用Composition API吧,体验Vue 3带来的全新开发体验!


本文首发于掘金,欢迎关注我的专栏获取更多前端技术干货!

大道至简 - Juejin Notifier - 掘金消息通知小助手

作者 fthux
2026年3月7日 10:07

1.png

在说正事之前,还是要祝各位彦祖亦菲在新的一年里,身体如龙马般精神健硕,事业如朝阳般蒸蒸日上;愿家中灯火可亲,有爱人相伴,有暖茶在心;愿前路浩浩荡荡,万事尽可期待,所求皆如愿,所行化坦途,岁岁常欢愉,年年皆胜意。新年快乐,阖家安康!

好了,收。RT,这个扩展只做一件事:掘金消息通知,就只是告诉你哪类消息有几条未读,无了。

开发初衷:作为一名合格的牛马,必然是天天坐在电脑前认真的搬砖,能够方便的知道有新消息来了,然后再摸着去网站上看一眼就够了。扩展就应该做扩展该做的事情,如果发文章这类事情都交给扩展来做,那 web 站用来做什么?所以这次扩展来掘城只办三件事:通知通知还是TMD通知。

Juejin Notifier 是一款 Chrome 扩展,通过掘金的官方 API 获取消息通知(顺道显示了一下头像和昵称,咱就确认一下是自己的账号就行了,别整了半天是别人的号)。无需频繁的打开掘金 web 站,即可第一(也可能是第二?)时间获取赞和收藏、评论、新增粉丝、私信、系统通知五类消息动态。

代码已开源,点击这里跳转到 GitHub 仓库 ↗

功能

  • 消息通知:及时获取新消息,不错过任何互动,快速查看各类消息通知条数。

  • 个性化设置:可忽略不关心的消息类型;自定义刷新时间间隔;也可手动刷新。

  • 多主题支持(跟随系统 / 浅色 / 深色)。

截图预览

1.png2.png3.png4.png

安装方法

方式一:Chrome Web Store 安装(推荐)

访问 Chrome Web Store 上的 Juejin Notifier ↗ 页面安装。

image.png

方式二:本地安装(开发者模式)

克隆项目,打开 Chrome 浏览器,进入扩展管理页面 (chrome://extensions/),开启右上角的“开发者模式”,点击“加载已解压的扩展程序”,选择扩展文件夹即可。

写在最后

如果各位彦祖亦菲还有什么过分的需求,尽管开口,我努力做到。

也不是我吹牛,反正在座的各位今年一定百事百顺,父母一定长命百岁,做什么事一定手气爆棚。

如果这款扩展真的帮助到了你,还请给个好评 ↗。如果没帮到你,说明我还有很大的进步空间,也请给个好评 ↗以资鼓励。数据只存在本地,代码已开源,可以点击这里跳转到 GitHub 仓库 ↗查看。

诺和诺德和HIMS将共同销售减肥药,结束双方的争端。

2026年3月7日 09:59
Hims & Hers Health(HIMS.US)美股盘后暴涨超35%,消息称诺和诺德计划在Hims & Hers Health Inc.的平台上销售其减肥药,从而终结两家公司之间一场高度公开的纠纷,该纠纷上月曾升级为法律诉讼。这位要求匿名的知情人士表示,诺和诺德与Hims最早可能于下周一宣布建立新的合作关系。两家公司去年曾达成类似协议,但在Hims拒绝停止营销和销售仿制版本药物后,诺和诺德单方面终止了协议。(格隆汇)

微软、谷歌和亚马逊确认继续提供Anthropic技术

2026年3月7日 09:52
在美国国防部近期将人工智能新贵 Anthropic 技术正式认定为供应链风险之后,谷歌周五宣布将继续向客户提供Anthropic的人工智能技术,但国防相关用途除外。随后亚马逊跟进,宣布将继续向其云服务客户提供Anthropic相关技术。此前一天,微软已向客户发布了类似声明。 上周,Anthropic拒绝接受国防部提出的使用条款后,美国总统特朗普下令联邦机构停止使用该公司技术;国防部长皮特・赫格塞斯表示,与Anthropic的合作将在六个月内逐步终止。(第一财经)

前端权限控制设计

2026年3月7日 09:41

一、展示控制

前端权限控制的目的是,根据当前用户的身份控制其能访问的页面和可执行的操作。需要注意的是:前端权限控制主要是为了提升用户体验(如隐藏无权限的菜单,按钮),正真的数据安全必须依赖后端实现。

二、RBAC

业界主流的权限管理模型是RBAC(基于角色的访问控制),其核心思想是将"权限"授予"角色",将"角色"授予"用户",实现了用户与权限的逻辑分离,极大的简化了权限的分配与管理。

三、主要流程

主要包括用户身份认证、权限分配、权限校验和页面展示控制。

  • 用户登录后,前端从后端获取用户的权限列表。
  • 前端根据用户权限信息,决定展示哪些菜单或按钮。
  • 路由级别做权限校验,未授权用户访问受限页面时自动跳转到无权限提示页或登录页。
  • 组件级别做权限控制,操作按钮或表单项根据权限动态展示或禁用。

四、实现要点

1.获取用户权限信息

// context/AuthProvider

const AuthContext = createContext(undefined);

export const useAuth = () => useContext(AuthContext);
export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);

  // 从本地存储中恢复用户权限信息
  useEffect(() => {
    const user = localStorage.getItem('user');
    if (user) {
      setUser(JSON.parse(user));
    }
  }, []);

  const login = async (username, password) => {
    const user = await loginApi(username,password);
    setUser(user);
    // 登录后缓存用户权限信息
    localStorage.setItem('user', JSON.stringify(user));
  };

  const logout = () => {
    setUser(null);
    // 登出后清除本地缓存
    localStorage.removeItem('user');
  };

  const hasPermission = (permission: string | string[]): boolean => {
    if (!user) return false;
    if (Array.isArray(permission)) {
      return permission.some(p => user.permissions.includes(p));
    }
    
    return user.permissions.includes(permission);
  };

  const value = {
    user,
    login,
    logout,
    hasPermission
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

2.封装路由权限校验组件

// components/AuthRoute.js
import { useAuth } from '../context/AuthProvider'; // 自定义 hook,获取用户信息

const AuthRoute = ({ children, meta }) => {
  const { user, hasPermission } = useAuth();

   // 用户未登录,重定向到登录页面
  if (meta.requiresAuth && !user) {
    return <Navigate to="/login" replace />;
  }

  // 用户没有权限,重定向到未授权页面
  if (meta.permission && !hasPermission(meta.permission)) {
    return <Navigate to="/403" replace />;
  }

  // 权限通过,渲染子组件
  return children;
};

export default AuthRoute;

3.创建路由

// router/index.js
import AuthRoute from '../components/AuthRoute';

const Router = () => {
  const element = routes.map(({ path, element:Component, meta }) => ({
      path,
      element: (
        <AuthRoute meta={meta}>
          <Component />
        </AuthRoute>
      )
  }));
  return <RouterProvider router={createBrowserRouter(routers)} />;
};

export default Router;

4.封装按钮权限校验组件


import { useAuth } from '../context/AuthProvider'; // 自定义 hook,获取用户信息

export const AuthButton = ({
  permission,
  children,
  onClick,
}) => {
  const { hasPermission } = useAuth();
  const hasAccess = hasPermission(permission);

  if (!hasAccess) {
    return null;
  }

  return (
    <button 
      onClick={onClick} 
    >
      {children}
    </button>
  );
};

5.按钮权限控制

import { AuthButton } from '../components/AuthButton';

export const ContentManagement = () => {
  
  return (
     <AuthButton 
        permission="content.edit"
        onClick={() => handleEdit(item.id)}
     >
        编辑
     </AuthButton>
  );
};

五、技术难点

1.多粒度权限控制

  • 页面级权限控制:通过前端路由守卫实现,例如,React Router的高阶组件、Vue Router 的beforeEach钩子。
  • 组件级权限控制:通过条件渲染隐藏或禁用无权限的按钮。

2.细粒度权限控制

按钮、表单项等细粒度权限控制,难点在于检查点分散,如果每个按钮都要添加额外的权限控制逻辑,维护成本高;另外权限检查函数频繁执行(如在列表中渲染几十个按钮),可能造成性能问题。

常用的做法是封装自定义 Hook(如 usePermission)或高阶组件,并且缓存组件的权限检查结果。

3.状态管理的复杂性

用户权限信息需要全局共享且保持一致性。难点在于:

  • 初始化时机:页面渲染时可能还没拿到用户信息,容易导致未授权页面闪现。
  • Token 过期:接口返回Token过期,需要自动跳转登录,同时清空本地缓存。
  • 多标签页同步:如果一个标签页登出,其他标签页也需要更新状态,否则可能操作报错。

解决方案通常是利用 Context全局共享,使用webStorage本地缓存,利用广播实现多标签页同步。

4.前后端权限一致性

前端权限控制本质是提升用户体验,正真的数据安全必须依赖后端实现。但难点在于:

  • 双重校验的一致性:前端隐藏了按钮,用户仍可能通过直接访问 API 进行操作,所以后端必须对所有接口做权限校验。
  • 数据同步滞后:如果后端修改了用户权限,前端可能仍保留旧的权限缓存,导致用户看到不应看到的操作或无法访问新功能。需要设计合适的刷新机制(如定时拉取、权限变更后强制刷新)。

美联储柯林斯:无紧急必要改变货币政策立场,预计通胀将缓慢回落至2%的目标

2026年3月7日 09:38
据报道,美联储柯林斯表示,就业增长步伐可能会加快,但很可能会保持总体温和;目前是美联储在利率政策上保持耐心和审慎的时候;若要再次降息,需看到通胀回落的明确证据;没有紧急必要改变货币政策立场,预计通胀将缓慢回落至2%的目标;通胀前景存在不确定性,存在上行风险。并表示,美联储政策目前处于有利地位,预计美联储利率目标“在一段时间内”将保持稳定。(界面新闻)

我国自主研发新一代深水多功能海洋工程船今日下水

2026年3月7日 09:34
3月7日,我国自主研发的新一代深水多功能海洋工程船在江苏启东下水,全面转入调试试验阶段。本次下水的多功能海洋工程船是我国面向深海开发需求自主研发的高端海工装备,采用双层结构、单体流线型设计,总长126米、型宽28米,船舶装备400吨级近海起重机,预搭载3000吨级卷缆盘、水下机器人、水下挖沟机等先进设备,最大作业水深能力达300米,可实现全球海域无限航区通行。(央视新闻)

事件循环底层原理:从 V8 引擎到浏览器实现

2026年3月7日 01:35

前阵子面试被问到:async/await 被编译成什么样了?

我答不上来。面试官说:你用了这么久 async/await,连它怎么实现的都不知道?

回来研究了 V8 源码和 ECMAScript 规范,才发现异步编程的水比想象中深得多。

一、async/await 不是语法糖

很多人说 async/await 是 Promise 的语法糖,严格来说不对。

它更接近 Generator + Promise 的自动执行器。V8 引擎会把 async 函数编译成状态机。

看这段代码:

async function foo() {
  console.log(1);
  await bar();
  console.log(2);
}

V8 编译后大致等价于:

function foo() {
  return new Promise(resolve => {
    const stateMachine = {
      state: 0,
      next(value) {
        switch (this.state) {
          case 0:
            console.log(1);
            this.state = 1;
            return Promise.resolve(bar()).then(v => this.next(v));
          case 1:
            console.log(2);
            resolve();
            return;
        }
      }
    };
    stateMachine.next();
  });
}

每个 await 把函数分成不同的状态,执行完一个 await 就切换到下一个状态。

这就是为什么 await 后面的代码会被放进微任务队列——因为它实际上是 .then() 的回调。

面试追问:为什么 async/await 比 Promise.then 性能好?

因为 V8 对 async/await 做了优化,减少了 Promise 对象的创建。手写 .then().then().then() 会创建多个 Promise 实例,而 async/await 内部可能只创建一个。

二、微任务队列的真实实现

网上都说"微任务队列",但实际上不止一个队列。

根据 HTML 规范,浏览器有:

  1. 微任务队列(Microtask Queue)

    • Promise.then/catch/finally
    • MutationObserver
    • queueMicrotask
  2. Job Queue(ECMAScript 层面)

    • Promise Jobs
    • 这是 ES 规范定义的,比 HTML 规范更底层

Node.js 更复杂:

process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);

Node.js 输出:nextTickpromisetimeoutimmediate

Node.js 有多个队列:

  • nextTick Queue(优先级最高)
  • Promise Queue
  • Timer Queue(setTimeout/setInterval)
  • Check Queue(setImmediate)
  • Poll Queue(I/O)
  • Close Queue

这是一个很多人不知道的点:Node.js 和浏览器的事件循环实现完全不同。

浏览器:HTML 规范定义,一个微任务队列 + 一个宏任务队列

Node.js:libuv 实现,多个阶段,每个阶段有自己的队列

三、MutationObserver 为什么是微任务?

MutationObserver 用来监听 DOM 变化:

const observer = new MutationObserver(() => {
  console.log('DOM changed');
});
observer.observe(document.body, { childList: true });

document.body.appendChild(document.createElement('div'));
console.log('sync');

输出:syncDOM changed

DOM 变化后,回调不是立即执行,而是放进微任务队列。

为什么这样设计?

假设一个循环里改了 100 次 DOM:

for (let i = 0; i < 100; i++) {
  document.body.appendChild(document.createElement('div'));
}

如果每次 DOM 变化都触发回调,会执行 100 次。但如果放进微任务队列,100 次修改完成后只执行一次回调(批量处理)。

这是性能优化的经典设计。

四、Promise 的 then 为什么返回新 Promise?

看这道题:

const p = Promise.resolve(1);
const p2 = p.then(val => val + 1);

console.log(p === p2); // false

then 返回的是新 Promise,不是原来的。

为什么?

为了链式调用。如果返回同一个 Promise,链就会断掉:

Promise.resolve(1)
  .then(val => val + 1) // 返回新 Promise,resolve(2)
  .then(val => val + 2) // 拿到上一个 then 返回的 Promise
  .then(console.log);   // 4

每个 then 都返回新 Promise,形成一条链。

深层问题:then 返回的 Promise 什么时候 settle?

const p = new Promise(resolve => {
  setTimeout(() => resolve('done'), 1000);
});

const p2 = p.then(val => val + '!');

p2 不是立即 settle 的,而是等 p resolve 后,then 的回调执行完,p2 才 resolve。

这涉及 Promise Resolution Procedure(Promise 解决过程),是 ES 规范里最复杂的部分之一。

五、手写 Promise 的核心难点

网上手写 Promise 的文章很多,但大部分都漏了关键点。

1. then 的回调可以返回 Promise

Promise.resolve(1)
  .then(val => Promise.resolve(val + 1))
  .then(console.log); // 2

then 的回调如果返回 Promise,要等这个 Promise settle 后,外层 then 返回的 Promise 才 settle。

then(onFulfilled) {
  return new Promise((resolve, reject) => {
    const result = onFulfilled(this.value);
    // 关键:如果 result 是 Promise,要等它
    if (result instanceof Promise) {
      result.then(resolve, reject);
    } else {
      resolve(result);
    }
  });
}

2. then 可以被调用多次

const p = Promise.resolve(1);
p.then(console.log); // 1
p.then(console.log); // 1
p.then(console.log); // 1

每个 then 都要执行,所以要维护一个回调数组:

class MyPromise {
  constructor(executor) {
    this.callbacks = [];
    
    const resolve = value => {
      this.value = value;
      this.callbacks.forEach(cb => cb(value));
    };
    
    executor(resolve);
  }
  
  then(onFulfilled) {
    this.callbacks.push(onFulfilled);
  }
}

3. 错误穿透

Promise.reject('error')
  .then(val => val + 1)
  .then(val => val + 2)
  .catch(err => console.log(err)); // error

错误会沿着链传递,直到遇到 catch。

then(onFulfilled, onRejected) {
  return new Promise((resolve, reject) => {
    const handle = () => {
      if (this.state === 'fulfilled') {
        try {
          const result = onFulfilled(this.value);
          resolve(result);
        } catch (err) {
          reject(err);
        }
      } else if (this.state === 'rejected') {
        if (onRejected) {
          try {
            const result = onRejected(this.reason);
            resolve(result);
          } catch (err) {
            reject(err);
          }
        } else {
          // 错误穿透:没有 onRejected 就继续传递
          reject(this.reason);
        }
      }
    };
    
    if (this.state) {
      // 已 settle,异步执行
      queueMicrotask(handle);
    } else {
      // pending,加入队列
      this.callbacks.push(handle);
    }
  });
}

六、性能优化:避免 Promise 地狱

问题:Promise 创建是有开销的

// 不好:创建大量不必要的 Promise
async function processItems(items) {
  const results = [];
  for (const item of items) {
    const result = await Promise.resolve(item).then(x => x * 2);
    results.push(result);
  }
  return results;
}

// 好:直接处理
async function processItems(items) {
  return items.map(item => item * 2);
}

问题:微任务队列堆积

// 这段代码会导致微任务队列堆积,阻塞渲染
async function bad() {
  while (true) {
    await Promise.resolve();
    // 这个循环会永远执行,UI 会卡死
  }
}

微任务不会让出执行权给渲染,所以长时间运行的微任务会让页面卡顿。

解决方案:偶尔让出控制权

async function good() {
  while (true) {
    await new Promise(resolve => setTimeout(resolve, 0));
    // 让出控制权,让浏览器有机会渲染
  }
}

setTimeout(0) 会创建宏任务,每次宏任务之间浏览器有机会渲染。

七、冷门但重要的知识点

1. Promise 的构造函数是同步执行的

const p = new Promise(resolve => {
  console.log('executor');
  resolve(1);
});

console.log('after new');

// 输出:executor → after new

Promise 构造函数里的代码是同步执行的,只有 then 回调是异步的。

2. unhandledrejection 事件

Promise.reject('error');

window.addEventListener('unhandledrejection', event => {
  console.log('未处理的 rejection:', event.reason);
});

Promise 被 reject 但没有 catch,会触发这个事件。

Node.js 类似:

process.on('unhandledRejection', (reason, promise) => {
  console.log('未处理的 rejection:', reason);
});

3. Promise.finally 的特殊行为

Promise.resolve(1)
  .finally(() => {
    console.log('finally');
    return 2; // 返回值被忽略
  })
  .then(console.log); // 1,不是 2

finally 不改变传递的值,只执行副作用。

但如果 finally 返回 rejected Promise:

Promise.resolve(1)
  .finally(() => {
    return Promise.reject('error');
  })
  .then(
    val => console.log(val),
    err => console.log(err) // error
  );

4. async 函数的隐式 try-catch

async function foo() {
  throw new Error('fail');
}

foo();
// 错误被包装成 rejected Promise,不会抛到全局

等价于:

function foo() {
  return new Promise((resolve, reject) => {
    try {
      throw new Error('fail');
    } catch (err) {
      reject(err);
    }
  });
}

八、调试异步代码的技巧

1. Chrome DevTools 的 Async Stack Trace

勾选 Console 的 "Async" 选项,可以看到异步调用栈:

async function a() {
  await b();
}

async function b() {
  await c();
}

async function c() {
  console.log('here');
  throw new Error('fail');
}

a();

不开启 Async Stack Trace,调用栈只有 c。

开启后,可以看到 a → b → c 的完整调用链。

2. Node.js 的 --async-stack-traces

node --async-stack-traces app.js

Node.js 12+ 支持,让异步错误堆栈更清晰。

总结

异步编程的难点不在 API,而在于:

  1. 理解底层机制 — V8 如何编译 async/await,事件循环如何调度
  2. 知道边界情况 — Node.js 和浏览器的差异,微任务堆积问题
  3. 能写出正确实现 — Promise 的 resolve procedure,then 的链式调用

面试时,面试官问你"async/await 怎么实现的",不是让你背答案,而是看你是否真的理解原理。


参考资料:

❌
❌