乐闻世界logo
搜索文章和话题

面试题手册

MobX 的依赖追踪系统是如何工作的?

MobX 的依赖追踪系统是其核心机制,它通过细粒度的追踪实现了高效的响应式更新。以下是 MobX 依赖追踪的详细工作原理:依赖追踪的基本原理MobX 使用观察者模式和依赖图来实现依赖追踪。当 observable 被访问时,MobX 会建立依赖关系;当 observable 被修改时,MobX 会通知所有依赖它的观察者。核心组件1. Reaction(反应)Reaction 是依赖追踪的执行单元,包括:autorun:立即执行,并在依赖变化时自动重新执行reaction:提供更细粒度的控制,可以指定追踪函数和效果函数observer(React 组件):包装 React 组件,使其能够响应状态变化computed:计算属性,也是一种特殊的 reaction2. Derivation(派生)Derivation 表示依赖于 observable 的计算或副作用。每个 derivation 维护一个依赖列表。3. Atom(原子)Atom 是最小的可观察单元,每个 observable 对象、数组、Map 等都由多个 atom 组成。依赖追踪的执行流程1. 追踪阶段(Tracing)当 reaction 执行时:autorun(() => { console.log(store.count); // 访问 observable});执行步骤:MobX 将当前 reaction 标记为"正在追踪"当访问 store.count 时,MobX 记录下这个 reaction 依赖于 count 这个 atom继续执行,记录所有访问的 observable执行完成后,reaction 进入"稳定"状态2. 通知阶段(Notification)当 observable 被修改时:runInAction(() => { store.count++; // 修改 observable});执行步骤:MobX 检测到 count atom 被修改查找所有依赖于 count 的 reaction将这些 reaction 标记为"过时"(stale)在下一个事件循环中,重新执行这些 reaction依赖图的结构MobX 维护一个双向的依赖图:Atom → Derivation:每个 atom 知道哪些 derivation 依赖于它Derivation → Atom:每个 derivation 知道自己依赖于哪些 atom这种双向关系使得 MobX 能够高效地进行依赖更新和清理。细粒度更新MobX 的依赖追踪是细粒度的,这意味着:只更新真正需要更新的部分避免不必要的重新计算和重新渲染自动处理嵌套的依赖关系示例:class Store { @observable firstName = 'John'; @observable lastName = 'Doe'; @observable age = 30; @computed get fullName() { return `${this.firstName} ${this.lastName}`; }}const observerComponent = observer(() => { // 只依赖 fullName,不依赖 age return <div>{store.fullName}</div>;});当 age 变化时,组件不会重新渲染;只有当 firstName 或 lastName 变化时才会重新渲染。批量更新MobX 会自动批量更新,避免多次触发 reaction:runInAction(() => { store.firstName = 'Jane'; store.lastName = 'Smith'; store.age = 25;});即使修改了多个 observable,相关的 reaction 只会执行一次。依赖清理当 reaction 不再需要时,MobX 会自动清理依赖关系:组件卸载时,observer 会自动清理使用 dispose() 方法手动清理 reaction避免内存泄漏性能优化MobX 的依赖追踪系统提供了多种性能优化:懒计算:computed 只在需要时才计算缓存机制:computed 的结果会被缓存批量更新:多个状态变化合并为一次更新细粒度追踪:只追踪真正需要的依赖调试依赖追踪MobX 提供了调试工具来查看依赖关系:import { trace } from 'mobx';// 追踪 computed 的依赖trace(store.fullName);// 追踪 reaction 的依赖autorun(() => { console.log(store.count);}, { name: 'myReaction' });常见问题1. 循环依赖MobX 能够检测和避免循环依赖,但设计时应尽量避免。2. 过度追踪避免在循环或条件中访问 observable,这可能导致不必要的依赖。3. 内存泄漏确保在组件卸载时清理 reaction,避免内存泄漏。总结MobX 的依赖追踪系统通过观察者模式和依赖图实现了高效的响应式更新。理解这个系统的工作原理有助于编写更高效的 MobX 代码,并避免常见的性能问题。
阅读 0·2月21日 15:45

如何测试 MobX 应用?

MobX 的测试策略和工具对于构建可靠的应用至关重要。以下是 MobX 测试的完整指南:1. 测试 Store基本测试import { UserStore } from './UserStore';describe('UserStore', () => { let store; beforeEach(() => { store = new UserStore(); }); it('should initialize with default values', () => { expect(store.user).toBeNull(); expect(store.isAuthenticated).toBe(false); }); it('should login user', async () => { await store.login({ username: 'test', password: 'test' }); expect(store.user).not.toBeNull(); expect(store.isAuthenticated).toBe(true); }); it('should logout user', () => { store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; store.logout(); expect(store.user).toBeNull(); expect(store.isAuthenticated).toBe(false); });});测试 computed 属性describe('ProductStore', () => { let store; beforeEach(() => { store = new ProductStore(); }); it('should compute featured products', () => { store.products = [ { id: 1, name: 'Product 1', featured: true }, { id: 2, name: 'Product 2', featured: false }, { id: 3, name: 'Product 3', featured: true } ]; expect(store.featuredProducts).toHaveLength(2); expect(store.featuredProducts[0].name).toBe('Product 1'); expect(store.featuredProducts[1].name).toBe('Product 3'); }); it('should update when products change', () => { store.products = [{ id: 1, name: 'Product 1', featured: true }]; expect(store.featuredProducts).toHaveLength(1); store.products.push({ id: 2, name: 'Product 2', featured: true }); expect(store.featuredProducts).toHaveLength(2); });});测试异步 actiondescribe('AsyncStore', () => { let store; let mockApi; beforeEach(() => { mockApi = { fetchData: jest.fn().mockResolvedValue({ data: 'test' }) }; store = new AsyncStore(mockApi); }); it('should fetch data successfully', async () => { await store.fetchData(); expect(store.data).toEqual({ data: 'test' }); expect(store.loading).toBe(false); expect(mockApi.fetchData).toHaveBeenCalled(); }); it('should handle errors', async () => { mockApi.fetchData.mockRejectedValue(new Error('Network error')); await expect(store.fetchData()).rejects.toThrow('Network error'); expect(store.error).toBe('Network error'); expect(store.loading).toBe(false); });});2. 测试 React 组件测试 observer 组件import { render, screen, fireEvent } from '@testing-library/react';import { observer } from 'mobx-react-lite';import { UserStore } from './UserStore';const TestComponent = observer(({ store }) => ( <div> {store.isAuthenticated ? ( <div>Welcome, {store.user?.name}</div> ) : ( <div>Please login</div> )} <button onClick={store.login}>Login</button> </div>));describe('TestComponent', () => { it('should show login message when not authenticated', () => { const store = new UserStore(); render(<TestComponent store={store} />); expect(screen.getByText('Please login')).toBeInTheDocument(); }); it('should show welcome message when authenticated', () => { const store = new UserStore(); store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; render(<TestComponent store={store} />); expect(screen.getByText('Welcome, Test')).toBeInTheDocument(); }); it('should update when state changes', () => { const store = new UserStore(); render(<TestComponent store={store} />); expect(screen.getByText('Please login')).toBeInTheDocument(); store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; expect(screen.getByText('Welcome, Test')).toBeInTheDocument(); });});测试表单组件describe('FormComponent', () => { it('should update form data', () => { const store = new FormStore(); render(<FormComponent store={store} />); const input = screen.getByLabelText('Name'); fireEvent.change(input, { target: { value: 'John' } }); expect(store.formData.name).toBe('John'); }); it('should submit form', async () => { const store = new FormStore(); store.submit = jest.fn(); render(<FormComponent store={store} />); const button = screen.getByText('Submit'); fireEvent.click(button); expect(store.submit).toHaveBeenCalled(); });});3. 使用 MobX 测试工具使用 spyimport { spy } from 'mobx';describe('Spy Usage', () => { it('should spy on observable changes', () => { const store = observable({ count: 0 }); const countSpy = jest.fn(); spy(store, 'count', (change) => { countSpy(change); }); store.count = 1; store.count = 2; expect(countSpy).toHaveBeenCalledTimes(2); });});使用 traceimport { trace } from 'mobx';describe('Trace Usage', () => { it('should trace computed dependencies', () => { const store = observable({ firstName: 'John', lastName: 'Doe' }); const fullName = computed(() => `${store.firstName} ${store.lastName}`); // 追踪依赖 trace(fullName); expect(fullName.get()).toBe('John Doe'); });});使用 isObservableimport { isObservable } from 'mobx';describe('IsObservable Usage', () => { it('should check if object is observable', () => { const observableObj = observable({ count: 0 }); const plainObj = { count: 0 }; expect(isObservable(observableObj)).toBe(true); expect(isObservable(plainObj)).toBe(false); });});4. Mock API 调用使用 Jest mockimport { UserStore } from './UserStore';describe('UserStore with API', () => { let store; let mockApi; beforeEach(() => { mockApi = { login: jest.fn(), logout: jest.fn(), fetchUser: jest.fn() }; store = new UserStore(mockApi); }); it('should call API on login', async () => { mockApi.login.mockResolvedValue({ id: 1, name: 'Test' }); await store.login({ username: 'test', password: 'test' }); expect(mockApi.login).toHaveBeenCalledWith({ username: 'test', password: 'test' }); }); it('should handle API errors', async () => { mockApi.login.mockRejectedValue(new Error('Invalid credentials')); await expect(store.login({ username: 'test', password: 'test' })) .rejects.toThrow('Invalid credentials'); expect(store.error).toBe('Invalid credentials'); });});使用 MSW (Mock Service Worker)import { setupServer, rest } from 'msw';import { UserStore } from './UserStore';const server = setupServer( rest.post('/api/login', (req, res, ctx) => { return res( ctx.status(200), ctx.json({ id: 1, name: 'Test' }) ); }));describe('UserStore with MSW', () => { let store; beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); beforeEach(() => { store = new UserStore(); }); it('should login successfully', async () => { await store.login({ username: 'test', password: 'test' }); expect(store.user).toEqual({ id: 1, name: 'Test' }); expect(store.isAuthenticated).toBe(true); });});5. 测试 reactiondescribe('Reaction Testing', () => { it('should trigger reaction when observable changes', () => { const store = observable({ count: 0 }); const reactionSpy = jest.fn(); reaction( () => store.count, (count) => { reactionSpy(count); } ); store.count = 1; expect(reactionSpy).toHaveBeenCalledWith(1); store.count = 2; expect(reactionSpy).toHaveBeenCalledWith(2); }); it('should not trigger when value is same', () => { const store = observable({ count: 0 }); const reactionSpy = jest.fn(); reaction( () => store.count, (count) => { reactionSpy(count); } ); store.count = 0; expect(reactionSpy).not.toHaveBeenCalled(); });});6. 测试中间件describe('Middleware Testing', () => { it('should call middleware before action', () => { const middlewareSpy = jest.fn(); const actionSpy = jest.fn(); const store = { @observable count: 0, @action increment() { this.count++; } }; const originalIncrement = store.increment; store.increment = function(...args) { middlewareSpy(...args); return originalIncrement.apply(this, args); }; store.increment(); expect(middlewareSpy).toHaveBeenCalled(); expect(actionSpy).toHaveBeenCalled(); });});7. 集成测试describe('Integration Tests', () => { it('should handle complete user flow', async () => { const store = new RootStore(); // 登录 await store.userStore.login({ username: 'test', password: 'test' }); expect(store.userStore.isAuthenticated).toBe(true); // 加载数据 await store.dataStore.loadData(); expect(store.dataStore.data).not.toBeNull(); // 添加到购物车 store.cartStore.addItem(store.dataStore.data[0]); expect(store.cartStore.items).toHaveLength(1); // 结账 await store.cartStore.checkout(); expect(store.cartStore.items).toHaveLength(0); });});8. 测试最佳实践1. 隔离测试// 每个测试都应该独立beforeEach(() => { store = new Store();});// 清理副作用afterEach(() => { if (store.dispose) { store.dispose(); }});2. 使用快照测试it('should match snapshot', () => { const store = new Store(); store.data = { id: 1, name: 'Test' }; expect(toJS(store.data)).toMatchSnapshot();});3. 测试边界情况it('should handle empty array', () => { const store = new Store(); store.items = []; expect(store.itemCount).toBe(0);});it('should handle null values', () => { const store = new Store(); store.user = null; expect(store.userName).toBe('Guest');});4. 测试错误处理it('should handle network errors gracefully', async () => { const store = new Store(); mockApi.fetchData.mockRejectedValue(new Error('Network error')); await expect(store.fetchData()).rejects.toThrow('Network error'); expect(store.error).toBe('Network error'); expect(store.loading).toBe(false);});总结MobX 测试的关键点:测试 Store:验证 observable、computed 和 action 的行为测试组件:验证 observer 组件的响应性使用测试工具:spy、trace、isObservableMock API:使用 Jest mock 或 MSW测试 reaction:验证副作用是否正确触发测试中间件:验证中间件是否正确执行集成测试:验证多个 store 之间的交互最佳实践:隔离测试、快照测试、边界情况、错误处理遵循这些测试策略,可以构建可靠、可维护的 MobX 应用。
阅读 0·2月21日 15:45

MQTT 的遗嘱消息(Last Will)是什么?如何使用?

MQTT 的遗嘱消息(Last Will and Testament,LWT)是一种重要的机制,用于在客户端异常断开连接时通知其他客户端。遗嘱消息的概念定义遗嘱消息是客户端在连接时预先设置的一条消息,当客户端异常断开连接时,Broker 会自动将这条消息发布到指定的主题。作用异常检测:通知其他客户端某个设备已离线状态通知:发布设备离线状态故障告警:触发告警机制资源清理:通知系统清理相关资源遗嘱消息的工作原理设置遗嘱消息客户端在发送 CONNECT 报文时设置遗嘱消息参数:CONNECT 报文参数:- Will Flag: true(启用遗嘱消息)- Will Topic: 遗嘱消息的主题- Will Message: 遗嘱消息的内容- Will QoS: 遗嘱消息的 QoS 级别- Will Retain: 是否保留遗嘱消息触发条件遗嘱消息在以下情况下会被触发:客户端异常断开网络故障设备断电程序崩溃连接超时Broker 检测到连接断开Keep Alive 超时TCP 连接断开心跳检测失败不触发的情况以下情况不会触发遗嘱消息:正常断开连接客户端发送 DISCONNECT 报文正常关闭连接连接未建立CONNECT 报文发送失败连接被拒绝遗嘱消息的参数Will Flag(遗嘱标志)作用:标识是否启用遗嘱消息值:true/false必需:启用遗嘱消息时必须为 trueWill Topic(遗嘱主题)作用:指定遗嘱消息发布的主题格式:标准的 MQTT 主题字符串示例:device/123/status要求:必须设置Will Message(遗嘱消息内容)作用:遗嘱消息的实际内容格式:二进制数据示例:offline 或 {"status":"offline","timestamp":1234567890}要求:必须设置Will QoS(遗嘱 QoS)作用:指定遗嘱消息的 QoS 级别值:0/1/2默认值:0选择建议:QoS 0:一般状态通知QoS 1:重要状态通知QoS 2:关键状态通知Will Retain(遗嘱保留)作用:指定是否保留遗嘱消息值:true/false默认值:false影响:true:新订阅者会收到遗嘱消息false:只有在线订阅者收到遗嘱消息使用场景1. 设备在线状态监控设备上线:- 发布 "online" 到 device/123/status设备离线(正常):- 发布 "offline" 到 device/123/status设备离线(异常):- 遗嘱消息 "offline" 发布到 device/123/status2. 故障告警遗嘱主题:alert/device/123遗嘱消息:{"type":"offline","device":"123","timestamp":1234567890}监控系统订阅 alert/device/123收到遗嘱消息后触发告警3. 资源清理遗嘱主题:cleanup/device/123遗嘱消息:{"device":"123","action":"cleanup"}清理服务订阅 cleanup/device/123收到遗嘱消息后清理相关资源4. 负载均衡遗嘱主题:worker/offline遗嘱消息:{"worker":"worker1"}负载均衡器订阅 worker/offline收到遗嘱消息后重新分配任务代码示例Python (paho-mqtt)import paho.mqtt.client as mqttimport jsonimport timedef on_connect(client, userdata, flags, rc): print(f"Connected with result code {rc}") client.subscribe("device/+/status")def on_message(client, userdata, msg): print(f"Received: {msg.topic} - {msg.payload.decode()}")client = mqtt.Client()# 设置遗嘱消息will_topic = "device/123/status"will_message = json.dumps({"status": "offline", "timestamp": int(time.time())})client.will_set(will_topic, will_message, qos=1, retain=True)client.on_connect = on_connectclient.on_message = on_messageclient.connect("broker.example.com", 1883, 60)# 发布在线状态client.publish("device/123/status", json.dumps({"status": "online"}))client.loop_forever()JavaScript (MQTT.js)const mqtt = require('mqtt');const client = mqtt.connect('mqtt://broker.example.com', { will: { topic: 'device/123/status', payload: JSON.stringify({ status: 'offline', timestamp: Date.now() }), qos: 1, retain: true }});client.on('connect', () => { console.log('Connected'); // 发布在线状态 client.publish('device/123/status', JSON.stringify({ status: 'online' })); // 订阅状态主题 client.subscribe('device/+/status');});client.on('message', (topic, message) => { console.log(`Received: ${topic} - ${message.toString()}`);});最佳实践1. 遗嘱消息设计简洁明了:消息内容简洁,易于解析包含时间戳:便于追踪离线时间设备标识:明确标识是哪个设备状态信息:包含详细的离线原因2. 主题命名规范推荐格式:- device/{device_id}/status- alert/{device_id}/offline- cleanup/{device_id}避免使用:- 通配符作为遗嘱主题- 过于复杂的主题结构3. QoS 选择一般设备:QoS 0重要设备:QoS 1关键设备:QoS 24. Retain 设置状态监控:建议设置为 true告警通知:建议设置为 false资源清理:根据需求设置5. 遗嘱消息处理及时处理:收到遗嘱消息后及时处理避免重复:防止重复处理同一设备的离线事件记录日志:记录离线事件,便于问题排查注意事项正常断开:正常断开连接时,应该先发送 DISCONNECT 报文,避免触发遗嘱消息遗嘱消息更新:重新连接时可以更新遗嘱消息内容Broker 限制:某些 Broker 可能对遗嘱消息有大小限制网络延迟:网络延迟可能导致遗嘱消息延迟发送多设备场景:在多设备场景中,需要明确区分不同设备的遗嘱消息遗嘱消息的局限性无法区分离线原因:遗嘱消息不包含具体的离线原因可能误报:网络抖动可能导致误报处理延迟:从离线到发送遗嘱消息可能有延迟依赖 Broker:完全依赖 Broker 的可靠性MQTT 遗嘱消息是物联网应用中非常重要的机制,合理使用可以有效监控设备状态,提高系统的可靠性和可维护性。
阅读 0·2月21日 15:45

MQTT 的发布/订阅模式是如何工作的?

MQTT 的发布/订阅模式是一种消息传递架构,它解耦了消息的生产者和消费者,实现了灵活的一对多通信。核心概念1. 主题(Topic)定义:主题是消息的路由地址,采用层级结构格式:使用斜杠(/)分隔的字符串,如 home/livingroom/temperature特点:层级清晰,便于组织和管理支持通配符订阅大小写敏感长度限制:最多 65535 字节2. 发布者(Publisher)角色:消息的生产者功能:向特定主题发送消息特点:不需要知道订阅者的存在可以同时向多个主题发布消息发布后立即返回,不等待订阅者响应3. 订阅者(Subscriber)角色:消息的消费者功能:订阅感兴趣的主题,接收相关消息特点:可以订阅多个主题可以使用通配符订阅一类主题只接收订阅后发布的消息4. Broker(代理服务器)角色:消息的中转站和路由器功能:接收发布者发送的消息根据订阅关系将消息分发给订阅者管理客户端连接和会话处理消息的 QoS 保证工作流程连接建立:客户端(发布者/订阅者)连接到 Broker订阅主题:订阅者向 Broker 发送订阅请求发布消息:发布者向特定主题发送消息消息路由:Broker 接收消息,查找订阅该主题的客户端消息分发:Broker 将消息转发给所有订阅者消息接收:订阅者接收并处理消息通配符订阅单级通配符(+)匹配单个层级示例:home/+/temperature 匹配 home/livingroom/temperature,但不匹配 home/livingroom/kitchen/temperature多级通配符(#)匹配多个层级,必须放在主题末尾示例:home/# 匹配 home/ 下的所有主题优势解耦性:发布者和订阅者完全解耦,互不依赖灵活性:支持一对多、多对一、多对多的通信模式可扩展性:易于添加新的发布者和订阅者异步性:发布者不需要等待订阅者响应高效性:Broker 负责消息路由,减少网络开销与点对点模式的对比| 特性 | 发布/订阅模式 | 点对点模式 ||-----|-------------|-----------|| 耦合度 | 低 | 高 || 消息接收者 | 多个 | 一个 || 消息持久化 | 可选 | 通常需要 || 复杂度 | 中等 | 简单 || 适用场景 | 广播、通知 | 直接通信 |MQTT 的发布/订阅模式使其成为物联网、实时通信和消息推送等场景的理想选择。
阅读 0·2月21日 15:45

MQTT 的保留消息(Retained Messages)是什么?如何使用?

MQTT 的保留消息(Retained Messages)是一种特殊的消息机制,允许 Broker 保存最新消息,供新订阅者接收。保留消息的概念定义保留消息是 Broker 持久化存储的消息,当有新的客户端订阅该主题时,Broker 会立即将保留消息发送给该客户端。作用状态同步:新订阅者可以立即获取最新状态初始化数据:为新连接的客户端提供初始数据状态恢复:帮助客户端快速恢复到最新状态减少请求:避免客户端主动请求最新状态保留消息的工作原理设置保留消息发布消息时设置 Retain 标志:PUBLISH 报文参数:- Topic: 主题名称- Payload: 消息内容- QoS: QoS 级别- Retain: true(设置为保留消息)保留消息的存储存储位置:Broker 内存或持久化存储存储数量:每个主题只保留一条最新消息存储覆盖:新发布的保留消息会覆盖之前的保留消息保留消息的发送当客户端订阅主题时:客户端发送 SUBSCRIBE 报文Broker 检查该主题是否有保留消息如果有,立即发送保留消息给客户端然后发送后续的新消息保留消息的特性1. 每个主题一条规则:每个主题只保留一条最新的保留消息覆盖机制:新发布的保留消息会替换之前的清除机制:发布空消息(Payload 为空)可以清除保留消息2. QoS 级别继承性:保留消息的 QoS 级别由发布时决定订阅限制:订阅者接收的 QoS 级别受限于订阅时的 QoS 设置QoS 规则:实际 QoS = min(发布 QoS, 订阅 QoS)3. 持久化内存存储:默认存储在内存中持久化存储:可配置持久化到磁盘Broker 重启:持久化的保留消息在 Broker 重启后仍然存在4. 消息顺序发送顺序:保留消息在普通消息之前发送订阅时机:只在订阅时发送一次后续消息:不重复发送保留消息使用场景1. 设备状态同步场景:温度传感器保留主题:sensor/123/temperature保留消息:{"value": 25.5, "unit": "C", "timestamp": 1234567890}新订阅者订阅 sensor/123/temperature立即收到最新温度值2. 配置信息发布场景:设备配置保留主题:config/device/123保留消息:{"mode": "auto", "interval": 60}新设备上线订阅配置主题立即获取最新配置3. 系统状态广播场景:系统状态保留主题:system/status保留消息:{"status": "running", "version": "1.0.0"}新客户端订阅系统状态立即获取当前系统状态4. 开关状态场景:智能开关保留主题:switch/123/state保留消息:{"state": "on"}新订阅者立即获取开关状态代码示例Python (paho-mqtt)import paho.mqtt.client as mqttimport jsonimport timedef on_connect(client, userdata, flags, rc): print(f"Connected with result code {rc}") # 订阅主题 client.subscribe("sensor/+/temperature")def on_message(client, userdata, msg): print(f"Received: {msg.topic} - {msg.payload.decode()}") print(f"Retained: {msg.retain}")# 发布保留消息client = mqtt.Client()client.connect("broker.example.com", 1883, 60)# 发布保留消息(retain=True)message = {"value": 25.5, "unit": "C", "timestamp": int(time.time())}client.publish("sensor/123/temperature", json.dumps(message), retain=True)# 清除保留消息(发布空消息)# client.publish("sensor/123/temperature", "", retain=True)client.disconnect()# 订阅者subscriber = mqtt.Client()subscriber.on_connect = on_connectsubscriber.on_message = on_messagesubscriber.connect("broker.example.com", 1883, 60)subscriber.loop_forever()JavaScript (MQTT.js)const mqtt = require('mqtt');// 发布保留消息const publisher = mqtt.connect('mqtt://broker.example.com');publisher.on('connect', () => { console.log('Publisher connected'); // 发布保留消息(retain: true) const message = JSON.stringify({ value: 25.5, unit: 'C', timestamp: Date.now() }); publisher.publish('sensor/123/temperature', message, { retain: true }); // 清除保留消息(发布空消息) // publisher.publish('sensor/123/temperature', '', { retain: true }); publisher.end();});// 订阅者const subscriber = mqtt.connect('mqtt://broker.example.com');subscriber.on('connect', () => { console.log('Subscriber connected'); subscriber.subscribe('sensor/+/temperature');});subscriber.on('message', (topic, message) => { console.log(`Received: ${topic} - ${message.toString()}`); console.log(`Retained: ${message.retain}`);});最佳实践1. 保留消息设计状态信息:保留消息应该表示当前状态简洁明了:消息内容简洁,易于解析包含时间戳:便于判断消息的新旧程度版本控制:可以包含版本信息2. 主题命名推荐格式:- sensor/{device_id}/temperature- config/{device_id}- status/{system_id}避免使用:- 通配符主题(不能发布到通配符主题)- 过于复杂的主题结构3. 消息大小限制大小:保留消息不宜过大建议大小:通常小于 1KBBroker 限制:注意 Broker 对消息大小的限制4. QoS 选择一般状态:QoS 0重要状态:QoS 1关键状态:QoS 25. 清除机制主动清除:发布空消息清除保留消息定期清理:定期检查和清理过期的保留消息生命周期管理:为保留消息设置合理的生命周期注意事项内存占用:保留消息会占用 Broker 内存,大量保留消息可能影响性能持久化配置:如果需要保留消息在 Broker 重启后仍然存在,需要配置持久化消息更新:频繁更新保留消息会增加 Broker 负担订阅时机:保留消息只在订阅时发送,不会重复发送QoS 限制:订阅者接收的 QoS 级别受限于订阅时的 QoS 设置空消息清除:发布空消息(Payload 为空)可以清除保留消息保留消息 vs 遗嘱消息| 特性 | 保留消息 | 遗嘱消息 ||-----|---------|---------|| 触发时机 | 订阅时 | 异常断开时 || 消息来源 | 发布者设置 | 客户端设置 || 存储位置 | Broker | Broker || 发送对象 | 新订阅者 | 订阅该主题的客户端 || 消息数量 | 每主题一条 | 每客户端一条 || 清除方式 | 发布空消息 | 正常断开或重新连接 |保留消息的局限性每主题一条:每个主题只能保留一条消息,无法保存历史消息内存占用:大量保留消息会占用较多内存实时性:保留消息可能不是最新的(取决于发布频率)无历史记录:无法获取历史状态变化依赖 Broker:完全依赖 Broker 的可靠性MQTT 保留消息是物联网应用中非常重要的机制,合理使用可以有效实现状态同步和初始化,提高用户体验和系统可靠性。
阅读 0·2月21日 15:44

OffscreenCanvas 如何在 Web Worker 中进行渲染?

OffscreenCanvas 是 HTML5 提供的一个功能,允许在 Web Worker 中进行 Canvas 渲染,从而将复杂的图形计算从主线程移到后台线程。OffscreenCanvas 的核心概念特点可以在 Worker 中进行 Canvas 绘图操作支持大部分 Canvas 2D API 和 WebGL API通过 transferControlToOffscreen() 方法将 Canvas 控制权转移适用于复杂的图形渲染和动画基本使用主线程设置// 获取 Canvas 元素const canvas = document.getElementById('myCanvas');// 将 Canvas 控制权转移到 OffscreenCanvasconst offscreen = canvas.transferControlToOffscreen();// 创建 Workerconst worker = new Worker('canvas-worker.js');// 将 OffscreenCanvas 发送给 Workerworker.postMessage({ canvas: offscreen }, [offscreen]);Worker 中渲染// canvas-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); // 在 Worker 中进行绘图 function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制图形 ctx.fillStyle = 'blue'; ctx.fillRect(50, 50, 100, 100); // 继续动画 requestAnimationFrame(render); } render();};实际应用场景1. 复杂动画渲染// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('animation-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// animation-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); let particles = []; function initParticles() { for (let i = 0; i < 1000; i++) { particles.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2, size: Math.random() * 3 + 1 }); } } function updateParticles() { particles.forEach(p => { p.x += p.vx; p.y += p.vy; if (p.x < 0 || p.x > canvas.width) p.vx *= -1; if (p.y < 0 || p.y > canvas.height) p.vy *= -1; }); } function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(p => { ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fillStyle = `rgba(100, 150, 255, 0.7)`; ctx.fill(); }); updateParticles(); requestAnimationFrame(render); } initParticles(); render();};2. 图像处理// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('image-worker.js');// 加载图像const img = new Image();img.onload = function() { worker.postMessage({ canvas: offscreen, image: img }, [offscreen]);};img.src = 'image.jpg';// image-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); const img = e.data.image; // 设置 Canvas 大小 canvas.width = img.width; canvas.height = img.height; // 绘制原始图像 ctx.drawImage(img, 0, 0); // 获取图像数据 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; // 图像处理:灰度化 for (let i = 0; i < data.length; i += 4) { const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; data[i] = avg; // R data[i + 1] = avg; // G data[i + 2] = avg; // B } // 放回处理后的图像 ctx.putImageData(imageData, 0, 0);};3. WebGL 渲染// 主线程const canvas = document.getElementById('glCanvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('webgl-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// webgl-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const gl = canvas.getContext('webgl'); // WebGL 初始化代码 const vertexShaderSource = ` attribute vec4 aVertexPosition; void main() { gl_Position = aVertexPosition; } `; const fragmentShaderSource = ` void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); } `; // 编译着色器 const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource); const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建程序 const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); // 渲染 function render() { gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(shaderProgram); gl.drawArrays(gl.TRIANGLES, 0, 3); requestAnimationFrame(render); } render();};function compileShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); return shader;}与主线程交互动态调整 Canvas 大小// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('canvas-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// 监听窗口大小变化window.addEventListener('resize', function() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; worker.postMessage({ type: 'resize', width: canvas.width, height: canvas.height });});// canvas-worker.jsself.onmessage = function(e) { if (e.data.type === 'resize') { canvas.width = e.data.width; canvas.height = e.data.height; }};接收用户输入// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('canvas-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// 发送鼠标位置canvas.addEventListener('mousemove', function(e) { const rect = canvas.getBoundingClientRect(); worker.postMessage({ type: 'mousemove', x: e.clientX - rect.left, y: e.clientY - rect.top });});// canvas-worker.jslet mouseX = 0, mouseY = 0;self.onmessage = function(e) { if (e.data.type === 'mousemove') { mouseX = e.data.x; mouseY = e.data.y; }};注意事项1. Canvas 控制权只能转移一次// ❌ 错误:多次转移const offscreen1 = canvas.transferControlToOffscreen();const offscreen2 = canvas.transferControlToOffscreen(); // 报错// ✅ 正确:只转移一次const offscreen = canvas.transferControlToOffscreen();2. OffscreenCanvas 不支持所有 Canvas API// ❌ 不支持canvas.toDataURL(); // 在 Worker 中不可用canvas.toBlob(); // 在 Worker 中不可用// ✅ 使用 ImageBitmap 代替const bitmap = await createImageBitmap(canvas);3. 浏览器兼容性// 检查浏览器支持if ('transferControlToOffscreen' in HTMLCanvasElement.prototype) { // 支持 OffscreenCanvas} else { // 不支持,使用回退方案}性能优化1. 批量绘制// ❌ 频繁调用绘制方法for (let i = 0; i < 1000; i++) { ctx.beginPath(); ctx.arc(particles[i].x, particles[i].y, particles[i].size, 0, Math.PI * 2); ctx.fill();}// ✅ 批量绘制ctx.beginPath();for (let i = 0; i < 1000; i++) { ctx.moveTo(particles[i].x, particles[i].y); ctx.arc(particles[i].x, particles[i].y, particles[i].size, 0, Math.PI * 2);}ctx.fill();2. 使用 ImageBitmap// 加载图像为 ImageBitmapconst bitmap = await createImageBitmap(image);// 在 Worker 中绘制ctx.drawImage(bitmap, 0, 0);3. 降低渲染频率let lastRenderTime = 0;const targetFPS = 30;const frameInterval = 1000 / targetFPS;function render(timestamp) { if (timestamp - lastRenderTime >= frameInterval) { // 执行渲染 ctx.clearRect(0, 0, canvas.width, canvas.height); // ... 绘制代码 lastRenderTime = timestamp; } requestAnimationFrame(render);}最佳实践复杂渲染使用 OffscreenCanvas:将计算密集型图形渲染移到 Worker合理控制渲染频率:避免不必要的重绘批量处理:减少绘制调用次数使用 ImageBitmap:提高图像加载和渲染性能检查浏览器兼容性:提供回退方案及时释放资源:使用完毕后清理资源
阅读 0·2月21日 15:44

如何优化Expo应用的性能?有哪些常见的性能问题?

Expo应用的性能优化是确保良好用户体验的关键。Expo基于React Native,因此许多React Native的性能优化技巧同样适用,同时Expo也提供了一些特定的优化工具和策略。性能优化策略:组件渲染优化使用React.memo、useMemo和useCallback减少不必要的渲染:// 使用React.memo避免不必要的重新渲染const MyComponent = React.memo(({ data }) => { return <Text>{data.value}</Text>;});// 使用useMemo缓存计算结果function ExpensiveComponent({ items }) { const sortedItems = useMemo(() => { return items.sort((a, b) => a.id - b.id); }, [items]); return <FlatList data={sortedItems} />;}// 使用useCallback缓存函数function ParentComponent() { const handleClick = useCallback(() => { console.log('Clicked'); }, []); return <ChildComponent onClick={handleClick} />;}列表优化使用FlatList而不是ScrollView处理长列表:<FlatList data={items} renderItem={({ item }) => <Item item={item} />} keyExtractor={(item) => item.id} removeClippedSubviews={true} maxToRenderPerBatch={10} windowSize={10} initialNumToRender={10} getItemLayout={(data, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index, })}/>关键属性说明:removeClippedSubviews:移除屏幕外的视图maxToRenderPerBatch:每批渲染的项目数windowSize:渲染窗口大小initialNumToRender:初始渲染项目数getItemLayout:提供项目布局信息图片优化使用expo-image和图片缓存策略:import { Image } from 'expo-image';<Image source={{ uri: 'https://example.com/image.jpg' }} style={{ width: 200, height: 200 }} cachePolicy="memory-disk" contentFit="cover" transition={200}/>优化技巧:使用适当的图片尺寸启用缓存策略使用WebP格式懒加载图片网络请求优化使用缓存和请求去重:import { useQuery } from '@tanstack/react-query';function UserProfile({ userId }) { const { data, isLoading } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), staleTime: 5 * 60 * 1000, // 5分钟 cacheTime: 10 * 60 * 1000, // 10分钟 }); if (isLoading) return <Loading />; return <Text>{data.name}</Text>;}动画优化使用react-native-reanimated进行高性能动画:import Animated, { useSharedValue, useAnimatedStyle, withTiming,} from 'react-native-reanimated';function AnimatedBox() { const opacity = useSharedValue(0); const animatedStyle = useAnimatedStyle(() => { return { opacity: withTiming(opacity.value, { duration: 500 }), }; }); useEffect(() => { opacity.value = 1; }, []); return <Animated.View style={animatedStyle} />;}内存管理及时释放资源:// 取消网络请求useEffect(() => { const controller = new AbortController(); fetchData(controller.signal); return () => controller.abort();}, []);// 清理定时器useEffect(() => { const timer = setInterval(() => { console.log('Tick'); }, 1000); return () => clearInterval(timer);}, []);// 清理订阅useEffect(() => { const subscription = someEvent.subscribe(); return () => subscription.unsubscribe();}, []);Bundle优化减少应用包大小:// 代码分割const LazyComponent = React.lazy(() => import('./LazyComponent'));// 动态导入const loadModule = async () => { const module = await import('./heavyModule'); module.doSomething();};// 移除未使用的依赖npm prune使用Expo的性能工具Expo提供了一些性能监控工具:import { Performance } from 'react-native';// 记录性能标记Performance.mark('component-start');// 组件渲染完成后Performance.mark('component-end');// 测量性能Performance.measure('component-render', 'component-start', 'component-end');性能分析工具:React DevTools Profiler分析组件渲染性能识别性能瓶颈优化渲染次数Flipper网络请求监控布局检查内存分析Expo DevTools实时性能监控Bundle大小分析加载时间追踪常见性能问题及解决方案:列表滚动卡顿使用FlatList而不是ScrollView启用removeClippedSubviews提供正确的getItemLayout图片加载慢使用expo-image启用缓存使用适当的图片尺寸应用启动慢优化初始化代码延迟加载非关键模块使用启动画面内存泄漏及时清理订阅和定时器使用WeakMap和WeakSet定期检查内存使用最佳实践:性能监控:定期使用性能工具分析应用渐进优化:先优化关键路径,再优化次要功能测试覆盖:在不同设备上测试性能持续优化:将性能优化作为持续过程用户体验:平衡性能和功能,优先保证核心体验通过系统性的性能优化,可以显著提升Expo应用的用户体验和竞争力。
阅读 0·2月21日 15:43

Ollama 与其他 LLM 部署方案(如 vLLM、LM Studio)相比有什么优缺点?

Ollama 与其他 LLM 部署方案相比,各有优劣:1. Ollama vs. vLLM:Ollama 优势:安装简单,一行命令即可部署跨平台支持(Linux/macOS/Windows)内置模型管理和 API 服务适合个人开发和小型应用vLLM 优势:更高的推理性能和吞吐量支持 PagedAttention 技术更适合大规模生产环境支持更多模型格式2. Ollama vs. LM Studio:Ollama 优势:命令行和 API 友好更适合服务器部署开源且免费更好的自动化集成LM Studio 优势:图形化界面,用户体验好内置模型市场适合桌面用户可视化配置选项3. Ollama vs. OpenAI API:Ollama 优势:完全本地运行,数据隐私保护无 API 调用费用可自定义模型无网络依赖OpenAI API 优势:模型性能更强(GPT-4 等)无需本地硬件持续更新和优化更好的多语言支持4. Ollama vs. LocalAI:Ollama 优势:更轻量级更简单的配置更好的性能优化活跃的社区支持LocalAI 优势:OpenAI API 兼容性更好支持更多模型类型更灵活的配置选项支持多模型并行5. Ollama vs. Text Generation WebUI:Ollama 优势:更适合 API 集成更简单的部署更好的性能命令行友好Text Generation WebUI 优势:功能丰富的 Web 界面支持更多高级功能可视化参数调整更好的交互体验选择建议:个人开发/学习:Ollama 或 LM Studio生产环境 API 服务:Ollama 或 vLLM需要 OpenAI 兼容性:LocalAI桌面用户:LM Studio需要最高性能:vLLM需要图形界面:LM Studio 或 Text Generation WebUI
阅读 0·2月21日 15:43