软件架构设计模式
有三种主要的设计模式分别是mvvm和mvc以及mvp设计模式,主要通过分离关注点的方式来组织代码结构,优化我们的开发效率
Contraller/Presenter负责业务逻辑,Model负责管理数据。View负责显示。
假设我们在进行项目开发的时候,使用单页应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里,如果项目过大,那么代码就会非常冗长,难以维护。
mvc模式
- MVC通过分离Model、View和Contraller的方式来组织代码结构。其中View负责页面的显示逻辑,Model负责存储页面的业务数据以及对相应数据的操作。
- 并且View和Model应用了观察者模式,当Model层发生改变的时候,他会通知有关View层更新页面
- Contraller层是View层和Model层的纽带,主要负责用户与应用的响应操作,View和Model是相互解耦的,他们不直接交互,而是通过Controller来进行通信。当用户与页面产生交互的时候,Controller根据事件的类型和用户输入处理相关的业务逻辑,并更新Model中的数据。然后Model层再去通知View层更新。
- 在MVC中,View需要主动从Model中获取数据,并由Controller负责将数据传递给View进行展示。这使得开发者需要手动编写代码来同步更新数据和UI,相对来说,MVVM的数据绑定机制在这方面更加简化了开发流程。
在开发过程中十分繁琐,不灵活,都是单向的,一个小的事件操作,都必须要经过这样的流程,开发不便捷
这种模式在开发中更加灵活,但是这种灵活也伴随着问题:
-
数据流混乱:
-
View比较庞大,而Controller比较单薄:由于很多开发者都会在View中写一些逻辑代码,导致View中的内容越来越庞大,而Controller变得越来越单薄(单纯的数据监听)
mvp模式
- MVP通过分离Model、View和Presenter的方式来组织代码结构其中View负责页面的显示逻辑,Model负责存储页面的业务数据以及对相应数据的操作。
- 并且View和Model应用了观察者模式,当Model层发生改变的时候,他会通知有关View层更新页面
- MVP模式与MVC唯一不同的在于Presenter和Contraller
- 在MVC模式中我们使用观察者模式,来实现Model层数据发生变化时通知View层的更新,这样View层和Model层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,可能会对代码的复用性造成一些问题。
- MVP模式通过使用Presenter来实现对View层和Model层的解耦,MVC中的Contraller只知道Model的接口,因此它没有办法控制View层的更新,MVP模式中,View层的接口暴露给了Presenter,因此可以在Presenter中将Model的变化和View的变化绑定在一起,以此来实现View和Model的同步更新,实现了对View和Model的解耦
- Presnter还包含了其他的响应的逻辑
MVVM模式
-
MVVM通过分离Model、View和ViewModel的方式来组织代码结构其中
-
View:是用户界面的可视化部分,负责展示数据并与用户进行交互。View通常由XML、HTML、XAML等描述,这取决于具体的开发平台。在ArkUI中为@Components修饰组件渲染的UI
-
Model:表示应用程序的数据模型或业务逻辑,负责处理数据的存取、处理和操作。它通常包含数据结构、数据库操作、网络请求等。Model并不直接与UI层交互,它只暴露一些接口供ViewModel层调用,使得ViewModel可以获取所需的数据。
-
ViewModel:层类似媒婆,起到了牵线搭桥的作用,负责将数据从Model中取出并转换成View可用的形式。ViewModel不直接操作View,而是通过数据绑定机制将数据与View进行绑定,使得数据的变化可以自动反映在View上,实现了数据的双向绑定。在ArkUI中,ViewModel是存储在自定义组件的状态变量。
- 自定义组件通过执行其build()方法或者@Builder装饰的方法来渲染UI,即ViewModel可以渲染View。
- View可以通过相应event handler来改变ViewModel,即事件驱动ViewModel的改变,另外ViewModel提供了@Watch回调方法用于监听状态数据的改变。
- 在ViewModel被改变时,需要同步回Model层,这样才能保证ViewModel和Model的一致性,即应用自身数据的一致性。
- ViewModel结构设计应始终为了适配自定义组件的构建和更新,这也是将Model和ViewModel分开的原因。
- ViewModel通常也包含用户交互的逻辑,例如处理用户输入、按钮点击等
- 它的思想和MVP其实是相同的,不过ViewModel通过双向的数据绑定,将View和Model的同步更新给自动化了,当Model发生变化时,ViewModel就会自动更新,ViewModel变化了,View也会更新,这样就将Presenter中的工作给自动化了。
-
MVVM在保持View和Model松耦合的同时,还减少了维护它们关系的代码,使用户专注于业务逻辑,兼顾开发效率和可维护性
ViewModel的数据源
ViewModel包含多个数据源。@State和@Provide装饰的变量以及LocalStorage和AppStorage都是顶层数据源,其余装饰器都是与数据源做同步的数据。装饰器的选择取决于状态需要在自定义组件之间的共享范围。共享范围从小到大的排序是:
- @State:组件级别的共享,通过命名参数机制传递,例如:CompA: ({ aProp: this.aProp }),表示传递层级(共享范围)是父子之间的传递。
- @Provide:组件级别的共享,可以通过key和@Consume绑定,因此不用参数传递,实现多层级的数据共享,共享范围大于@State。
- LocalStorage:页面级别的共享,可以通过@Entry在当前组件树上共享LocalStorage实例。
- AppStorage:应用全局的UI状态存储,和应用进程绑定,在整个应用内的状态数据的共享。
@State装饰的变量与一个或多个子组件共享状态数据
@State可以初始化多种状态变量,@Prop、@Link和@ObjectLink可以和其建立单向或双向同步。
- 使用Parent根节点中@State装饰的testNum作为ViewModel数据项。将testNum传递给其子组件LinkChild和Sibling。
// xxx.ets
@Entry
@Component
struct Parent {
@State @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${this.testNum}`)
}
build() {
Column() {
LinkChild({ testNum: $testNum })
Sibling({ testNum: $testNum })
}
}
}
- LinkChild和Sibling中用@Link和父组件的数据源建立双向同步。其中LinkChild中创建了LinkLinkChild和PropLinkChild。
@Component
struct Sibling {
@Link @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${this.testNum}`);
}
build() {
Text(`Sibling: ${this.testNum}`)
}
}
@Component
struct LinkChild {
@Link @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${this.testNum}`);
})
Text(`LinkChild: ${this.testNum}`)
LinkLinkChild({ testNumGrand: $testNum })
PropLinkChild({ testNumGrand: this.testNum })
}
.height(200).width(200)
}
}
- LinkLinkChild和PropLinkChild声明如下,PropLinkChild中的@Prop和其父组件建立单向同步关系。
@Component
struct LinkLinkChild {
@Link @Watch("testNumChange") testNumGrand: number;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`LinkLinkChild: ${this.testNumGrand}`)
}
}
@Component
struct PropLinkChild {
@Prop @Watch("testNumChange") testNumGrand: number = 0;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
当LinkChild中的@Link testNum更改时。
- 更改首先同步到其父组件Parent,然后更改从Parent同步到Sibling。
- LinkChild中的@Link testNum更改也同步给子组件LinkLinkChild和PropLinkChild。
@State装饰器与@Provide、LocalStorage、AppStorage的区别:
- @State如果想要将更改传递给孙子节点,需要先将更改传递给子组件,再从子节点传递给孙子节点。
- 共享只能通过构造函数的参数传递,即命名参数机制CompA: ({ aProp: this.aProp })。
完整的代码示例如下:
@Component
struct LinkLinkChild {
@Link @Watch("testNumChange") testNumGrand: number;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`LinkLinkChild: ${this.testNumGrand}`)
}
}
@Component
struct PropLinkChild {
@Prop @Watch("testNumChange") testNumGrand: number = 0;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
@Component
struct Sibling {
@Link @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${this.testNum}`);
}
build() {
Text(`Sibling: ${this.testNum}`)
}
}
@Component
struct LinkChild {
@Link @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${this.testNum}`);
})
Text(`LinkChild: ${this.testNum}`)
LinkLinkChild({ testNumGrand: $testNum })
PropLinkChild({ testNumGrand: this.testNum })
}
.height(200).width(200)
}
}
@Entry
@Component
struct Parent {
@State @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${this.testNum}`)
}
build() {
Column() {
LinkChild({ testNum: $testNum })
Sibling({ testNum: $testNum })
}
}
}
@Provide装饰的变量与任何后代组件共享状态数据
@Provide装饰的变量可以与任何后代组件共享状态数据,其后代组件使用@Consume创建双向同步,详情见@Provide和@Consume。
因此,@Provide-@Consume模式比使用@State-@Link-@Link从父组件将更改传递到孙子组件更方便。@Provide-@Consume适合在单个页面UI组件树中共享状态数据。
使用@Provide-@Consume模式时,@Consume和其祖先组件中的@Provide通过绑定相同的key连接,而不是在组件的构造函数中通过参数来进行传递。
以下示例通过@Provide-@Consume模式,将更改从父组件传递到孙子组件。
@Component
struct LinkLinkChild {
@Consume @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNum value ${this.testNum}`);
}
build() {
Text(`LinkLinkChild: ${this.testNum}`)
}
}
@Component
struct PropLinkChild {
@Prop @Watch("testNumChange") testNumGrand: number = 0;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
@Component
struct Sibling {
@Consume @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${this.testNum}`);
}
build() {
Text(`Sibling: ${this.testNum}`)
}
}
@Component
struct LinkChild {
@Consume @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${this.testNum}`);
})
Text(`LinkChild: ${this.testNum}`)
LinkLinkChild({ /* empty */ })
PropLinkChild({ testNumGrand: this.testNum })
}
.height(200).width(200)
}
}
@Entry
@Component
struct Parent {
@Provide @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${this.testNum}`)
}
build() {
Column() {
LinkChild({ /* empty */ })
Sibling({ /* empty */ })
}
}
}
给LocalStorage实例中对应的属性建立双向或单向同步
通过@LocalStorageLink和@LocalStorageProp,给LocalStorage实例中的属性建立双向或单向同步。可以将LocalStorage实例视为@State变量的Map。
LocalStorage对象可以在ArkUI应用程序的几个页面上共享。因此,使用@LocalStorageLink、@LocalStorageProp和LocalStorage可以在应用程序的多个页面上共享状态。
以下示例中:
- 创建一个LocalStorage实例,并通过@Entry(storage)将其注入根节点。
- 在Parent组件中初始化@LocalStorageLink(“testNum”)变量时,将在LocalStorage实例中创建testNum属性,并设置指定的初始值为1,即@LocalStorageLink(“testNum”) testNum: number = 1。
- 在其子组件中,都使用@LocalStorageLink或@LocalStorageProp绑定同一个属性名key来传递数据。
LocalStorage可以被认为是@State变量的Map,属性名作为Map中的key。
@LocalStorageLink和LocalStorage中对应的属性的同步行为,和@State和@Link一致,都为双向数据同步。
以下为组件的状态更新图:
@Component
struct LinkLinkChild {
@LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNum value ${this.testNum}`);
}
build() {
Text(`LinkLinkChild: ${this.testNum}`)
}
}
@Component
struct PropLinkChild {
@LocalStorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
@Component
struct Sibling {
@LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${this.testNum}`);
}
build() {
Text(`Sibling: ${this.testNum}`)
}
}
@Component
struct LinkChild {
@LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${this.testNum}`);
})
Text(`LinkChild: ${this.testNum}`)
LinkLinkChild({ /* empty */ })
PropLinkChild({ /* empty */ })
}
.height(200).width(200)
}
}
// create LocalStorage object to hold the data
const storage = new LocalStorage();
@Entry(storage)
@Component
struct Parent {
@LocalStorageLink("testNum") @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${this.testNum}`)
}
build() {
Column() {
LinkChild({ /* empty */ })
Sibling({ /* empty */ })
}
}
}
给AppStorage中对应的属性建立双向或单向同步
AppStorage是LocalStorage的单例对象,ArkUI在应用程序启动时创建该对象,在页面中使用@StorageLink和@StorageProp为多个页面之间共享数据,具体使用方法和LocalStorage类似。
也可以使用PersistentStorage将AppStorage中的特定属性持久化到本地磁盘的文件中,再次启动的时候@StorageLink和@StorageProp会恢复上次应用退出的数据。
示例如下:
@Component
struct LinkLinkChild {
@StorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNum value ${this.testNum}`);
}
build() {
Text(`LinkLinkChild: ${this.testNum}`)
}
}
@Component
struct PropLinkChild {
@StorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
@Component
struct Sibling {
@StorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${this.testNum}`);
}
build() {
Text(`Sibling: ${this.testNum}`)
}
}
@Component
struct LinkChild {
@StorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${this.testNum}`);
})
Text(`LinkChild: ${this.testNum}`)
LinkLinkChild({ /* empty */
})
PropLinkChild({ /* empty */
})
}
.height(200).width(200)
}
}
@Entry
@Component
struct Parent {
@StorageLink("testNum") @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${this.testNum}`)
}
build() {
Column() {
LinkChild({ /* empty */
})
Sibling({ /* empty */
})
}
}
}
ViewModel的嵌套场景
大多数情况下,ViewModel数据项都是复杂类型的,例如,对象数组、嵌套对象或者这些类型的组合。对于嵌套场景,可以使用@Observed搭配@Prop或者@ObjectLink来观察变化。
@Prop和@ObjectLink嵌套数据结构
推荐设计单独的@Component来渲染每一个数组或对象。此时,对象数组或嵌套对象(属性是对象的对象称为嵌套对象)需要两个@Component,一个@Component呈现外部数组/对象,另一个@Component呈现嵌套在数组/对象内的类对象。 @Prop、@Link、@ObjectLink修饰的变量只能观察到第一层的变化。
- 对于类:
- 可以观察到赋值的变化:this.obj=new ClassObj(…)
- 可以观察到对象属性的更改:this.obj.a=new ClassA(…)
- 不能观察更深层级的属性更改:this.obj.a.b = 47
- 对于数组:
- 可以观察到数组的整体赋值:this.arr=[…]
- 可以观察到数据项的删除、插入和替换:this.arr[1] = new ClassA()、this.arr.pop()、 this.arr.push(new ClassA(…))、this.arr.sort(…)
- 不能观察更深层级的数组变化:this.arr[1].b = 47
如果要观察嵌套类的内部对象的变化,可以使用@ObjectLink或@Prop。优先考虑@ObjectLink,其通过嵌套对象内部属性的引用初始化自身。@Prop会对嵌套在内部的对象的深度拷贝来进行初始化,以实现单向同步。在性能上@Prop的深度拷贝比@ObjectLink的引用拷贝慢很多。
@ObjectLink或@Prop可以用来存储嵌套内部的类对象,该类必须用@Observed类装饰器装饰,否则类的属性改变并不会触发更新,UI并不会刷新。@Observed为其装饰的类实现自定义构造函数,此构造函数创建了一个类的实例,并使用ES6代理包装(由ArkUI框架实现),拦截修饰class属性的所有“get”和“set”。“set”观察属性值,当发生赋值操作时,通知ArkUI框架更新。“get”收集哪些UI组件依赖该状态变量,实现最小化UI更新。
如果嵌套场景中,嵌套数据内部是数组或者class时,需根据以下场景使用@Observed类装饰器。
- 如果嵌套数据内部是class,直接被@Observed装饰。
- 如果嵌套数据内部是数组,可以通过以下方式来观察数组变化。
@Observed class ObservedArray<T> extends Array<T> {
constructor(args: T[]) {
if (args instanceof Array) {
super(...args);
} else {
super(args)
}
}
/* otherwise empty */
}
ViewModel为外层class。
class Outer {
innerArrayProp : ObservedArray<string> = [];
...
}
嵌套数据结构中@Prop和@ObjectLink之的区别
以下示例中:
- 父组件ViewB渲染@State arrA:Array。@State可以观察新数组的分配、数组项插入、删除和替换。
- 子组件ViewA渲染每一个ClassA的对象。
- 类装饰器@Observed ClassA与@ObjectLink a: ClassA。
- 可以观察嵌套在Array内的ClassA对象的变化。
- 不使用@Observed时:
- ViewB中的this.arrA[Math.floor(this.arrA.length/2)].c=10将不会被观察到,相应的ViewA组件也不会更新。
对于数组中的第一个和第二个数组项,每个数组项都初始化了两个ViewA的对象,渲染了同一个ViewA实例。在一个ViewA中的属性赋值this.a.c += 1;时不会引发另外一个使用同一个ClassA初始化的ViewA的渲染更新。
let NextID: number = 1;
// 类装饰器@Observed装饰ClassA
@Observed
class ClassA {
public id: number;
public c: number;
constructor(c: number) {
this.id = NextID++;
this.c = c;
}
}
@Component
struct ViewA {
@ObjectLink a: ClassA;
label: string = "ViewA1";
build() {
Row() {
Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`)
.onClick(() => {
// 改变对象属性
this.a.c += 1;
})
}
}
}
@Entry
@Component
struct ViewB {
@State arrA: ClassA[] = [new ClassA(0), new ClassA(0)];
build() {
Column() {
ForEach(this.arrA,
(item: ClassA) => {
ViewA({ label: `#${item.id}`, a: item })
},
(item: ClassA): string => { return item.id.toString(); }
)
Divider().height(10)
if (this.arrA.length) {
ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] })
ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] })
}
Divider().height(10)
Button(`ViewB: reset array`)
.onClick(() => {
// 替换整个数组,会被@State this.arrA观察到
this.arrA = [new ClassA(0), new ClassA(0)];
})
Button(`array push`)
.onClick(() => {
// 数组中插入数据,会被@State this.arrA观察到
this.arrA.push(new ClassA(0))
})
Button(`array shift`)
.onClick(() => {
// 数组中移除数据,会被@State this.arrA观察到
this.arrA.shift()
})
Button(`ViewB: chg item property in middle`)
.onClick(() => {
// 替换数组中的某个元素,会被@State this.arrA观察到
this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11);
})
Button(`ViewB: chg item property in middle`)
.onClick(() => {
// 改变数组中某个元素的属性c,会被ViewA中的@ObjectLink观察到
this.arrA[Math.floor(this.arrA.length / 2)].c = 10;
})
}
}
}
在ViewA中,将@ObjectLink替换为@Prop。
@Component
struct ViewA {
@Prop a: ClassA = new ClassA(0);
label : string = "ViewA1";
build() {
Row() {
Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`)
.onClick(() => {
// change object property
this.a.c += 1;
})
}
}
}
与用@Prop修饰不同,用@ObjectLink修饰时,点击数组的第一个或第二个元素,后面两个ViewA会发生同步的变化。
@Prop是单向数据同步,ViewA内的Button只会触发Button自身的刷新,不会传播到其他的ViewA实例中。在ViewA中的ClassA只是一个副本,并不是其父组件中@State arrA : Array中的对象,也不是其他ViewA的ClassA,这使得数组的元素和ViewA中的元素表面是传入的同一个对象,实际上在UI上渲染使用的是两个互不相干的对象。
需要注意@Prop和@ObjectLink还有一个区别:@ObjectLink装饰的变量是仅可读的,不能被赋值;@Prop装饰的变量可以被赋值。
- @ObjectLink实现双向同步,因为它是通过数据源的引用初始化的。
- @Prop是单向同步,需要深拷贝数据源。
- 对于@Prop赋值新的对象,就是简单地将本地的值覆写,但是对于实现双向数据同步的@ObjectLink,覆写新的对象相当于要更新数据源中的数组项或者class的属性,这个对于 TypeScript/JavaScript是不能实现的。
总结
- 这三者都是框架模式,他们设计的目标都是为了解决Model和View的耦合问题
- MVC模式它的优点是分层清晰,缺点是数据流混乱,灵活性带来的维护性问题
- MVP模式是MVC的进化形式,Presenter作为中间层负责MV通信,解决了两者耦合问题,但P层过于臃肿会导致维护问题
- MVVM模式在前端领域有广泛应用,不仅解决MV耦合问题,还同时解决了两者映射关系的大量繁杂代码,在提高开发效率、可读性同时还保持了优越的性能表现。