交互逻辑分层模型:把复杂拆成三块
做座舱交互设计,最怕什么?
最怕一个按钮按下去,导航卡了、音乐停了、空调也失灵了。这种「牵一发而动全身」的噩梦,我经历过不止一次。
后来我学乖了。把交互逻辑拆成三层,各管各的,互不干扰。说白了,就是「分而治之」。
三层模型:感知层、决策层、执行层
我习惯把交互逻辑分成三层:
- 感知层:负责接收输入。触摸、语音、手势、旋钮……所有用户操作都在这层被捕获。
- 决策层:负责判断「用户想干嘛」。比如用户说「我冷了」,系统得知道这是要调空调温度。
- 执行层:负责干活。调温度、切歌、导航到目的地……真正改变状态的动作都在这里。
你想想看,如果这三层混在一起,会怎样?
我在一个项目里见过这样的代码:触摸事件里直接调用了空调控制接口。结果后来要加语音控制,整个逻辑都得重写。嗯,那叫一个酸爽。
核心原则:每一层只做自己的事,不越界。
感知层不决策,决策层不执行,执行层不感知。
实际案例:语音调温度
举个例子。用户说「把温度调到26度」。
// 感知层:语音识别
class VoiceInput {
onVoiceCommand(text) {
// 只做识别,不做判断
this.emit('command', { type: 'voice', raw: text });
}
}
// 决策层:意图理解
class IntentProcessor {
process(command) {
if (command.raw.includes('温度')) {
// 提取温度值
const temp = parseInt(command.raw.match(/\d+/)[0]);
return { action: 'setTemp', value: temp };
}
}
}
// 执行层:实际控制
class ClimateController {
setTemperature(temp) {
// 调用硬件接口
hvac.setTargetTemp(temp);
}
}
这样拆开之后,好处很明显:
- 想换语音引擎?只改感知层。
- 想加手势控制?再加一个感知模块就行。
- 空调硬件换了?只改执行层。
我的经验:刚开始做分层时,别追求完美。先拆成三层,哪怕每层只有几个函数。后面再慢慢细化。
事件驱动机制:别轮询,等通知
座舱里同时发生的事情太多了。用户操作、传感器数据、网络请求……如果都用轮询,CPU得累死。
事件驱动,说白了就是「有事叫我,没事别烦我」。
事件的三要素
一个完整的事件机制,需要三样东西:
- 事件源:谁发出的?比如触摸屏、麦克风。
- 事件类型:什么事?比如 click、voiceCommand。
- 事件数据:带什么信息?比如坐标、语音文本。
我见过最糟糕的设计,是把所有事件都塞到一个全局变量里。结果调试的时候,根本不知道是谁改了这个变量。嗯,那叫一个崩溃。
事件总线:一个简单的实现
class EventBus {
constructor() {
this.listeners = {};
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
emit(event, data) {
const callbacks = this.listeners[event] || [];
callbacks.forEach(cb => cb(data));
}
off(event, callback) {
const callbacks = this.listeners[event] || [];
this.listeners[event] = callbacks.filter(cb => cb !== callback);
}
}
用起来很简单:
const bus = new EventBus();
// 订阅事件
bus.on('temperatureChanged', (temp) => {
updateDisplay(temp);
});
// 发布事件
bus.emit('temperatureChanged', 26);
避坑指南:我曾经在一个项目里忘了取消订阅,结果事件回调越积越多,内存泄漏了。记得在组件销毁时调用 off()。
事件优先级
座舱里有些事件需要优先处理。比如安全相关的告警,必须排在音乐切换前面。
| 优先级 | 事件类型 | 示例 |
|---|---|---|
| 高 | 安全告警 | 碰撞预警、车道偏离 |
| 中 | 用户操作 | 触摸、语音指令 |
| 低 | 后台更新 | 地图数据下载 |
实现优先级队列也不难,给每个事件加个 priority 字段就行。
状态管理基础:别让状态「跑偏」
座舱的状态,说白了就是「当前系统是什么样子」。空调温度、导航目的地、音乐播放列表……这些都是状态。
状态管理最怕什么?最怕多个地方同时改同一个状态,最后谁也不知道真实值是多少。
单一数据源
我坚持一个原则:所有状态只存一份。
比如空调温度,只在状态管理里存一次。UI 显示、语音反馈、硬件控制,都从同一个地方读。
// 状态管理
class StateManager {
constructor() {
this.state = {
climate: { temp: 24, fanSpeed: 2 },
media: { track: 'Song A', volume: 50 },
nav: { destination: null }
};
this.listeners = {};
}
get(key) {
return this.state[key];
}
set(key, value) {
this.state[key] = { ...this.state[key], ...value };
this.notify(key, this.state[key]);
}
subscribe(key, callback) {
if (!this.listeners[key]) {
this.listeners[key] = [];
}
this.listeners[key].push(callback);
}
notify(key, data) {
(this.listeners[key] || []).forEach(cb => cb(data));
}
}
关键点:状态变更必须通过 set 方法,不能直接修改 state 对象。这样才能触发通知,让 UI 更新。
状态变更的「原子性」
有时候一个操作要改多个状态。比如「开启空调」这个动作,要同时改温度、风速、模式。
我建议把这些操作封装成一个 action:
function turnOnAC(stateManager) {
stateManager.set('climate', {
temp: 24,
fanSpeed: 2,
mode: 'cool',
power: 'on'
});
}
这样做的好处是:要么全部改成功,要么一个都不改。不会出现「温度改了但风速没改」的尴尬情况。
我的习惯:每个 action 只做一件事。如果「开启空调」和「设置温度」是两件事,就拆成两个 action。这样复用性更高。
状态持久化
用户熄火再启动,希望空调还是上次的温度。这就涉及到状态持久化。
我一般把状态分成两类:
- 持久状态:需要保存的,比如空调偏好、座椅位置。存到本地存储或云端。
- 临时状态:不需要保存的,比如当前播放进度、导航路线。熄火就丢。
分清楚这两类,能省不少存储空间和读写时间。
我曾经踩过的坑:把所有状态都持久化了。结果每次启动都要读一堆数据,启动速度慢得离谱。后来只持久化了用户偏好,启动快了 3 倍。
小结
交互逻辑分层、事件驱动、状态管理,这三块是座舱交互的基石。
分层让代码不乱,事件驱动让响应及时,状态管理让数据可靠。三者配合好了,座舱交互才能流畅又稳定。
下一章,我会聊聊「用户动线设计」——怎么让用户的操作路径最短、最自然。到时候见。