JSBox 事件驱动架构:优雅处理界面更新
在复杂的 JSBox 应用中,界面更新往往涉及多个组件之间的协调。事件驱动架构提供了一种优雅的解决方案,让 UI 更新变得更加可控、可维护。让我们深入探讨如何构建一个强大的事件驱动 UI 系统。
一、为什么需要事件驱动的 UI 架构
1. 传统方式的痛点
在没有事件驱动架构的情况下,UI 更新通常是直接调用:
// 传统方式:直接更新UI
function updateUserInfo(user) {
$("userName").text = user.name;
$("userAvatar").src = user.avatar;
$("userScore").text = user.score;
// 如果有多处需要更新...
updateSidebar(user);
updateHeader(user);
refreshStatistics(user);
// 代码耦合严重,难以维护
}
这种方式存在以下问题:
- 耦合度高:业务逻辑与 UI 更新混杂
- 难以扩展:添加新的 UI 组件需要修改多处代码
- 状态同步困难:多个组件显示同一数据时容易不一致
- 调试困难:难以追踪 UI 更新的来源和时机
2. 事件驱动的优势
事件驱动架构通过发布-订阅模式解耦了数据变化和 UI 更新:
// 事件驱动方式
eventBus.emit('user:updated', user);
// UI组件自行决定如何响应这个事件
优势包括:
- 松耦合:数据层不需要知道有哪些 UI 需要更新
- 易扩展:新增 UI 组件只需订阅相应事件
- 统一管理:所有 UI 更新通过事件流转,便于监控和调试
- 异步友好:天然支持异步更新,避免阻塞
二、构建 UI 事件系统
1. 核心事件总线
首先,我们需要一个强大的事件总线作为整个系统的核心:
class UIEventBus {
constructor() {
this.events = new Map();
this.eventHistory = [];
this.maxHistorySize = 100;
this.debug = false;
}
// 订阅事件
on(eventName, handler, options = {}) {
const {
priority = 0, // 优先级
once = false, // 是否只执行一次
context = null // 执行上下文
} = options;
if (!this.events.has(eventName)) {
this.events.set(eventName, []);
}
const wrapper = {
handler,
priority,
once,
context,
id: Date.now() + Math.random()
};
// 按优先级插入
const handlers = this.events.get(eventName);
const insertIndex = handlers.findIndex(h => h.priority < priority);
if (insertIndex === -1) {
handlers.push(wrapper);
} else {
handlers.splice(insertIndex, 0, wrapper);
}
// 返回取消订阅函数
return () => this.off(eventName, wrapper.id);
}
// 取消订阅
off(eventName, handlerId) {
if (!this.events.has(eventName)) return;
const handlers = this.events.get(eventName);
const index = handlers.findIndex(h => h.id === handlerId);
if (index !== -1) {
handlers.splice(index, 1);
}
// 清理空数组
if (handlers.length === 0) {
this.events.delete(eventName);
}
}
// 发射事件
emit(eventName, data) {
// 记录事件历史
if (this.debug) {
this.recordEvent(eventName, data);
}
if (!this.events.has(eventName)) return;
const handlers = [...this.events.get(eventName)];
handlers.forEach(wrapper => {
try {
// 在指定上下文中执行
const result = wrapper.context ?
wrapper.handler.call(wrapper.context, data) :
wrapper.handler(data);
// 处理异步handler
if (result instanceof Promise) {
result.catch(error => {
console.error(`Event handler error for ${eventName}:`, error);
});
}
// 如果是一次性事件,执行后移除
if (wrapper.once) {
this.off(eventName, wrapper.id);
}
} catch (error) {
console.error(`Event handler error for ${eventName}:`, error);
}
});
}
// 异步发射事件(等待所有handler完成)
async emitAsync(eventName, data) {
if (!this.events.has(eventName)) return;
const handlers = [...this.events.get(eventName)];
for (const wrapper of handlers) {
try {
await Promise.resolve(
wrapper.context ?
wrapper.handler.call(wrapper.context, data) :
wrapper.handler(data)
);
if (wrapper.once) {
this.off(eventName, wrapper.id);
}
} catch (error) {
console.error(`Async event handler error for ${eventName}:`, error);
}
}
}
// 记录事件历史(用于调试)
recordEvent(eventName, data) {
this.eventHistory.push({
eventName,
data: JSON.parse(JSON.stringify(data || {})),
timestamp: Date.now(),
handlers: this.events.get(eventName)?.length || 0
});
// 限制历史记录大小
if (this.eventHistory.length > this.maxHistorySize) {
this.eventHistory.shift();
}
}
// 获取事件历史
getEventHistory(eventName = null) {
if (eventName) {
return this.eventHistory.filter(e => e.eventName === eventName);
}
return [...this.eventHistory];
}
}
2. UI 组件基类
为了让 UI 组件更好地集成事件系统,我们创建一个基类:
class UIComponent {
constructor(id, eventBus) {
this.id = id;
this.eventBus = eventBus;
this.subscriptions = [];
this.state = {};
this.initialized = false;
}
// 订阅事件的便捷方法
subscribe(eventName, handler, options = {}) {
const unsubscribe = this.eventBus.on(eventName, handler.bind(this), {
...options,
context: this
});
this.subscriptions.push(unsubscribe);
return unsubscribe;
}
// 发射事件的便捷方法
emit(eventName, data) {
this.eventBus.emit(eventName, {
...data,
source: this.id,
timestamp: Date.now()
});
}
// 更新组件状态
setState(updates) {
const oldState = { ...this.state };
this.state = { ...this.state, ...updates };
// 触发状态变化事件
this.emit('component:stateChanged', {
componentId: this.id,
oldState,
newState: this.state,
changes: updates
});
// 触发UI更新
this.render();
}
// 批量更新状态
batchUpdate(updater) {
const updates = {};
const proxy = new Proxy(updates, {
set: (target, prop, value) => {
target[prop] = value;
return true;
}
});
updater(proxy);
this.setState(updates);
}
// 渲染UI(子类实现)
render() {
// 子类覆盖此方法
}
// 销毁组件
destroy() {
// 取消所有事件订阅
this.subscriptions.forEach(unsubscribe => unsubscribe());
this.subscriptions = [];
// 发射销毁事件
this.emit('component:destroyed', { componentId: this.id });
}
}
3. 实际 UI 组件示例
让我们创建一些实际的 UI 组件来展示如何使用这个系统:
// 用户信息组件
class UserInfoComponent extends UIComponent {
constructor(eventBus) {
super('userInfo', eventBus);
this.setupEventListeners();
this.createUI();
}
setupEventListeners() {
// 监听用户数据更新
this.subscribe('user:updated', (data) => {
this.setState({
user: data.user
});
});
// 监听用户登出
this.subscribe('user:logout', () => {
this.setState({
user: null
});
});
}
createUI() {
$ui.render({
views: [{
type: "view",
props: {
id: "userInfoContainer"
},
layout: $layout.fill,
views: [
{
type: "image",
props: {
id: "userAvatar",
radius: 25
},
layout: (make, view) => {
make.left.top.inset(10);
make.size.equalTo($size(50, 50));
}
},
{
type: "label",
props: {
id: "userName",
font: $font("bold", 16)
},
layout: (make, view) => {
make.left.equalTo($("userAvatar").right).offset(10);
make.top.equalTo($("userAvatar"));
}
},
{
type: "label",
props: {
id: "userScore",
font: $font(14),
textColor: $color("gray")
},
layout: (make, view) => {
make.left.equalTo($("userName"));
make.top.equalTo($("userName").bottom).offset(5);
}
}
]
}]
});
}
render() {
const { user } = this.state;
if (user) {
$("userAvatar").src = user.avatar;
$("userName").text = user.name;
$("userScore").text = `积分: ${user.score}`;
$("userInfoContainer").hidden = false;
} else {
$("userInfoContainer").hidden = true;
}
}
}
// 消息列表组件
class MessageListComponent extends UIComponent {
constructor(eventBus) {
super('messageList', eventBus);
this.state = {
messages: [],
filter: 'all'
};
this.setupEventListeners();
this.createUI();
}
setupEventListeners() {
// 新消息
this.subscribe('message:new', (data) => {
this.addMessage(data.message);
});
// 删除消息
this.subscribe('message:delete', (data) => {
this.removeMessage(data.messageId);
});
// 标记已读
this.subscribe('message:markRead', (data) => {
this.markAsRead(data.messageId);
});
// 过滤器变化
this.subscribe('filter:changed', (data) => {
this.setState({ filter: data.filter });
});
}
addMessage(message) {
this.setState({
messages: [message, ...this.state.messages]
});
// 发射消息数量变化事件
this.emit('messages:countChanged', {
count: this.state.messages.length,
unreadCount: this.getUnreadCount()
});
}
removeMessage(messageId) {
this.setState({
messages: this.state.messages.filter(m => m.id !== messageId)
});
}
markAsRead(messageId) {
const messages = this.state.messages.map(m =>
m.id === messageId ? { ...m, read: true } : m
);
this.setState({ messages });
}
getUnreadCount() {
return this.state.messages.filter(m => !m.read).length;
}
getFilteredMessages() {
const { messages, filter } = this.state;
switch (filter) {
case 'unread':
return messages.filter(m => !m.read);
case 'starred':
return messages.filter(m => m.starred);
default:
return messages;
}
}
createUI() {
$ui.render({
views: [{
type: "list",
props: {
id: "messageList",
rowHeight: 80,
template: {
views: [
{
type: "label",
props: {
id: "title",
font: $font("bold", 16)
},
layout: (make, view) => {
make.left.top.inset(10);
make.right.inset(50);
}
},
{
type: "label",
props: {
id: "content",
font: $font(14),
lines: 2
},
layout: (make, view) => {
make.left.right.equalTo($("title"));
make.top.equalTo($("title").bottom).offset(5);
}
},
{
type: "view",
props: {
id: "unreadDot",
bgcolor: $color("red"),
circular: true
},
layout: (make, view) => {
make.right.inset(15);
make.centerY.equalTo(view.super);
make.size.equalTo($size(8, 8));
}
}
]
},
actions: [
{
title: "删除",
color: $color("red"),
handler: (sender, indexPath) => {
const message = this.getFilteredMessages()[indexPath.row];
this.emit('message:delete', { messageId: message.id });
}
},
{
title: "标记已读",
handler: (sender, indexPath) => {
const message = this.getFilteredMessages()[indexPath.row];
this.emit('message:markRead', { messageId: message.id });
}
}
]
},
layout: $layout.fill,
events: {
didSelect: (sender, indexPath, data) => {
const message = this.getFilteredMessages()[indexPath.row];
this.emit('message:selected', { message });
}
}
}]
});
}
render() {
const filteredMessages = this.getFilteredMessages();
$("messageList").data = filteredMessages.map(message => ({
title: { text: message.title },
content: { text: message.content },
unreadDot: { hidden: message.read }
}));
}
}
三、高级 UI 更新模式
1. 状态管理器
为了更好地管理应用状态,我们创建一个中央状态管理器:
class StateManager {
constructor(eventBus) {
this.eventBus = eventBus;
this.state = {};
this.computedValues = new Map();
this.watchers = new Map();
}
// 设置状态
setState(path, value) {
const oldValue = this.getState(path);
// 使用路径设置嵌套值
const keys = path.split('.');
let current = this.state;
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) {
current[keys[i]] = {};
}
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
// 触发状态变化事件
this.eventBus.emit('state:changed', {
path,
oldValue,
newValue: value
});
// 执行相关的watchers
this.executeWatchers(path);
// 更新计算属性
this.updateComputedValues();
}
// 获取状态
getState(path) {
const keys = path.split('.');
let current = this.state;
for (const key of keys) {
if (current[key] === undefined) {
return undefined;
}
current = current[key];
}
return current;
}
// 监听状态变化
watch(path, handler) {
if (!this.watchers.has(path)) {
this.watchers.set(path, []);
}
this.watchers.get(path).push(handler);
// 返回取消监听函数
return () => {
const handlers = this.watchers.get(path);
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
};
}
// 定义计算属性
computed(name, getter) {
this.computedValues.set(name, {
getter,
cache: null,
dirty: true
});
}
// 获取计算属性值
getComputed(name) {
const computed = this.computedValues.get(name);
if (!computed) return undefined;
if (computed.dirty) {
computed.cache = computed.getter(this.state);
computed.dirty = false;
}
return computed.cache;
}
// 执行watchers
executeWatchers(path) {
// 执行精确匹配的watchers
const exactWatchers = this.watchers.get(path) || [];
exactWatchers.forEach(handler => handler(this.getState(path)));
// 执行父路径的watchers
const keys = path.split('.');
for (let i = keys.length - 1; i > 0; i--) {
const parentPath = keys.slice(0, i).join('.');
const parentWatchers = this.watchers.get(parentPath) || [];
parentWatchers.forEach(handler => handler(this.getState(parentPath)));
}
}
// 更新所有计算属性
updateComputedValues() {
this.computedValues.forEach(computed => {
computed.dirty = true;
});
}
}
// 使用示例
const stateManager = new StateManager(eventBus);
// 定义计算属性
stateManager.computed('totalUnread', (state) => {
return (state.messages || []).filter(m => !m.read).length;
});
stateManager.computed('activeUserName', (state) => {
return state.user ? state.user.name : '未登录';
});
// 监听状态变化
stateManager.watch('user', (user) => {
if (user) {
eventBus.emit('user:updated', { user });
} else {
eventBus.emit('user:logout');
}
});
// 在组件中使用
class HeaderComponent extends UIComponent {
constructor(eventBus, stateManager) {
super('header', eventBus);
this.stateManager = stateManager;
this.setupWatchers();
}
setupWatchers() {
// 监听计算属性变化
this.stateManager.watch('messages', () => {
const unreadCount = this.stateManager.getComputed('totalUnread');
this.updateBadge(unreadCount);
});
}
updateBadge(count) {
$("messageBadge").text = count > 0 ? String(count) : "";
$("messageBadge").hidden = count === 0;
}
}
2. UI 更新队列
为了优化性能,我们实现一个 UI 更新队列,批量处理更新:
class UIUpdateQueue {
constructor() {
this.queue = [];
this.isProcessing = false;
this.batchDelay = 16; // 约等于60fps
}
// 添加更新任务
enqueue(component, updateFn, priority = 0) {
const task = {
component,
updateFn,
priority,
timestamp: Date.now()
};
// 按优先级插入
const insertIndex = this.queue.findIndex(t => t.priority < priority);
if (insertIndex === -1) {
this.queue.push(task);
} else {
this.queue.splice(insertIndex, 0, task);
}
// 开始处理队列
if (!this.isProcessing) {
this.scheduleProcessing();
}
}
// 调度处理
scheduleProcessing() {
this.isProcessing = true;
// 使用requestAnimationFrame或setTimeout
$delay(this.batchDelay / 1000, () => {
this.processQueue();
});
}
// 处理队列
processQueue() {
const startTime = Date.now();
const timeLimit = 16; // 单帧时间限制
while (this.queue.length > 0) {
// 检查时间限制
if (Date.now() - startTime > timeLimit) {
// 超时,下一帧继续
this.scheduleProcessing();
return;
}
const task = this.queue.shift();
try {
task.updateFn.call(task.component);
} catch (error) {
console.error(`UI update error in ${task.component.id}:`, error);
}
}
this.isProcessing = false;
}
// 清空队列
clear() {
this.queue = [];
this.isProcessing = false;
}
}
// 全局更新队列
const uiUpdateQueue = new UIUpdateQueue();
// 修改UIComponent基类,使用更新队列
class UIComponentWithQueue extends UIComponent {
render() {
// 将更新加入队列而不是立即执行
uiUpdateQueue.enqueue(this, this._render, this.renderPriority || 0);
}
_render() {
// 实际的渲染逻辑
}
// 立即渲染(跳过队列)
forceRender() {
this._render();
}
}
3. 虚拟 DOM 实现
对于复杂的列表更新,我们可以实现一个简单的虚拟 DOM:
class VirtualDOM {
constructor() {
this.currentTree = null;
}
// 创建虚拟节点
createElement(type, props, children) {
return {
type,
props: props || {},
children: children || []
};
}
// 比较两个虚拟节点
diff(oldNode, newNode) {
const patches = [];
if (!oldNode) {
patches.push({ type: 'CREATE', newNode });
} else if (!newNode) {
patches.push({ type: 'REMOVE' });
} else if (oldNode.type !== newNode.type) {
patches.push({ type: 'REPLACE', newNode });
} else {
// 比较属性
const propPatches = this.diffProps(oldNode.props, newNode.props);
if (propPatches.length > 0) {
patches.push({ type: 'UPDATE_PROPS', props: propPatches });
}
// 比较子节点
const childPatches = this.diffChildren(oldNode.children, newNode.children);
if (childPatches.length > 0) {
patches.push({ type: 'UPDATE_CHILDREN', children: childPatches });
}
}
return patches;
}
// 比较属性
diffProps(oldProps, newProps) {
const patches = [];
const allKeys = new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);
allKeys.forEach(key => {
if (oldProps[key] !== newProps[key]) {
patches.push({ key, value: newProps[key] });
}
});
return patches;
}
// 比较子节点
diffChildren(oldChildren, newChildren) {
const patches = [];
const maxLength = Math.max(oldChildren.length, newChildren.length);
for (let i = 0; i < maxLength; i++) {
const childPatches = this.diff(oldChildren[i], newChildren[i]);
if (childPatches.length > 0) {
patches.push({ index: i, patches: childPatches });
}
}
return patches;
}
// 应用补丁到真实DOM
applyPatches(element, patches) {
patches.forEach(patch => {
switch (patch.type) {
case 'UPDATE_PROPS':
patch.props.forEach(prop => {
element.props[prop.key] = prop.value;
});
break;
case 'UPDATE_CHILDREN':
// 递归应用子节点补丁
patch.children.forEach(child => {
const childElement = element.views[child.index];
if (childElement) {
this.applyPatches(childElement, child.patches);
}
});
break;
// 其他补丁类型...
}
});
}
}
// 在列表组件中使用虚拟DOM
class OptimizedListComponent extends UIComponent {
constructor(eventBus) {
super('optimizedList', eventBus);
this.virtualDOM = new VirtualDOM();
this.items = [];
}
updateItems(newItems) {
const oldTree = this.createVirtualTree(this.items);
const newTree = this.createVirtualTree(newItems);
const patches = this.virtualDOM.diff(oldTree, newTree);
// 只更新变化的部分
this.applyListPatches(patches);
this.items = newItems;
}
createVirtualTree(items) {
return this.virtualDOM.createElement('list', {},
items.map(item =>
this.virtualDOM.createElement('item', {
id: item.id,
title: item.title,
selected: item.selected
})
)
);
}
}
四、实战案例:聊天应用
让我们通过一个完整的聊天应用示例,展示事件驱动架构的威力:
// 聊天应用主类
class ChatApp {
constructor() {
this.eventBus = new UIEventBus();
this.stateManager = new StateManager(this.eventBus);
this.components = new Map();
this.initializeState();
this.createComponents();
this.setupEventHandlers();
}
initializeState() {
// 初始化应用状态
this.stateManager.setState('currentUser', null);
this.stateManager.setState('conversations', []);
this.stateManager.setState('activeConversation', null);
this.stateManager.setState('messages', {});
this.stateManager.setState('typing', {});
}
createComponents() {
// 创建各个UI组件
this.components.set('sidebar', new ConversationListComponent(this.eventBus, this.stateManager));
this.components.set('chatView', new ChatViewComponent(this.eventBus, this.stateManager));
this.components.set('inputBar', new MessageInputComponent(this.eventBus, this.stateManager));
this.components.set('header', new ChatHeaderComponent(this.eventBus, this.stateManager));
}
setupEventHandlers() {
// WebSocket连接
this.websocket = new WebSocketManager(this.eventBus);
// 处理接收到的消息
this.eventBus.on('ws:message', (data) => {
const { type, payload } = data;
switch (type) {
case 'new_message':
this.handleNewMessage(payload);
break;
case 'user_typing':
this.handleUserTyping(payload);
break;
case 'user_online':
this.handleUserOnline(payload);
break;
}
});
}
handleNewMessage(message) {
const conversationId = message.conversationId;
const messages = this.stateManager.getState(`messages.${conversationId}`) || [];
// 添加新消息
messages.push(message);
this.stateManager.setState(`messages.${conversationId}`, messages);
// 更新会话最后消息
const conversations = this.stateManager.getState('conversations');
const updatedConversations = conversations.map(conv =>
conv.id === conversationId ?
{ ...conv, lastMessage: message, unreadCount: conv.unreadCount + 1 } :
conv
);
this.stateManager.setState('conversations', updatedConversations);
// 发送通知
if (message.userId !== this.stateManager.getState('currentUser.id')) {
this.showNotification(message);
}
}
}
// 会话列表组件
class ConversationListComponent extends UIComponent {
constructor(eventBus, stateManager) {
super('conversationList', eventBus);
this.stateManager = stateManager;
this.setupUI();
this.setupEventListeners();
}
setupEventListeners() {
// 监听会话列表变化
this.stateManager.watch('conversations', (conversations) => {
this.updateConversationList(conversations);
});
// 监听活动会话变化
this.stateManager.watch('activeConversation', (conversationId) => {
this.highlightActiveConversation(conversationId);
});
}
setupUI() {
// 创建会话列表UI
$ui.push({
views: [{
type: "list",
props: {
id: "conversationList",
rowHeight: 70,
template: {
views: [
{
type: "image",
props: {
id: "avatar",
radius: 20
},
layout: (make) => {
make.left.inset(10);
make.centerY.equalTo();
make.size.equalTo($size(40, 40));
}
},
{
type: "label",
props: {
id: "name",
font: $font("bold", 16)
},
layout: (make) => {
make.left.equalTo($("avatar").right).offset(10);
make.top.inset(10);
make.right.inset(60);
}
},
{
type: "label",
props: {
id: "lastMessage",
font: $font(14),
textColor: $color("gray")
},
layout: (make) => {
make.left.equalTo($("name"));
make.top.equalTo($("name").bottom).offset(5);
make.right.inset(60);
}
},
{
type: "label",
props: {
id: "time",
font: $font(12),
textColor: $color("gray"),
align: $align.right
},
layout: (make) => {
make.right.inset(10);
make.top.inset(10);
}
},
{
type: "label",
props: {
id: "badge",
font: $font(12),
textColor: $color("white"),
bgcolor: $color("red"),
circular: true,
align: $align.center
},
layout: (make) => {
make.right.inset(10);
make.bottom.inset(10);
make.size.equalTo($size(20, 20));
}
}
]
}
},
layout: $layout.fill,
events: {
didSelect: (sender, indexPath, data) => {
const conversation = this.conversations[indexPath.row];
this.emit('conversation:selected', { conversationId: conversation.id });
}
}
}]
});
}
updateConversationList(conversations) {
this.conversations = conversations;
$("conversationList").data = conversations.map(conv => ({
avatar: { src: conv.avatar },
name: { text: conv.name },
lastMessage: { text: conv.lastMessage?.text || "" },
time: { text: this.formatTime(conv.lastMessage?.timestamp) },
badge: {
text: conv.unreadCount > 0 ? String(conv.unreadCount) : "",
hidden: conv.unreadCount === 0
}
}));
}
formatTime(timestamp) {
if (!timestamp) return "";
const now = Date.now();
const diff = now - timestamp;
if (diff < 60000) return "刚刚";
if (diff < 3600000) return Math.floor(diff / 60000) + "分钟前";
if (diff < 86400000) return Math.floor(diff / 3600000) + "小时前";
return new Date(timestamp).toLocaleDateString();
}
}
// 聊天视图组件
class ChatViewComponent extends UIComponent {
constructor(eventBus, stateManager) {
super('chatView', eventBus);
this.stateManager = stateManager;
this.messageCache = new Map();
this.setupUI();
this.setupEventListeners();
}
setupEventListeners() {
// 监听活动会话变化
this.stateManager.watch('activeConversation', (conversationId) => {
if (conversationId) {
this.loadMessages(conversationId);
}
});
// 监听新消息
this.eventBus.on('message:new', (data) => {
if (data.conversationId === this.stateManager.getState('activeConversation')) {
this.appendMessage(data.message);
}
});
// 监听输入状态
this.stateManager.watch('typing', (typingUsers) => {
this.updateTypingIndicator(typingUsers);
});
}
appendMessage(message) {
// 使用虚拟滚动优化大量消息
const messageElement = this.createMessageElement(message);
// 添加动画效果
messageElement.alpha = 0;
$("messageContainer").add(messageElement);
$ui.animate({
duration: 0.3,
animation: () => {
messageElement.alpha = 1;
},
completion: () => {
// 滚动到底部
this.scrollToBottom();
}
});
}
createMessageElement(message) {
const isCurrentUser = message.userId === this.stateManager.getState('currentUser.id');
return {
type: "view",
props: {
id: `message_${message.id}`
},
layout: (make) => {
make.left.right.inset(10);
make.height.equalTo(80);
},
views: [
{
type: "view",
props: {
bgcolor: isCurrentUser ? $color("#007AFF") : $color("#E5E5EA"),
radius: 16
},
layout: (make) => {
if (isCurrentUser) {
make.right.inset(0);
} else {
make.left.inset(0);
}
make.top.bottom.inset(5);
make.width.lessThanOrEqualTo(250);
},
views: [
{
type: "label",
props: {
text: message.text,
textColor: isCurrentUser ? $color("white") : $color("black"),
lines: 0
},
layout: (make) => {
make.edges.inset(10);
}
}
]
}
]
};
}
}
// WebSocket管理器
class WebSocketManager {
constructor(eventBus) {
this.eventBus = eventBus;
this.ws = null;
this.reconnectAttempts = 0;
this.messageQueue = [];
this.connect();
}
connect() {
this.ws = new WebSocket('wss://chat.example.com');
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
// 发送队列中的消息
this.flushMessageQueue();
this.eventBus.emit('ws:connected');
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.eventBus.emit('ws:message', data);
};
this.ws.onclose = () => {
console.log('WebSocket disconnected');
this.eventBus.emit('ws:disconnected');
// 自动重连
this.scheduleReconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.eventBus.emit('ws:error', error);
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
// 加入队列等待连接
this.messageQueue.push(data);
}
}
flushMessageQueue() {
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.send(message);
}
}
scheduleReconnect() {
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
this.reconnectAttempts++;
setTimeout(() => {
console.log(`Attempting to reconnect... (attempt ${this.reconnectAttempts})`);
this.connect();
}, delay);
}
}
五、性能优化技巧
1. 事件节流和防抖
对于频繁触发的事件,使用节流和防抖优化性能:
class EventOptimizer {
constructor(eventBus) {
this.eventBus = eventBus;
this.throttleTimers = new Map();
this.debounceTimers = new Map();
}
// 节流
throttle(eventName, handler, delay = 100) {
return this.eventBus.on(eventName, (data) => {
const key = `${eventName}_throttle`;
if (!this.throttleTimers.has(key)) {
handler(data);
this.throttleTimers.set(key, setTimeout(() => {
this.throttleTimers.delete(key);
}, delay));
}
});
}
// 防抖
debounce(eventName, handler, delay = 300) {
return this.eventBus.on(eventName, (data) => {
const key = `${eventName}_debounce`;
if (this.debounceTimers.has(key)) {
clearTimeout(this.debounceTimers.get(key));
}
this.debounceTimers.set(key, setTimeout(() => {
handler(data);
this.debounceTimers.delete(key);
}, delay));
});
}
}
// 使用示例
const optimizer = new EventOptimizer(eventBus);
// 搜索输入防抖
optimizer.debounce('search:input', async (data) => {
const results = await searchAPI(data.query);
eventBus.emit('search:results', { results });
}, 500);
// 滚动事件节流
optimizer.throttle('scroll:update', (data) => {
updateScrollPosition(data.position);
}, 100);
2. 事件委托
对于大量相似元素的事件处理,使用事件委托:
class EventDelegator {
constructor(container, eventBus) {
this.container = container;
this.eventBus = eventBus;
this.handlers = new Map();
this.setupDelegation();
}
setupDelegation() {
// 委托点击事件
this.container.addEventListener('click', (event) => {
this.handleEvent('click', event);
});
// 委托其他事件...
}
handleEvent(type, event) {
let target = event.target;
// 向上查找匹配的元素
while (target && target !== this.container) {
const handlers = this.handlers.get(`${type}:${target.className}`);
if (handlers) {
handlers.forEach(handler => {
handler.call(target, event);
});
if (!event.defaultPrevented) {
event.stopPropagation();
}
}
target = target.parentElement;
}
}
on(selector, eventType, handler) {
const key = `${eventType}:${selector}`;
if (!this.handlers.has(key)) {
this.handlers.set(key, []);
}
this.handlers.get(key).push(handler);
}
}
// 使用示例
const delegator = new EventDelegator($("listContainer"), eventBus);
// 为所有列表项添加点击处理
delegator.on('list-item', 'click', function(event) {
const itemId = this.getAttribute('data-id');
eventBus.emit('item:selected', { itemId });
});
3. 懒加载和虚拟滚动
对于长列表,实现虚拟滚动以优化性能:
class VirtualScroller {
constructor(container, eventBus, options = {}) {
this.container = container;
this.eventBus = eventBus;
this.itemHeight = options.itemHeight || 50;
this.buffer = options.buffer || 5;
this.items = [];
this.visibleRange = { start: 0, end: 0 };
this.setupScrollListener();
}
setItems(items) {
this.items = items;
this.updateView();
}
setupScrollListener() {
let scrollTimer;
this.container.addEventListener('scroll', () => {
clearTimeout(scrollTimer);
scrollTimer = setTimeout(() => {
this.updateView();
}, 10);
});
}
updateView() {
const scrollTop = this.container.scrollTop;
const containerHeight = this.container.clientHeight;
// 计算可见范围
const start = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer);
const end = Math.min(
this.items.length,
Math.ceil((scrollTop + containerHeight) / this.itemHeight) + this.buffer
);
// 只在范围变化时更新
if (start !== this.visibleRange.start || end !== this.visibleRange.end) {
this.visibleRange = { start, end };
this.renderVisibleItems();
}
}
renderVisibleItems() {
const visibleItems = this.items.slice(
this.visibleRange.start,
this.visibleRange.end
);
// 发射更新事件
this.eventBus.emit('virtualScroll:update', {
visibleItems,
range: this.visibleRange,
totalHeight: this.items.length * this.itemHeight
});
}
}
六、调试和监控
1. 事件追踪器
创建一个事件追踪器来监控和调试事件流:
class EventTracker {
constructor(eventBus) {
this.eventBus = eventBus;
this.tracking = false;
this.events = [];
this.filters = new Set();
}
start() {
this.tracking = true;
// 拦截所有事件
const originalEmit = this.eventBus.emit;
this.eventBus.emit = (eventName, data) => {
if (this.tracking && this.shouldTrack(eventName)) {
this.recordEvent(eventName, data);
}
return originalEmit.call(this.eventBus, eventName, data);
};
}
stop() {
this.tracking = false;
}
shouldTrack(eventName) {
if (this.filters.size === 0) return true;
return this.filters.has(eventName);
}
recordEvent(eventName, data) {
const event = {
name: eventName,
data: JSON.parse(JSON.stringify(data || {})),
timestamp: Date.now(),
stack: new Error().stack
};
this.events.push(event);
// 实时显示
if (this.liveView) {
this.displayEvent(event);
}
}
displayEvent(event) {
console.log(`[${new Date(event.timestamp).toLocaleTimeString()}] ${event.name}`, event.data);
}
getReport() {
const report = {
totalEvents: this.events.length,
eventCounts: {},
timeline: []
};
this.events.forEach(event => {
report.eventCounts[event.name] = (report.eventCounts[event.name] || 0) + 1;
report.timeline.push({
time: event.timestamp,
name: event.name
});
});
return report;
}
exportToFile() {
const report = this.getReport();
const data = JSON.stringify(report, null, 2);
$share.sheet([data]);
}
}
// 使用示例
const tracker = new EventTracker(eventBus);
tracker.filters.add('user:updated');
tracker.filters.add('message:new');
tracker.liveView = true;
tracker.start();
// 稍后生成报告
setTimeout(() => {
const report = tracker.getReport();
console.log('Event Report:', report);
}, 60000);
2. 性能监控
监控 UI 更新性能:
class PerformanceMonitor {
constructor(eventBus) {
this.eventBus = eventBus;
this.metrics = new Map();
}
measureUIUpdate(componentId, updateFn) {
const startTime = performance.now();
const startMemory = performance.memory?.usedJSHeapSize;
const result = updateFn();
const endTime = performance.now();
const endMemory = performance.memory?.usedJSHeapSize;
const metrics = {
duration: endTime - startTime,
memoryDelta: endMemory - startMemory,
timestamp: Date.now()
};
// 记录指标
if (!this.metrics.has(componentId)) {
this.metrics.set(componentId, []);
}
this.metrics.get(componentId).push(metrics);
// 发送警告
if (metrics.duration > 16) { // 超过一帧时间
this.eventBus.emit('performance:warning', {
componentId,
duration: metrics.duration,
type: 'slowUpdate'
});
}
return result;
}
getAverageMetrics(componentId) {
const componentMetrics = this.metrics.get(componentId) || [];
if (componentMetrics.length === 0) return null;
const sum = componentMetrics.reduce((acc, m) => ({
duration: acc.duration + m.duration,
memoryDelta: acc.memoryDelta + m.memoryDelta
}), { duration: 0, memoryDelta: 0 });
return {
avgDuration: sum.duration / componentMetrics.length,
avgMemoryDelta: sum.memoryDelta / componentMetrics.length,
sampleCount: componentMetrics.length
};
}
}
七、最佳实践总结
1. 事件命名规范
使用清晰、一致的事件命名:
// 推荐的命名模式
const eventPatterns = {
// 实体:动作
'user:login': '用户登录',
'user:logout': '用户登出',
'user:updated': '用户信息更新',
// 组件:状态
'modal:opened': '模态框打开',
'modal:closed': '模态框关闭',
// 数据:操作
'data:loaded': '数据加载完成',
'data:error': '数据加载错误',
// 界面:交互
'ui:scroll': '界面滚动',
'ui:resize': '界面大小改变'
};
2. 错误处理策略
建立统一的错误处理机制:
class ErrorBoundary {
constructor(eventBus) {
this.eventBus = eventBus;
// 全局错误捕获
this.eventBus.on('*', this.handleError.bind(this));
}
handleError(data) {
if (data.error) {
console.error('Event error:', data.error);
// 显示用户友好的错误提示
this.showErrorNotification(data.error);
// 上报错误
this.reportError(data.error);
}
}
showErrorNotification(error) {
$ui.toast(`发生错误: ${error.message}`, 2);
}
reportError(error) {
// 发送错误报告到服务器
$http.post({
url: 'https://api.example.com/errors',
body: {
error: error.message,
stack: error.stack,
userAgent: $device.info,
timestamp: Date.now()
}
});
}
}
3. 内存管理
确保正确清理事件监听器和资源:
class ComponentLifecycle {
constructor(eventBus) {
this.eventBus = eventBus;
this.components = new WeakMap();
}
register(component) {
const lifecycle = {
subscriptions: [],
timers: [],
resources: []
};
this.components.set(component, lifecycle);
// 自动清理
component.destroy = () => {
this.cleanup(component);
};
}
cleanup(component) {
const lifecycle = this.components.get(component);
if (lifecycle) {
// 取消事件订阅
lifecycle.subscriptions.forEach(unsubscribe => unsubscribe());
// 清理定时器
lifecycle.timers.forEach(timer => clearTimeout(timer));
// 释放其他资源
lifecycle.resources.forEach(resource => resource.dispose());
this.components.delete(component);
}
}
}
结语
事件驱动架构为 JSBox 应用的 UI 更新提供了一个强大而灵活的解决方案。通过解耦数据变化和界面更新,我们可以构建更加可维护、可扩展的应用。
关键要点:
- 使用事件总线统一管理所有 UI 相关事件
- 组件化思维,每个 UI 组件独立订阅和处理事件
- 性能优化,使用批量更新、虚拟滚动等技术
- 调试工具,建立完善的事件追踪和性能监控
- 最佳实践,遵循命名规范,做好错误处理和内存管理
通过合理运用这些技术和模式,你可以构建出响应迅速、用户体验优秀的 JSBox 应用。记住,好的架构不是一蹴而就的,需要在实践中不断优化和完善。