当逻辑需回调函数时如何实现UI与逻辑解耦
好问题!你提到的松耦合原则完全正确——逻辑层绝对不该知晓GUI的具体细节,而UI按需触发逻辑才是合理的分工。针对chrome.serial这类异步回调式API的场景,我有几个实用的方案,既能保持两层的解耦,又能优雅地传递异步结果:
方案1:事件发布/订阅(Pub/Sub)模式
这是我最推荐的方式,完美契合松耦合的核心思想。逻辑层只负责发布事件,不用关心谁会接收;UI层则提前订阅感兴趣的事件,收到数据后再更新界面。
举个具体的实现例子:
// 先搞一个轻量的事件总线(可以单独抽成工具文件,复用性拉满) const EventBus = { listeners: {}, // 订阅事件 on(eventName, callback) { if (!this.listeners[eventName]) this.listeners[eventName] = []; this.listeners[eventName].push(callback); }, // 发布事件 emit(eventName, data) { this.listeners[eventName]?.forEach(callback => callback(data)); } }; // 逻辑层:串口服务类,完全不碰UI class SerialService { fetchDevices() { chrome.serial.getDevices((devices) => { // 拿到设备列表后,只发布事件,不管谁处理 EventBus.emit('serial:devices-fetched', devices); }); } } // UI层:初始化时订阅事件,按需触发逻辑 document.addEventListener('DOMContentLoaded', () => { // 订阅设备获取完成的事件 EventBus.on('serial:devices-fetched', (devices) => { // 这里只做UI相关的事,比如渲染列表 renderDeviceList(devices); }); // 用户点击按钮时,触发逻辑层的操作 document.getElementById('refresh-btn').addEventListener('click', () => { new SerialService().fetchDevices(); }); });
这种方式的好处是:逻辑层完全独立,就算换个UI框架(比如从原生JS换成React),逻辑层代码不用改一行;如果多个UI组件需要同一批数据,只要都订阅同一个事件就行,逻辑层不用做任何额外处理。
方案2:Promise封装异步API
把chrome.serial的回调式API封装成Promise,让逻辑层返回Promise对象,UI层通过async/await或.then()来处理结果。这种方式代码更简洁,也符合现代JS的异步编程习惯。
示例代码:
// 逻辑层:封装串口API为Promise class SerialService { async getDevices() { return new Promise((resolve) => { chrome.serial.getDevices((devices) => { resolve(devices); }); }); } } // UI层:调用逻辑层并处理结果 document.getElementById('refresh-btn').addEventListener('click', async () => { try { const serialService = new SerialService(); const devices = await serialService.getDevices(); renderDeviceList(devices); } catch (err) { // 优雅处理错误,比如显示提示 showError('获取设备列表失败,请检查串口连接'); } });
这里逻辑层依然完全不知道UI的存在,只是返回一个承载结果的Promise;UI层自己决定怎么处理数据和错误,完全符合松耦合的要求。
方案3:依赖注入通用回调(谨慎使用)
如果某些场景下需要更灵活的回调传递,你可以让UI层把回调函数作为参数传给逻辑层,但要注意:这个回调必须是通用的、不包含UI细节的,逻辑层只负责调用它并传递数据,不关心回调内部做什么。
示例:
// 逻辑层:只接受回调,不关心回调的具体实现 class SerialService { getDevices(onSuccess) { chrome.serial.getDevices((devices) => { onSuccess(devices); }); } } // UI层:传入自己的处理函数 document.getElementById('refresh-btn').addEventListener('click', () => { const serialService = new SerialService(); serialService.getDevices((devices) => { renderDeviceList(devices); // 逻辑层不知道这个函数是UI相关的 }); });
这个方案的耦合度比前两个稍高,适合单次调用、不需要多组件共享数据的场景。一定要避免让逻辑层依赖UI的任何具体实现(比如不要把DOM元素传给逻辑层)。
总结
优先推荐事件发布/订阅模式,尤其是当你的应用有多个UI组件需要共享异步数据时;如果是简单的单次调用场景,Promise封装会让代码更简洁。这两种方式都能严格遵守你提到的松耦合原则,不会出现回调满天飞的混乱情况。
内容的提问来源于stack exchange,提问作者Fii

