普通视图
2025年全年、2026年春节假期国内出游人次与花费均创历史新高
人力资源社会保障部部长:正研究措施发挥人工智能创造新岗位和赋能传统岗位作用
我国高等教育毛入学率超60% “双一流”高校扩招3.8万人
青岛:发布首批OPC AI工具包清单
中信证券:继续看好存储需求超预期 预计行业供不应求将持续至27H1
Flutter GetX 深入浅出详解
一、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 内部:
- 更新
_value - 通过
Stream广播一个"值变了"的事件 - 所有订阅了这个 Stream 的监听者收到通知
问题三:Obx 怎么知道要监听哪些变量?
这是 GetX 最巧妙的设计。Obx 并不需要你手动告诉它"我依赖了哪些变量",它是 自动收集依赖 的。
原理分三步:
-
Obx在首次 build 时,先打开一个"全局监听开关" - 执行你传入的 builder 函数(比如
() => Text('${count.value}')) - 当
count.value的 getter 被调用时,Rx对象检测到"监听开关"是打开的,就把自己注册到Obx的依赖列表中 - 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 的原理简单得多:
-
GetBuilder在initState时,把自己注册到 Controller 的监听者列表 - Controller 调用
update(),遍历列表,调用每个GetBuilder的setState -
GetBuilder在dispose时,从列表中移除自己
没有 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()) 跳转时:
- 创建一个路由条目
- 如果 HomePage 关联了 Controller(通过
GetBuilder或Bindings),自动put进依赖容器 - 当路由
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 之上做了 语法糖封装(.obs、Obx、ever 等),降低了使用门槛。
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),你就能用好它,也知道它的边界在哪里。
我国科学家研发出人体热发电新型材料
iOS 可视化埋点与无痕埋点详解
一、为什么需要不同的埋点方式?
最早大家都是 手动写代码埋点:在每个按钮点击、页面出现的地方,手动调用 track("事件名")。这种方式最精确,但有两个痛点:
- 每加一个埋点就要改代码、发版,周期太长
- 埋点需求爆炸式增长,开发根本忙不过来
于是业界开始思考:能不能让机器自动采集?能不能让运营自己配置?
这就催生了三种埋点方式的演进:
手动代码埋点 → 无痕埋点(全自动) → 可视化埋点(半自动)
| 维度 | 代码埋点 | 无痕埋点 | 可视化埋点 |
|---|---|---|---|
| 谁来埋 | 开发 | 机器自动 | 运营圈选 |
| 需要发版吗 | 需要 | 不需要 | 不需要 |
| 能带业务参数吗 | 能(商品ID、金额等) | 不能 | 有限支持 |
| 数据量 | 按需 | 巨大 | 按需 |
| 精确度 | 最高 | 最低 | 中等 |
二、无痕埋点(全埋点)
一句话理解
不写任何埋点代码,SDK 自动采集用户的所有操作。
原理:偷梁换柱
iOS 有个强大的 Runtime 机制叫 Method Swizzling —— 可以在运行时把系统方法的实现"偷偷换掉"。
举个例子,iOS 中所有按钮点击最终都会走 UIControl 的 sendAction: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]
这就像是给每个按钮一个"门牌号"。
致命缺陷
-
门牌号不稳定:UI 稍微改一下层级(比如在按钮外面多套一层 View),路径就变了,之前的数据就对不上了
-
只知道行为,不知道内容:SDK 能告诉你"用户点了第3个 Cell 里的按钮",但不知道那个 Cell 显示的是什么商品、多少钱
-
数据量爆炸:用户每一次点击、每一次滑动都会上报,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 不稳定是无痕埋点和可视化埋点最大的技术挑战。业界的应对思路:
- 用 accessibilityIdentifier 做锚点:给关键元素设置固定 ID,优先用 ID 而不是层级位置来标识
- 模糊匹配:不要求路径完全一致,允许中间层级有增减,只要首尾和关键节点匹配度达到 80% 就算命中
- 哈希指纹:结合元素类型、文本内容、相对位置等多维度信息生成指纹,不完全依赖层级路径
六、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亿美元的新薪酬方案
Vue 3 Composition API深度解析:构建可复用逻辑的终极方案
引言
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为我们提供了更强大、更灵活的代码组织方式:
核心优势
- 逻辑复用:通过自定义组合函数轻松复用逻辑
- 代码组织:相关逻辑可以组织在一起,而不是分散在options中
- 类型推断:更好的TypeScript支持
- 灵活性:更灵活的代码组织方式
最佳实践
- 合理使用ref和reactive:基本类型用ref,复杂对象用reactive
- 提取组合函数:将可复用逻辑提取为独立的组合函数
- 保持单一职责:每个组合函数只负责一个功能
- 善用toRefs:解构reactive对象时使用toRefs保持响应性
- 合理使用生命周期:在setup中正确使用生命周期钩子
学习路径
- 掌握setup函数和响应式API
- 学习computed和watch的使用
- 理解生命周期钩子
- 实践自定义组合函数
- 掌握依赖注入和模板引用
Composition API不仅是一种新的API,更是一种新的思维方式。它让我们能够以更函数式、更模块化的方式组织代码,提高了代码的可维护性和可测试性。开始在你的项目中使用Composition API吧,体验Vue 3带来的全新开发体验!
本文首发于掘金,欢迎关注我的专栏获取更多前端技术干货!
大道至简 - Juejin Notifier - 掘金消息通知小助手
![]()
在说正事之前,还是要祝各位彦祖亦菲在新的一年里,身体如龙马般精神健硕,事业如朝阳般蒸蒸日上;愿家中灯火可亲,有爱人相伴,有暖茶在心;愿前路浩浩荡荡,万事尽可期待,所求皆如愿,所行化坦途,岁岁常欢愉,年年皆胜意。新年快乐,阖家安康!
好了,收。RT,这个扩展只做一件事:掘金消息通知,就只是告诉你哪类消息有几条未读,无了。
开发初衷:作为一名合格的牛马,必然是天天坐在电脑前认真的搬砖,能够方便的知道有新消息来了,然后再摸着去网站上看一眼就够了。扩展就应该做扩展该做的事情,如果发文章这类事情都交给扩展来做,那 web 站用来做什么?所以这次扩展来掘城只办三件事:通知通知还是TMD通知。
Juejin Notifier 是一款 Chrome 扩展,通过掘金的官方 API 获取消息通知(顺道显示了一下头像和昵称,咱就确认一下是自己的账号就行了,别整了半天是别人的号)。无需频繁的打开掘金 web 站,即可第一(也可能是第二?)时间获取赞和收藏、评论、新增粉丝、私信、系统通知五类消息动态。
代码已开源,点击这里跳转到 GitHub 仓库 ↗。
功能
-
消息通知:及时获取新消息,不错过任何互动,快速查看各类消息通知条数。
-
个性化设置:可忽略不关心的消息类型;自定义刷新时间间隔;也可手动刷新。
-
多主题支持(跟随系统 / 浅色 / 深色)。
截图预览
安装方法
方式一:Chrome Web Store 安装(推荐)
访问 Chrome Web Store 上的 Juejin Notifier ↗ 页面安装。
![]()
方式二:本地安装(开发者模式)
克隆项目,打开 Chrome 浏览器,进入扩展管理页面 (chrome://extensions/),开启右上角的“开发者模式”,点击“加载已解压的扩展程序”,选择扩展文件夹即可。
写在最后
如果各位彦祖亦菲还有什么过分的需求,尽管开口,我努力做到。
也不是我吹牛,反正在座的各位今年一定百事百顺,父母一定长命百岁,做什么事一定手气爆棚。
如果这款扩展真的帮助到了你,还请给个好评 ↗。如果没帮到你,说明我还有很大的进步空间,也请给个好评 ↗以资鼓励。数据只存在本地,代码已开源,可以点击这里跳转到 GitHub 仓库 ↗查看。
诺和诺德和HIMS将共同销售减肥药,结束双方的争端。
微软、谷歌和亚马逊确认继续提供Anthropic技术
欧洲私募巨头CVC拟在上市前收购比利时Belfius银行股份
前端权限控制设计
一、展示控制
前端权限控制的目的是,根据当前用户的身份控制其能访问的页面和可执行的操作。需要注意的是:前端权限控制主要是为了提升用户体验(如隐藏无权限的菜单,按钮),正真的数据安全必须依赖后端实现。
二、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%的目标
我国自主研发新一代深水多功能海洋工程船今日下水
事件循环底层原理:从 V8 引擎到浏览器实现
前阵子面试被问到: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 规范,浏览器有:
-
微任务队列(Microtask Queue)
- Promise.then/catch/finally
- MutationObserver
- queueMicrotask
-
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 输出:nextTick → promise → timeout → immediate
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');
输出:sync → DOM 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,而在于:
- 理解底层机制 — V8 如何编译 async/await,事件循环如何调度
- 知道边界情况 — Node.js 和浏览器的差异,微任务堆积问题
- 能写出正确实现 — Promise 的 resolve procedure,then 的链式调用
面试时,面试官问你"async/await 怎么实现的",不是让你背答案,而是看你是否真的理解原理。
参考资料: