发布-订阅模式

article/2025/10/27 3:30:24

发布-订阅模式

学习知识要善于思考,思考,再思考。 —— 爱因斯

在众多设计模式中,可能最常见、最有名的就是发布 - 订阅模式了,本篇我们一起来学习这个模式。

发布 - 订阅模式 (Publish-Subscribe Pattern, pub-sub)又叫观察者模式(Observer Pattern),它定义了一种一对多的关系,让多个订阅者对象同时监听某一个发布者,或者叫主题对象,这个主题对象的状态发生变化时就会通知所有订阅自己的订阅者对象,使得它们能够自动更新自己。

当然有人提出发布 - 订阅模式和观察者模式之间是有一些区别的,但是大部分情况下你可以将他们当成是一个模式,本文将不对它们之间进行区分,文末会简单讨论一下他们之间的微妙区别,了解即可。

注意: 本文可能用到一些编码技巧比如 IIFE(Immediately Invoked Function Expression, 立即调用函数表达式),ES6 的语法 let/const、 箭头函数、 rest 参数等,如果还没接触过可以点击链接稍加学习~

1.你曾遇见过的发布 - 订阅模式

在现实生活中其实我们会经常碰到发布 - 订阅模式的例子。

比如当我们进入一个聊天室 / 群,如果有人在聊天室发言,那么这个聊天室里的所有人都会收到这个人的发言。这是一个典型的发布 - 订阅模式,当我们加入了这个群,相当于订阅了在这个聊天室发送的消息,当有新的消息产生,聊天室会负责将消息发布给所有聊天室的订阅者。

再举个栗子,当我们去 adadis 买鞋,发现看中的款式已经售罄了,售货员告诉你不久后这个款式会进货,到时候打电话通知你。于是你留了个电话,离开了商场,当下周某个时候 adadis 进货了,售货员拿出小本本,给所有关注这个款式的人打电话。

这也是一个日常生活中的一个发布 - 订阅模式的实例,虽然不知道什么时候进货,但是我们可以登记号码之后等待售货员的电话,不用每天都打电话问鞋子的信息。

上面两个小栗子,都属于发布 - 订阅模式的实例,群成员 / 买家属于消息的订阅者,订阅消息的变化,聊天室 / 售货员属于消息的发布者,在合适的时机向群成员 / 小本本上的订阅者发布消息。

adadis 售货员这个例子的各方关系大概如下图:

在这样的逻辑中,有以下几个特点:

  1. 买家(订阅者)只要声明对消息的一次订阅,就可以在未来的某个时候接受来自售货员(发布者)的消息,不用一直轮询消息的变化;
  2. 售货员(发布者)持有一个小本本(订阅者列表),对这个本本上记录的订阅者的情况并不关心,只需要在消息发生时挨个去通知小本本上的订阅者,当订阅者增加或减少时,只需要在小本本上增删记录即可;
  3. 将上面的逻辑升级一下,一个人可以加多个群,售货员也可以有多个小本本,当不同的群产生消息或者不款式的鞋进货了,发布者可以按照不同的名单 / 小本本分别去通知订阅了不同类型消息的订阅者,这里有个消息类型的概念;

2.实例的代码实现

如果你在 DOM 上绑定过事件处理函数 addEventListener,那么你已经使用过发布 - 订阅模式了。

我们经常将一些操作挂载在 onload 事件上执行,当页面元素加载完毕,就会触发你注册在 onload 事件上的回调。我们无法预知页面元素何时加载完毕,但是通过订阅 window 的 onload 事件,window 会在加载完毕时向订阅者发布消息,也就是执行回调函数。

window.addEventListener('load', function () {console.log('loaded!')
})

这与买鞋的例子类似,我们不知道什么时候进货,但只需订阅鞋子的消息,进货的时候售货员会打电话通知我们。

在现实中和编程中我们还会遇到很多这样类似的问题,我们可以将 adadis 的例子提炼一下,用 JavaScript 来实现:

const adadisPub = {adadisBook: [],              // adadis售货员的小本本subShoe(phoneNumber) {       // 买家在小本本是登记号码this.adadisBook.push(phoneNumber)},notify() {                     // 售货员打电话通知小本本上的买家for (const customer of this.adadisBook) {customer.update()}
}
}const customer1 = {phoneNumber: '152xxx',update() {console.log(this.phoneNumber + ': 去商场看看')}
}const customer2 = {phoneNumber: '138yyy',update() {console.log(this.phoneNumber + ': 给表弟买双')}
}adadisPub.subShoe(customer1)  // 在小本本上留下号码
adadisPub.subShoe(customer2)adadisPub.notify()            // 打电话通知买家到货了// 152xxx: 去商场看看
// 138yyy: 给表弟买双

这样我们就实现了在有新消息时对买家的通知。

当然还可以对功能进行完善,比如:

  1. 在登记号码的时候进行一下判重操作,重复号码就不登记了;
  2. 买家登记之后想了一下又不感兴趣了,那么以后也就不需要通知了,增加取消订阅的操作;
const adadisPub = {adadisBook: [],              // adadis售货员的小本本subShoe(customer) {       // 买家在小本本是登记号码if (!this.adadisBook.includes(customer))    // 判重this.adadisBook.push(customer)},unSubShoe(customer) {     // 取消订阅if (!this.adadisBook.includes(customer)) returnconst idx = this.adadisBook.indexOf(customer)this.adadisBook.splice(idx, 1)},notify() {                     // 售货员打电话通知小本本上的买家for (const customer of this.adadisBook) {customer.update()}}
}const customer1 = {phoneNumber: '152xxx',update() {console.log(this.phoneNumber + ': 去商场看看')}
}const customer2 = {phoneNumber: '138yyy',update() {console.log(this.phoneNumber + ': 给表弟买双')}
}adadisPub.subShoe(customer1)  // 在小本本上留下号码
adadisPub.subShoe(customer1)
adadisPub.subShoe(customer2)
adadisPub.unSubShoe(customer1)adadisPub.notify()            // 打电话通知买家到货了// 138yyy: 给表弟买双

到现在我们已经简单完成了一个发布 - 订阅模式。

但是还可以继续改进,比如买家可以关注不同的鞋型,那么当某个鞋型进货了,只通知关注了这个鞋型的买家,总不能通知所有买家吧。改写后的代码:

const adadisPub = {adadisBook: {},                    // adadis售货员的小本本subShoe(type, customer) {       // 买家在小本本是登记号码if (this.adadisBook[type]) {   // 如果小本本上已经有这个typeif (!this.adadisBook[type].includes(customer))    // 判重this.adadisBook[type].push(customer)} else this.adadisBook[type] = [customer]},unSubShoe(type, customer) {     // 取消订阅if (!this.adadisBook[type] ||!this.adadisBook[type].includes(customer)) returnconst idx = this.adadisBook[type].indexOf(customer)this.adadisBook[type].splice(idx, 1)},notify(type) {                     // 售货员打电话通知小本本上的买家if (!this.adadisBook[type]) returnthis.adadisBook[type].forEach(customer =>customer.update(type))}
}const customer1 = {phoneNumber: '152xxx',update(type) {console.log(this.phoneNumber + ': 去商场看看' + type)}
}const customer2 = {phoneNumber: '138yyy',update(type) {console.log(this.phoneNumber + ': 给表弟买双' + type)}
}adadisPub.subShoe('运动鞋', customer1)    // 订阅运动鞋
adadisPub.subShoe('运动鞋', customer1)
adadisPub.subShoe('运动鞋', customer2)
adadisPub.subShoe('帆布鞋', customer1)    // 订阅帆布鞋adadisPub.notify('运动鞋')    // 打电话通知买家运动鞋到货了// 152xxx: 去商场看看运动鞋
// 138yyy: 给表弟买双运动鞋

3.发布-订阅模式的通用实现

我们可以把上面例子的几个核心概念提取一下,买家可以被认为是订阅者(Subscriber),售货员可以被认为是发布者(Publisher),售货员持有小本本(SubscriberMap),小本本上记录有买家订阅(subscribe)的不同鞋型(Type)的信息,当然也可以退订(unSubscribe),当鞋型有消息时售货员会给订阅了当前类型消息的订阅者发布(notify)消息。

主要有下面几个概念:

  1. Publisher :发布者,当消息发生时负责通知对应订阅者
  2. Subscriber :订阅者,当消息发生时被通知的对象
  3. SubscriberMap :持有不同 type 的数组,存储有所有订阅者的数组
  4. type :消息类型,订阅者可以订阅的不同消息类型
  5. subscribe :该方法为将订阅者添加到 SubscriberMap 中对应的数组中
  6. unSubscribe :该方法为在 SubscriberMap 中删除订阅者
  7. notify :该方法遍历通知 SubscriberMap 中对应 type 的每个订阅者

现在的结构如下图:

下面使用通用化的方法实现一下。

首先我们使用立即调用函数 IIFE(Immediately Invoked Function Expression) 方式来将不希望公开的 SubscriberMap 隐藏,然后可以将注册的订阅行为换为回调函数的形式,这样我们可以在消息通知时附带参数信息,在处理通知的时候也更灵活:

const Publisher = (function() {const _subsMap = {}   // 存储订阅者return {/* 消息订阅 */subscribe(type, cb) {if (_subsMap[type]) {if (!_subsMap[type].includes(cb))_subsMap[type].push(cb)} else _subsMap[type] = [cb]},/* 消息退订 */unsubscribe(type, cb) {if (!_subsMap[type] ||!_subsMap[type].includes(cb)) returnconst idx = _subsMap[type].indexOf(cb)_subsMap[type].splice(idx, 1)},/* 消息发布 */notify(type, ...payload) {if (!_subsMap[type]) return_subsMap[type].forEach(cb => cb(...payload))}}
})()Publisher.subscribe('运动鞋', message => console.log('152xxx' + message))    // 订阅运动鞋
Publisher.subscribe('运动鞋', message => console.log('138yyy' + message))
Publisher.subscribe('帆布鞋', message => console.log('139zzz' + message))    // 订阅帆布鞋Publisher.notify('运动鞋', ' 运动鞋到货了 ~')   // 打电话通知买家运动鞋消息
Publisher.notify('帆布鞋', ' 帆布鞋售罄了 T.T') // 打电话通知买家帆布鞋消息// 输出:  152xxx 运动鞋到货了 ~
// 输出:  138yyy 运动鞋到货了 ~
// 输出:  139zzz 帆布鞋售罄了 T.T

上面是使用 IIFE 实现的,现在 ES6 如此流行,也可以使用 class 语法来改写一下:

class Publisher {constructor() {this._subsMap = {}}/* 消息订阅 */subscribe(type, cb) {if (this._subsMap[type]) {if (!this._subsMap[type].includes(cb))this._subsMap[type].push(cb)} else this._subsMap[type] = [cb]}/* 消息退订 */unsubscribe(type, cb) {if (!this._subsMap[type] ||!this._subsMap[type].includes(cb)) returnconst idx = this._subsMap[type].indexOf(cb)this._subsMap[type].splice(idx, 1)}/* 消息发布 */notify(type, ...payload) {if (!this._subsMap[type]) returnthis._subsMap[type].forEach(cb => cb(...payload))}
}const adadis = new Publisher()adadis.subscribe('运动鞋', message => console.log('152xxx' + message))    // 订阅运动鞋
adadis.subscribe('运动鞋', message => console.log('138yyy' + message))
adadis.subscribe('帆布鞋', message => console.log('139zzz' + message))    // 订阅帆布鞋adadis.notify('运动鞋', ' 运动鞋到货了 ~')   // 打电话通知买家运动鞋消息
adadis.notify('帆布鞋', ' 帆布鞋售罄了 T.T') // 打电话通知买家帆布鞋消息// 输出:  152xxx 运动鞋到货了 ~
// 输出:  138yyy 运动鞋到货了 ~
// 输出:  139zzz 帆布鞋售罄了 T.T

4.实战中的发布——订阅模式

使用 jQuery 的方式

我们使用 jQuery 的时候可以通过其自带的 API 比如 on、trigger、off 来轻松实现事件的订阅、发布、取消订阅等操作:

function eventHandler() {console.log('自定义方法')
}/* ---- 事件订阅 ---- */
$('#app').on('myevent', eventHandler)
// 发布
$('#app').trigger('myevent')// 输出:自定义方法/* ---- 取消订阅 ---- */
$('#app').off('myevent')
$('#app').trigger('myevent')// 没有输出

甚至我们可以使用原生的 addEventListener、dispatchEvent、removeEventListener 来实现发布订阅:

// 输出:自定义方法
function eventHandler(dom) {console.log('自定义方法', dom)
}var app = document.getElementById('app')/* ---- 事件订阅 ---- */
app.addEventListener('myevent', eventHandler)
// 发布
app.dispatchEvent(new Event('myevent'))// 输出:自定义方法+DOM/* ---- 取消订阅 ---- */
app.removeEventListener('myevent', eventHandler)
app.dispatchEvent(new Event('myevent'))// 没有输出

使用 Vue 的 EventBus

和 jQuery 一样,Vue 也是实现有一套事件机制,其中一个我们熟知的用法是 EventBus。在多层组件的事件处理中,如果你觉得一层层 o n 、 on、 onemit 比较麻烦,而你又不愿意引入 Vuex,那么这时候推介使用 EventBus 来解决组件间的数据通信:

// event-bus.jsimport Vue from 'vue'
export const EventBus = new Vue()

使用时:

// 组件A
import { EventBus } from "./event-bus.js";EventBus.$on("myevent", args => {console.log(args)
})
// 组件B
import { EventBus } from "./event-bus.js";EventBus.$emit("myevent", 'some args')

实现组件间的消息传递,不过在中大型项目中,还是推介使用 Vuex,因为如果 Bus 上的事件挂载过多,事件满天飞,就分不清消息的来源和先后顺序,对可维护性是一种破坏。

源码中的发布——订阅模式

下面稍微解释一下这个图(框架源码整个过程比较复杂,如果现在看不懂下面几段也没关系,大致了解一下即可)。

组件渲染函数(Component Render Function)被执行前,会对数据层的数据进行响应式化。响应式化大致就是使用 Object.defineProperty 把数据转为 getter/setter,并为每个数据添加一个订阅者列表的过程。这个列表是 getter 闭包中的属性,将会记录所有依赖这个数据的组件。

也就是说,响应式化后的数据相当于发布者。

每个组件都对应一个 Watcher 订阅者。当每个组件的渲染函数被执行时,都会将本组件的 Watcher 放到自己所依赖的响应式数据的订阅者列表里,这就相当于完成了订阅,一般这个过程被称为依赖收集(Dependency Collect)。

组件渲染函数执行的结果是生成虚拟 DOM 树(Virtual DOM Tree),这个树生成后将被映射为浏览器上的真实的 DOM 树,也就是用户所看到的页面视图。

当响应式数据发生变化的时候,也就是触发了 setter 时,setter 会负责通知(Notify)该数据的订阅者列表里的 Watcher,Watcher 会触发组件重渲染(Trigger re-render)来更新(update)视图。

我们可以看看 Vue 的源码:

// src/core/observer/index.js 响应式化过程Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter() {// ...const value = getter ? getter.call(obj) : val // 如果原本对象拥有getter方法则执行dep.depend()                     // 进行依赖收集,dep.addSubreturn value},set: function reactiveSetter(newVal) {// ...if (setter) { setter.call(obj, newVal) }    // 如果原本对象拥有setter方法则执行dep.notify()               // 如果发生变更,则通知更新}
})

而这个 dep 上的 depend 和 notify 就是订阅和发布通知的具体方法。

简单来说,响应式数据是消息的发布者,而视图层是消息的订阅者,如果数据更新了,那么发布者会发布数据更新的消息来通知视图更新,从而实现数据层和视图层的双向绑定。

6.发布——订阅模式的优缺点

发布——订阅模式最大的优点就是解耦:

时间上的解耦 :注册的订阅行为由消息的发布方来决定何时调用,订阅者不用持续关注,当消息发生时发布者会负责通知;
对象上的解耦 :发布者不用提前知道消息的接受者是谁,发布者只需要遍历处理所有订阅该消息类型的订阅者发送消息即可(迭代器模式),由此解耦了发布者和订阅者之间的联系,互不持有,都依赖于抽象,不再依赖于具体;
由于它的解耦特性,发布 - 订阅模式的使用场景一般是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变。发布 - 订阅模式还可以帮助实现一些其他的模式,比如中介者模式。

发布——订阅模式也有缺点:

增加消耗 :创建结构和缓存订阅者这两个过程需要消耗计算和内存资源,即使订阅后始终没有触发,订阅者也会始终存在于内存;
增加复杂度 :订阅者被缓存在一起,如果多个订阅者和发布者层层嵌套,那么程序将变得难以追踪和调试,参考一下 Vue 调试的时候你点开原型链时看到的那堆 deps/subs/watchers 们…
缺点主要在于理解成本、运行效率、资源消耗,特别是在多级发布 - 订阅时,情况会变得更复杂。

7.其他相关模式

观察者模式与发布 - 订阅者模式,在平时你可以认为他们是一个东西,但是某些场合(比如面试)下可能需要稍加注意,借用网上一张流行的图:

区别主要在发布 - 订阅模式中间的这个 Event Channel:

  1. 观察者模式 中的观察者和被观察者之间还存在耦合,被观察者还是知道观察者的;
  2. 发布 - 订阅模式 中的发布者和订阅者不需要知道对方的存在,他们通过消息代理来进行通信,解耦更加彻底;

发布——订阅模式和责任链模式

发布——订阅模式和责任链模式也有点类似,主要区别在于:

  1. 发布——订阅模式 传播的消息是根据需要随时发生变化,是发布者和订阅者之间约定的结构,在多级发布 - 订阅的场景下,消息可能完全不一样;
  2. 责任链模式 传播的消息是不变化的,即使变化也是在原来的消息上稍加修正,不会大幅改变结构;

转载慕课网慕课专栏javascript设计模式精讲:https://www.imooc.com/read/38/article/493


http://chatgpt.dhexx.cn/article/uPaV7rh8.shtml

相关文章

什么是发布订阅模式?

发布-订阅模式(Publish-Subscribe pattern)是一种软件架构模式,用于实现组件之间的解耦和消息传递。在这种模式中,组件(发布者)将消息发送到一个中心(消息代理或主题),然…

发布订阅模式

零、目录 应用场景实现原理代码实现全局模式下的订阅发布模式(泛化的订阅发布模式)总结 一、应用场景 ​ 发布订阅模式,广泛的存在于在我们的生活之中。 ​ 举个一个简单的例子来说,当我们在浏览视频或者博客论坛之类的网…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:VLCPlayer

本文简述如何在Smobiler中使用VLCPlayer插件,该插件支持播放rtsp流。 Step 1. 新建一个SmobilerForm窗体,再拖入VLCPlay,布局如下 在设计器中给VLCPlayer.Url赋值或者在窗体的Load事件中赋值 演示使用的rtsp流地址 rtsp://wowzaec2demo.strea…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:TTS

本文简述如何在Smobiler中使用TTS文字转语音。 Step 1. 新建一个SmobilerForm窗体,并在窗体中加入TTS和Button,布局如下 Button的点击事件代码: private void button1_Press(object sender, EventArgs e){ //第一个参数为文本;第…

Smobiler 仿得到APP个人主页

原型如下: 完整代码参考 https://github.com/comsmobiler/BlogsCode/blob/master/Source/BlogsCode_SmobilerForm/MyForm/dedao.cs 思路 可以将原型按照上图分成2个部分,部分A可以使用label、image、button、imagebutton、fontIcon控件来实现&#xff…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:PDFView

本文简述如何在Smobiler中使用PDFView。 Step 1. 新建一个SmobilerForm窗体,再拖入PDfView,布局如下 PDFView.ResourcrPath默认Document,指项目下\Resources\Document,若是pdf文件放在该文件夹下,则在设计器中直接赋值…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:OCR组件

本文简述如何在Smobiler中使用OCR组件进行文字识别。 Step 1. 新建一个SmobilerForm窗体,并在窗体中加入OCR和Button,布局如下 Button的点击事件代码: private void button1_Press(object sender, EventArgs e){ocr1.Recognize((obj,args)>…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:ArcFace人脸识别

本文简述如何在Smobiler中使用ArcFace(虹软人脸识别)。 Step 1. 新建一个SmobilerForm窗体,再拖入Button,Label,TextBox和AcrFace,布局如下 在设计器中给MediaView.Url赋值或者在窗体的Load事件中赋值 Button的事件代码如下 string message …

移动OA办公——Smobiler第一个开源应用解决方案,快来get吧

产品简介 SmoONE是一款移动OA类的开源解决方案,通过Smobiler平台开发,包含了注册、登陆、用户信息等基本功能。集成了OA中使用场景较多的报销、请假、部门管理、成本中心等核心功能。 免费获取方案 开源代码:https://github.com/comsmobile…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:UsbSerial串口通讯组件

本文简述如何在Smobiler中使用UsbSerial。 Step 1. 新建一个SmobilerForm窗体,再拖入UsbSerial和Button,布局如下 按钮事件代码: //连接private void button1_Press_2(object sender, EventArgs e){usbSerial1.Connect(Smobiler.Plugins.USBS…

Smobiler实现手机弹窗

前言 在实际项目中有很多场景需要用到弹窗,如图1 那么这些弹窗在Smobiler中如何实现呢? 正文 Smobiler实现弹窗有两种方式:1.MessageBox.Show 2.ShowDialog和ShowContextDialog。前者适合简易弹窗,后者适合自定义弹窗。 Messa…

Smobiler实现美观登录界面——C# 或.NET Smobiler实例开发手机app(二)

目录 一、 本文目标 二、 准备工作 1、 数据库 2、 材料 三、 界面布局 1、设置控件的属性值 (1) 输入框 (2) 图片属性 (3) HandElectricity的标题的label属性 (4)登录按钮…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:MediaView

本文简述如何在Smobiler中使用MediaView。 Step 1. 新建一个SmobilerForm窗体,再拖入MediaView,MediaView.Size设置(300,225),布局如下 在设计器中给MediaView.Url赋值或者在窗体的Load事件中赋值 播放本地视频可以通过GetResourc…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:AliPay组件

本文简述如何在Smobiler中调用支付宝支付。 Step 1. 界面 新建一个窗体,并在窗体中拖入Button,Label,AliPay等控件,布局如下: Step 2. 代码 在窗体中声明变量 //订单编号private string tradeNo;//支付宝应用编号&am…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:BarcodeReader组件

本文简述如何在Smobiler中使用BarcodeReader组件进行条码识别。Barcodereader通过机器学习能识别不规则条码,效率更好。 Step 1. 新建一个SmobilerForm窗体,并在窗体中加入Barcodereader和Button,布局如下 Button的点击事件代码: …

.NET(C#、VB)APP开发——Smobiler平台控件介绍:LiveStream和LiveStreamPlayer

本文简述如何在Smobiler中使用LiveStream和LiveStreamPlayer。 LiveStream 直播推送插件 Step 1. 新建一个SmobilerForm窗体,并在窗体中加入LiveStream和Button,布局如下 选中LisvStream,在设计器中设置Url(需要事先准备一个视频…

【转载】smobiler说明

类似开发WinForm的方式,使用C#开发Android和IOS的移动应用?听起来感觉不可思议,那么Smobiler平台到底是如何实现的呢,这里给大家介绍一下。 客户端 Smobiler分为两种客户端,一种是开发版,一种是打包版 开发…

.NET(C#)能开发出什么样的APP?盘点那些通过Smobiler开发的移动应用

.NET程序员一定最熟悉所见即所得式开发,熟悉的Visual Studio开发界面,熟悉的C#代码。 Smobiler也是因为具备这样的特性,使开发人员,可以在VisualStudio上,像开发WinForm一样拖拉控件,让许多人在开发APP时,再次回到所见即所得的开发方式中去。 Smobiler的快速开发,让Ama…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:MapView MaptrimView

本文简述如何在Smobiler中使用MapView和MaptrimView。 Mapview MapView 地图插件,可用于显示指定地点地图,显示轨迹等。 Step 1. 新建一个SmobilerForm窗体,再拖入MapView和Button,MapView.Size设置(300,300&#xf…

Smobiler 窗体

在Smobiler开发过程中,大家经常会对窗体的跳转,显示,关闭,生命周期存在一些不明白的地方,这篇文章主要用来说明Smobiler窗体。 Smobiler Form 和WindowsForm编程一样,在手机上显示的界面在Smobiler就是一个…