要解决的问题
HTML DOM, Mysql Table, ElaticSearch 等等技术都有一个自己对于状态的表现形式。 为了操作这些不同的表现形式,我们需要使用不同的API,不同的语法。 但是本质上来说,这些状态都可以用统一的抽象形式来表达。 当我们在表达状态,以及状态的变化的时候。可以把形式上的差异屏蔽掉,关注其本质,可以简化思维复杂度。
解决方案
从原表现形式,监听其改变事件,然后作用于目标表现形式。
构成
| 构成 | 解释 |
|---|---|
| source | 原表现形式 |
| target | 目标表现形式 |
| binder | 实现binding的中间人 |
解决方案案例
mobx-vue
DomainModel.ts
import { action, computed, observable } from "mobx";
export default class DomainModel {
@observable items: string[] = [];
@computed get itemsCount() {
return this.items.length;
}
@action.bound addItem(newItem: string) {
this.items.push(newItem);
}
@action.bound removeItem(item: string) {
this.items.splice(this.items.indexOf(item), 1);
}
}这里定义的是这个应用的业务逻辑。也就是其实质性的东西。而视图是对这份状态的一种呈现方式。
App.vue
<template>
<div>
<TitleBar :state="domainModel"/>
<ItemsList :state="domainModel"/>
<NewItemPanel :state="newItemPanelViewModel"/>
</div>
</template>
<script lang="ts">
import { observer } from "mobx-vue";
import DomainModel from "./DomainModel.ts";
import NewItemPanelViewModel from "./NewItemPanelViewModel.ts"
import TitleBar from "./TitleBar";
import ItemsList from "./ItemsList";
import NewItemPanel from "./NewItemPanel"
export default observer({
components: {TitleBar, ItemsList, NewItemPanel},
data() {
let domainModel = new DomainModel();
let newItemPanelViewModel = new NewItemPanelViewModel(domainModel);
return { domainModel, newItemPanelViewModel }
}
})
</script>TitleBar.vue
<template>
<h3>Total: {{ state.itemsCount }}</h3>
</template>
<script lang="ts">
import { observer } from "mobx-vue";
export default observer({
props: ['state']
})
</script>从 TitleBar 这里,我们可以看出来。View是对DomainModel做了一个单向的绑定。如果DomainModel更新了,View里的Total数量就会自动更新。
ItemsList.vue
<template>
<ul>
<li v-for="item in state.items"><a @click="state.removeItem(item)">[x]</a> {{ item }}</li>
</ul>
</template>
<script lang="ts">
import { observer } from "mobx-vue";
export default observer({
props: ['state']
})
</script>这里的 ItemsList 和 TitleBar 一样,都是一个视图,是对同一份数据的两份不同的呈现。一个展示了总数,一个展示了明细。都会被单向同步。
NewItemPanelViewModel.ts
import { action, computed, observable } from "mobx";
import DomainModel from "./DomainModel"
export default class NewItemPanelViewModel {
@observable domainModel: DomainModel;
@observable newItem = '';
constructor(domainModel: DomainModel) {
this.domainModel = domainModel;
}
@action.bound addItem() {
this.domainModel.addItem(this.newItem);
this.newItem = '';
}
}这里定义的是一个视图的私有的状态。它承上启下。 一方面和NewItemPanel这个视图进行了连接。 另外一方面和DomainModel进行了连接。 然后通过DomainModel,它又与TitleBar和ItemsList这两个视图进行了间接的连接关系。
NewItemPanel.vue
<template>
<div>
<input type="text" v-model="state.newItem" />
<button @click="state.addItem()">Add</button>
</div>
</template>
<script lang="ts">
import { observer } from "mobx-vue";
export default observer({
props: ['state']
})
</script>这里的 NewItemPanel 和 NewItemPanelViewModel 的关系是双向绑定。其实就是用 ViewModel 来做为 View 的替身。
这样我们所有需要取的数据,不用管View了,直接从ViewModel上取。
我们所有对View希望作的更新,也不用更新View了,直接更新到ViewModel上。
两者之间的同步,由 vue 框架 v-model="state.newItem" 来保证。
| 构成 | 解释 |
|---|---|
| source | HTML DOM |
| target | Javascript Object Model |
| binder | vue + mobx |
vue
App.vue
<template>
<div>
<h3>Todo: {{ items.length }}</h3>
<ul>
<li v-for="item in items">{{ item }}</li>
</ul>
<input type="text" v-model="newItem"/>
<button @click="addNewItem">add</button>
</div>
</template>
<script lang="ts">
export default {
data() {
return {
items: [],
newItem: ''
}
},
methods: {
addNewItem() {
this.items.push(this.newItem)
this.newItem = ''
}
}
}
</script>vue 没有 mobx + vue 那么灵活。状态和视图必须在一个component里。如果要多个视图共享一个后端状态,就需要其他库的协助。
| 构成 | 解释 |
|---|---|
| source | HTML DOM |
| target | Javascript Object Model |
| binder | vue |