写在前面
日常写页面,90% 的通信都发生在 主进程 · 主线程 的组件树上------和 Vue 的 props / Pinia / mitt 几乎同构。
只有卡 UI、跑卡片 Extension、嵌 Web、跨应用时,才碰到线程 / 进程边界。
本篇阅读顺序:
- 组件通信(状态怎么共享)
- 事件通知(EventHub / Emitter)
- 进程 / 线程是什么 ,以及 谁能通、怎么通
| 边界 | 能不能直接共享对象 | 典型手段 |
|---|---|---|
| 同进程 · 同线程(组件树) | 可以 | @State 系 / Storage / EventHub |
| 同进程 · 跨 ArkTS 线程 | 否(要序列化) | TaskPool / Worker / Emitter |
| 跨进程 / 跨应用 | 否 | CommonEvent / Want / IPC / Web 桥 |
一、组件通信:状态共享(同进程 · 同线程)
业务特征:改数据 → UI 自动刷。优先用状态,别一上来 EventBus。
1.1 按共享范围选型(对照 Vue)
| 共享范围 | 鸿蒙 | Web |
|---|---|---|
| 组件自己 | @State |
ref / reactive |
| 父子单向 | @State + @Prop |
props |
| 父子双向 | @State + @Link |
v-model |
| 嵌套对象深层字段 | @Observed + @ObjectLink |
深层 reactive |
| 跨很多层、同棵树 | @Provide / @Consume |
provide / inject |
| 同 Ability 多页面 | LocalStorage + @LocalStorageLink/Prop |
页面级 store |
| 应用全局(同进程) | AppStorage + @StorageLink/Prop |
Pinia |
| 重启还要在 | PersistentStorage(经 AppStorage) |
persist 插件 / localStorage |
官方优先级(范围能小则小):
@State+@Prop/@Link/@ObjectLink → @Provide/@Consume → LocalStorage → AppStorage
1.2 组件内:@State ≈ ref
ts
@State isLoading: boolean = false
@State title: string = '首页'
只读、从不改、或不绑 UI 的字段,不要标成状态变量(有性能开销)。
1.3 父子:@Prop / @Link ≈ props / v-model
ts
// 父
@State count: number = 0
Child({ count: this.count, countLink: $count })
// 子:单向
@Prop count: number = 0
// 子:双向(父用 $ 传引用)
@Link countLink: number
@Prop |
@Link |
|
|---|---|---|
| 绑定 | 单向(深拷贝) | 双向(引用) |
| 复杂对象 | 拷贝贵 | 更省 |
| 适合 | 编辑页「确认前不同步父」 | 滚动同步、实时联动 |
嵌套 class 的字段变化要用 @Observed + @ObjectLink,否则子属性改了 UI 可能不刷。
1.4 跨层:@Provide / @Consume ≈ provide / inject
ts
// 祖先
@Provide('theme') themeColor: string = '#0A59F7'
// 任意后代,按名或别名匹配
@Consume('theme') themeColor: string
适合主题、导航栈、登录用户简要信息等「整棵树全局一点、又不想层层 props」的状态。
改动极频繁、结构巨大的,仍优先拆小或放到合适的 Storage。
1.5 页面级 / 应用级:LocalStorage / AppStorage
ts
const pageStore = new LocalStorage({ tabIndex: 0 })
@Entry(pageStore)
@Component
struct Home {
@LocalStorageLink('tabIndex') tabIndex: number = 0
}
@StorageLink('userName') userName: string = ''
| LocalStorage | AppStorage | |
|---|---|---|
| 范围 | Ability / 页面级组件树 | 应用进程内全局 |
| 生命周期 | 无引用可回收 | 进程起创建、进程终回收 |
| 典型 | 当前 Tab、页内筛选 | 用户信息、语言、全局开关 |
持久化用 PersistentStorage.persistProp(经 AppStorage)。无 UI 同步的纯缓存更适合 Preferences。
AppStorage 不能跨进程,别当跨 Ability 进程的数据库。
1.6 Ability / 页面跳转带参
Want / 路由参数 = 启动时一次性信封(≈ router.push({ query }))。
跳转后还要持续共享 → 再用 LocalStorage / AppStorage。
二、事件通知:EventHub 与 Emitter
业务特征:组件树对不上,或不想用状态硬绑------只要「发一下、收一下」。
能状态驱动就别滥用事件 ;更不要用 @StorageLink 当事件总线(会误刷一堆 UI)。
2.1 怎么选
| 场景 | 手段 | Web 对照 |
|---|---|---|
| 同一 UIAbility 内、主线程 UI ↔ 页面 / VM | EventHub (context.eventHub) |
页面内 mitt |
| 同进程内可能跨线程(含 TaskPool / Worker) | Emitter | 可跨 Worker 的事件总线 |
| 想强类型 API | Emitter 外包一层 EventBus | 自封一层 mitt |
| EventHub | Emitter | |
|---|---|---|
| 定位 | Ability 上下文上的轻量发布订阅 | 带队列 / 优先级的事件机制 |
| 作用域 | 某个 context |
同进程线程内 / 线程间 |
| 跨进程 | ❌ | ❌ |
| 另有 | --- | once、优先级 |
| 数据 | 较自由 | EventData,有大小上限(常见约 16MB) |
口诀:主线程 UI 里简单广播 → EventHub;涉及子线程或要跨线程 → Emitter。
2.2 EventHub(同 Ability · 主线程)
ts
// 订阅(注意在 aboutToDisappear / onDestroy 里 off)
this.context.eventHub.on('loginSuccess', (userName: string) => {
// ...
});
// 发送
this.context.eventHub.emit('loginSuccess', 'Alice');
EventHub 不是全局单例 ,跟具体 context 绑定;换 Ability / 跨进程都无效。
2.3 Emitter(同进程 · 可跨线程)
ts
import { emitter } from '@kit.BasicServicesKit';
const EVENT_ID = 1;
emitter.on({ eventId: EVENT_ID }, (eventData: emitter.EventData) => {
const value = eventData.data?.value;
});
emitter.emit(
{ eventId: EVENT_ID, priority: emitter.EventPriority.HIGH },
{ data: { value: 2 } }
);
emitter.off(EVENT_ID);
组件或任务结束务必 off。业务可包强类型 EventBus(序列化 + 泛型),见 掘金封装。
三、通信总原则(先看边界再选 API)
到这里,日常组件和事件已经够用。下面进入边界:线程 / 进程。
text
同进程同线程? → 状态装饰器 / EventHub(第一、二节)
同进程跨线程? → TaskPool / Worker / Emitter
跨进程 / 跨应用? → CommonEvent / Want / IPC(第一节的 Storage、第二节的 EventHub/Emitter 都过不去)
| 手段 | 同线程 | 同进程跨线程 | 跨进程 | 跨应用 |
|---|---|---|---|---|
@State / @Prop / @Link / Provide |
✅ | ❌ | ❌ | ❌ |
| LocalStorage / AppStorage | ✅(主线程 UI) | ❌ | ❌ | ❌ |
| EventHub | ✅ | ❌ | ❌ | ❌ |
| Emitter | ✅ | ✅ | ❌ | ❌ |
| TaskPool 入参/返回值 | --- | ✅ | ❌ | ❌ |
Worker postMessage |
--- | ✅ | ❌ | ❌ |
| Want 启动传参 | ✅ | ✅ | ✅(一次性) | ✅(有权限) |
| CommonEvent | ✅ | ✅ | ✅ | ✅(注意权限) |
| IPC / RPC | --- | --- | ✅ | ✅ |
| Web 桥接 | --- | --- | ✅(主进程↔Render) | --- |
掘金粘贴注意:
- 代码块语言选
mermaid(不要选 text)- 从本文件纯文本复制,勿带网页转义字符
- 节点文案避免
@、/、嵌套subgraph(掘金易空白);特殊字符用双引号包起来,写法对齐本系列第 07 篇---
四、进程是什么(Stage 模型)
进程是系统分配资源的基本单位(独立地址空间)。进程 A 崩了,默认不拖死 B。
Web 类比:浏览器里标签 / 扩展 / 渲染进程各自隔离;鸿蒙把隔离做进应用的 Ability 组合里。
4.1 一个应用里常见进程
| 进程类型 | 谁跑在里面 | Web 粗类比 |
|---|---|---|
| 主进程 | 默认所有 UIAbility;以及 Service / DataShare Extension |
SPA 主进程 |
| Extension 进程 | 同类型 Extension(卡片、输入法等;不含上面两个例外) | 插件 / Widget 进程 |
| Render 进程 | Web 的网页渲染 / JS |
Chromium renderer |
| 子进程(可选) | childProcessManager;跟随父进程生命周期 |
少见 |
| 模块独立 / 动态 / 静态指定进程(可选) | 多 HAP、isolationMode / isolationProcess / process |
大应用拆进程 |
ArkWeb 还有 GPU / 孵化等系统进程;业务主要关心 主进程 ↔ 渲染进程(走 Web 桥,不是自己 IPC)。
进程名字符串没有稳定业务语义,不要写死「某进程名 === 输入法」。
4.2 和 Ability 的关系
- 一进程可有多个 AbilityStage / Ability;进程内 Ability 都退出后进程才销毁。
- 多数页面通信 = 主进程内部(第一、二节)。
- 卡片 Extension、独立进程 HAP、别的 App、Web 渲染 → 才跨进程。
本仓库:EntryAbility + 页面默认都在主进程主线程。
- 函数要
@Concurrent,逻辑自包含。 - 传参 / 返回有序列化开销。
- 子线程禁止改
@State、动 UI、用 AppStorage / 非线程安全模块。 - 执行中要喊别的线程:Concurrent 里
emitter.emit。
6.3 主线程 ↔ Worker
ts
this.worker.postMessage({ type: 'reverse', payload: 'abc' });
this.worker.onmessage = (e) => { /* e.data */ };
// worker.ets
workerPort.onmessage = (e) => {
workerPort.postMessage(String(e.data.payload).split('').reverse().join(''));
};
和 Web Worker 同构;只有主线程更新 UI。
七、进程 ↔ 进程:能不能通、怎么通
7.1 矩阵
| 从 \ 到 | 主进程 | 本应用 Extension | Render(Web) | 子进程 | 其它应用 |
|---|---|---|---|---|---|
| 主进程 | ---(见第一~六节) | ✅ CommonEvent / Want / IPC | ✅ Web 桥 | ✅ 视创建 API | ✅ CommonEvent / Want / IPC |
| Extension | ✅ 同上 | ✅ CES / IPC | ⚠️ 多经主进程 | ⚠️ 少见 | ✅ CommonEvent 等 |
| Render | ✅ Web 桥 / 回调 | ⚠️ 不直连业务 Extension | --- | ❌ | ❌ |
| 其它应用 | ✅ | ✅ | ❌ | ❌ | ✅ |
跨进程无效: Emitter、EventHub、@State / AppStorage / LocalStorage。
7.2 CommonEvent(跨进程首选通知)
ts
import { commonEventManager } from '@kit.BasicServicesKit';
let subscriber: commonEventManager.CommonEventSubscriber;
commonEventManager.createSubscriber(
{ events: ['com.example.MY_EVENT'] },
(err, sub) => {
if (err) return;
subscriber = sub;
commonEventManager.subscribe(subscriber, (err2, data) => {
// data.event / data.data
});
}
);
commonEventManager.publish('com.example.MY_EVENT', {
data: JSON.stringify({ from: 'main' }),
}, (err) => {});
一对多、松耦合。可用权限 / bundleName 限制收发方。事件名要全局唯一;敏感信息别塞广播。
典型:主进程 ↔ 卡片 FormExtension。
7.3 Want / IPC / Web 桥
| 手段 | 用途 |
|---|---|
| Want | 拉起 Ability 时一次性带参,不是长连接 |
| IPC / RPC | 跨进程(或跨设备)方法调用;多经系统 Kit 封装 |
| Web 桥 | 主进程 ↔ Render;JSON 过桥,见 插播 WebView |
网页里 emitter.on 收不到主线程业务事件。
八、按业务再问一遍
text
1. 驱动 UI 的状态? → @State 系 / Storage(范围尽量小)
2. 只是通知且树对不上? → EventHub(同 Ability)或 Emitter(可跨线程)
3. 会卡主线程? → TaskPool(默认)或 Worker(常驻)
4. 跨进程 / 跨应用? → CommonEvent 或 IPC
九、小结
- 先组件状态、再事件通知------主进程主线程上和 Vue 同构。
- EventHub 管 Ability 内广播;Emitter 管同进程(含跨线程);二者都 不能跨进程。
- 线程 :宿主↔TaskPool 靠任务参数/返回值;宿主↔Worker 靠
postMessage;任意线程靠 Emitter。 - 进程:跨进程用 CommonEvent / Want / IPC;AppStorage 不是跨进程存储。
- Web 渲染进程走专用桥,不是业务 Emitter。
先做对组件和事件,再按「同不同线程 / 同不同进程」升级通信手段。