Vue单页应用中的数据同步探索

单页应用的一个特点就是即时响应,对发生变化数据实现 UI 的快速变更。实现的基础技术不外乎 AJAX 和 WebSocket,前者负责数据的获取和更新,后者负责变更数据的客户端同步。其中要解决的最主要的问题还是数据同步。

可以把这个问题拆分为两个具体问题:

数据共享:多个视图引用的数据能在发生变化后,即时响应变化。

数据同步:多终端访问的数据能在一个客户端发生变化后,即时响应变化。

发布订阅模式

在旧的项目中是使用了发布订阅模式解决这些问题。不管是 AJAX 请求的返回数据还是 WebSocket 的推送数据,统一向全局发布消息,每个需要这些数据的视图去订阅对应的消息使视图变化。

缺点是:一个视图为了响应变化需要写很多订阅并更新视图数据的硬编码,涉及数据越多,逻辑也越复杂。

数据流

对于 Vue,首先它是一个 MVVM 框架。

Model <----> ViewModel <----> View

一目了然的关系,Model 的变化影响到 ViewModel 的变化再触发 View 更新。那么反过来呢,View 更改 ViewModel 再更改 Model?

对于更新数据而言,更改 ViewModel 真是多此一举了。因为我们只需要改变 Model 数据自然就会按照Model > ViewModel > View的路径同步过来了。这也就是为什么 Vue 后来抛弃了双向绑定,而仅仅支持表单组件的双向绑定。对于双向绑定而言,表单算得上是最佳实践场景了。

在开发实践中,最常见的还是单向数据流。

Model --> ViewModel --> View --> Model

单向数据流告诉我们这样两样事:

不直接绑定 Model,而是使用由 1~N 个 Model 聚合的 ViewModel。

View 的变化永远去修改变更值对应的 Model。

Data Flow

解决数据问题的答案已经呼之欲出了。

多个视图引用的数据在发生变化后,如何响应变化?

保证多个 View 绑定的 ViewModel 中共同数据来自同一个Model。

多终端访问的数据在一个客户端发生变化后,如何响应变化?

首先多终端数据同步来源于 WebSocket 数据推送,要保证收到数据推送时去更改直接对应的 Model,而不是 ViewModel。

Vue中的解决方案

不只是要思想上解决问题,而且要代入到编程语言、框架等开发技术中实现。

Model的存放

Model 作为原始数据,即使用 AJAX GET 得到的数据,应该位于整个 Vue 项目结构的最上层。对于 Model 的存放位置,也有不同的选择。

非共享Model

不需要共享的 Model 可以放到视图组件的data中。但仍然避免 View 直接绑定 Model,即使该 View 的 ViewModel 不再需要额外的 Model 聚合。因为最终影响 View 呈现的不只是来自服务器的 Model 数据,还有视图状态ViewState。

来个:chestnut::一个简单的列表组件,负责渲染展示数据和关键字过滤功能。输入的过滤关键字和列表数据都作为 data 存放。

exportdefault{

data() {

return{

filterVal:'',

list: []

}

},

created() {

Ajax.getData().then(data=> {

this.list =data

})

},

methods: {

filter() {

this.list =this.list.filter(item =>item.name===this.filterVal)

}

}

}

试想一下,如果 View 直接绑定了以上代码中的list,那么在filter函数执行一次后,虽然 View 更新了,但同时list也被改变,不再是一个原始数据了,下一次执行filter函数将是从上一次的结果集中过滤。

很尴尬,总不能重新请求数据吧,那样还搞什么 SPA。

现在我们有了新的发现:ViewModel受Model和ViewState的双重影响。

ViewModel = 一个或多个 Model 组合 + 影响 View 展示的 ViewState

Vue 中有没有好的方法可以很好的描述这个表达式呢?那就是计算属性computed。

exportdefault{

data() {

return{

filterVal:'',

list: []

}

},

computed: {

viewList() {

returnthis.filterVal

?this.list.filter(item =>item.name===this.filterVal)

:this.list

}

},

created() {

Ajax.getData().then(data=> {

this.list =data

})

},

}

改写代码后,View 绑定计算属性viewList,有过滤关键字就返回过滤结果,否则返回原始数据。这才称得上是数据驱动。

共享Model

如果一个 View 中存在多处共享的 Model,那么毫不犹豫的使用 Vuex 吧。

对于复杂单页应用,可以考虑分模块管理,避免全局状态过于庞大。即使是共享的 Model 也是分属不同的业务模块和共享级别。

比如文档数据,可能只有/document起始路径下的视图需要共享。那么从节省内存的角度考虑,只有进入该路由时才去装载对应的 Vuex 模块。幸运的是 Vuex 提供的模块动态装载的 API。

对于共享级别高的数据,比如用户相关的数据,可以直接绑定到 Vuex 模块中。

store

| actions.js

| index.js

| mutations.js

+---global

| user.js

+---partial

| foo.js

| bar.js

分模块管理后,马上就会遇到跨模块调用数据的问题。一个 View 中需要的数据往往是全局状态和模块状态数据的聚合,可以使用getter解决这个问题。

exportdefault{

// ...

getters: {

viewData (state, getters, rootState) {

returnstate.data+ rootState.data

}

}

}

如果一个 View 是需要多个模块状态的数据呢?

exportdefault{

// ...

getters: {

viewData (state, getters) {

returnstate.data+ getters.partialData

}

}

}

虽然不能直接访问到其他模块的 state,但是getter和action、mutation都注册在全局命名空间,访问不受限制。

计算属性 vs Getter

Getter 与组件的计算属性拥有相同的作用,其中引用的任何 state 或者 getter 变化都会触发这个 getter 重新计算。

那么问题来了:什么时候我应当使用计算属性?什么时候使用 Getter?

这里其实是有一个数据前置原则:能放到上层的就不放到下层。

需要聚合多个 state 或 getter 时,使用 getter。如果有多个视图需要同样的数据组合就可以实现 getter 的复用。

需要聚合的数据中包含 ViewState 时,使用 computed。因为在 store 中无法访问 ViewState。

至此我们已经保证了应用内的任何一个共享数据最终都来源于某个全局状态或某个模块的状态。

Model的更新

Model 的更新有两种,一种是本地触发的更新,另一种是其他客户端更新再由服务器推送的更新。

可以这样表示:

Model = 本地原始数据 + 本地更新数据 + 推送数据

我们似乎又回到了那个列表组件类似的问题上。要不把 3 种数据都设为 state,由 3 种数据组合的 getter 来表示 Model?

现在来比较一下。另外有一个前提是 Vuex 只允许提交 mutation 来更改 state。

单State

对于一个 state 的更新不外乎是增、删、改、查四种情况,所以至少对应有 4 个 action 和 4 个 mutation,直接对表示源数据的 state 进行更改。

exportdefault{

state: {

data: []

},

mutations: {

init(state, payload) {

state.data= payload

},

add(state, payload) {

state.data.push(payload)

},

delete(state, payload) {

state.data.splice(state.data.findIndex(item=>item.id===payload), 1)

},

update(state, payload) {

Object.assign(state.data.find(item=>item.id===payload.id), payload)

}

},

actions: {

fetch({ commit }) {

Api.getData().then(data=> {

commit('init',data)

})

},

add({ commit }, item) {

Api.add(item).then(data=> {

commit('add',item)

})

},

delete({ commit }, id) {

Api.delete(id).then(data=> {

commit('delete',id)

})

},

update({ commit }, item) {

Api.update(item).then(data=> {

commit('update',item)

})

}

}

}

多State

如果把一个 Model 拆成多个 state,本地更新数据和推送数据统一为变更数据,对应到增、删、改、查四种情况,那就需要 4 个 state,即:originData、addData、deleteData、updateData。

mutation 和 action 到不会有什么变化,增、删、改原本就是分开写的,只是各自对应到不同的 state 上,最终的 Model 由一个 getter 来表示。

export default {

state: {

originData:[],

addData:[],

deleteData:[],

updateData:[]

},

getters:{

data(state) {

returnstate.originData.concat(state.addData) //add

.map(item => Object.assign(item,

state.updateData.find(uItem =>uItem.id===item.id))) //update

.filter(item => !state.deleteData.find(id => id ===item.id)) //delete

}

},

mutations:{

init(state, payload) {

state.originData = payload

},

add(state, payload) {

state.addData.push(payload)

},

delete(state, payload) {

state.deleteData.push(payload)

},

update(state, payload) {

state.updateData.push(payload)

}

},

actions:{

// 略...

}

}

这么一大串方法链看起来很酷对不对,但是性能呢?任何一个 state 的变更都将引起这个复杂的 getter 重新执行 5 个循环操作。

知乎上有个相关问题的讨论:JavaScript 函数式编程存在性能问题么?

其中提到的解决办法是惰性计算。相关的函数库有:lazy.js,或者使用 lodash 中的_.chain函数。

还有一种办法是统一为K, V数据结构,这样一个混合函数就搞定了Object.assign(originData, addData, updateData, deleteData)。

对比而言,我认为多 state 的方式更符合数据驱动及响应式编程思维,但需要有好的办法去解决复杂的循环操作这个问题,单 state 的方式就是面向大众了,两者都可以解决问题。甚至于全面使用响应式编程,使用RxJS替代 Vuex。

数据同步

前面提到过了,不管是本地更新数据还是服务端推送数据,可以统一为增、删、改三种接口。不管是本地更新还是推送数据,根据数据同步类型走同一个数据更改函数。

这在 Vuex 中很容易实现。利于 Vuex 的插件功能,可以在接受推送后提交到对应的 mutation。前提是要和后端约好数据格式,更方便的映射到对应的 mutationType,比如:{ 数据名,同步类型,同步数据 }。

exportdefaultstore => {

socket.on('data',data=> {

const{name,type,data} =data

store.commit(type+ name,data)

})

}

这样就实现了本地增、删、改与推送数据增、删、改的无差异化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,603评论 5 465
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,422评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,478评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,116评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,037评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,832评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,075评论 3 389
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,685评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,976评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,028评论 2 315
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,830评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,584评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,101评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,229评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,521评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,121评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,314评论 2 339

推荐阅读更多精彩内容