原文链接, 如果感兴趣可以加QQ群: 157937068, 一起交流。
本次分享是带大家了解什么是 mvvm,mvvm 的原理,以及近几年产生了哪些演变。
同时借 mvvm 这个话题拓展到对各类前端数据流方案的思考,形成对前端数据流整体认知,帮助大家在团队中更好的做技术选型。
Mvvm 的概念与发展
Mvvm & 单向数据流
Mvvm 是指双向数据流,即 View-Model 之间的双向通信,由 ViewModel 作桥接。如下图所示:
而单向数据流则去除了 View -> Model 这一步,需要由用户手动绑定。
生态 - 内置 & 解耦
许多前端框架都内置了 Mvvm 功能,比如 Knockout、Angular、Ember、Avalon、Vue、San 等等。
而就像 Redux 一样,Mvvm 框架中也出现了许多与框架解耦的库,比如 Mobx、Immer、Dob 等,这些库需要一个中间层与框架衔接,比如 mobx-react、redux-box、dob-react。解耦让框架更专注 View 层,实现了库与框架灵活搭配的能力。
解耦的数据流框架也诠释了更高抽象级别的 Mvvm 架构,即:View - 前端框架,Model - (mobx, dob),ViewModel - (mobx-react, dob-react)。
同时也实现了数据与框架分离,便于测试与维护。比如下面的例子,左边是框架无关的纯数据/数据操作定义,右边是 View + ViewModel:
运行效率 - 脏检测 & getter/setter 劫持
Angluar 早期的脏检测机制虽然开创了 mvvm 先河,但监听效率比较低,需要 N + 1 次确认数据是否有联动变化,就像下图所示:
现在几乎所有框架都改为 getter/setter 劫持实现监听,任何数据的变化都可以在一个事件循环周期内完成:
语法 - 特殊语法 & 原生语法
早期一些 Mvvm 框架需要手动触发视图刷新,现在这种做法几乎都被原生赋值语句取代。
数据变更方式 - Mutable & Immutable
下图的代码语法虽为 mutable,但产生的结果可能是 mutable,也可能是 immutable,取决于 mvvm 框架内置实现机制:
Connect 的两种写法
由于 mvvm 支持了 mutable 与 immutable 两种写法,所以对于 mutable 的底层,我们使用左图的 connect 语法,对于 immutable 的底层,需要使用右图的 conenct 语法:
[图片上传失败...(image-b7408b-1522595335875)]
对左图而言,由于 mutable 驱动,所有数据改动会自动调用视图刷新,因此不但更新可以一步到位,而且可以数据全量注入,因为没用到的变量不会导致额外渲染。
对右图,由于 immutable 驱动,本身并没有主动驱动视图刷新能力,所以当右下角节点变更时,会在整条链路产生新的对象,通过 view 更新机制一层层传导到要更新的视图。
从 TFRP 到 mvvm
讲到 mvvm 的原理,先从 TFRP 说起,详细可以参考 dob-框架实现 这里以 dob 框架为例子,一步步介绍了如何实现 mvvm。本文简单做个介绍。
autorun & reaction
autorun 是 TFRP 的函数效果,即集成了依赖收集与监听,autorun 背后由 reaction 实现。
reaction 实现 autorun
如下图所示,autorun 是 subscription 套上 track 的 reaction,并且初始化时主动 dispatch,从入口(subscription)处激活循环,完成 subscription -> track -> 监听修改 -> subscription 完成闭环。
track 的实现
每个 track 在其执行期间会监听 callback 的 getter 事件,并将 target 与 properityKey 存储在二维 Map 中,当任何 getter 触发后,从这个二维表中查询依赖关系,即可找到对应的 callback 并执行。
View-Model 的实现
由于 autorun 与 view 的 render 函数很像,我们在 render 函数初始化执行时,使其包裹在 autorun 环境中,第 2 次 render 开始遍剥离外层的 autorun,保证只绑定一遍数据。
这样 view 层在原本 props 更新机制的基础上,增加了 autorun 的功能,实现修改任何数据自动更新对应 view 的效果。
Mvvm 的缺点与解法?
Mvvm 所有已知缺点几乎都有了解决方案。
无法监听新增属性
用过 Mobx 的同学都知道,给 store 添加一个不存在的属性,需要使用 extendObservable
这个方法。这个问题在 Dob 与 Mobx4.0 中都得到了解决,解决方法就是使用 proxy
替代 Object.defineProperty
:
异步问题
由于 getter/setter 无法获得当前执行函数,只能通过全局变量方式解决,因此 autorun 的 callback 函数不支持异步:
嵌套问题
由于 reaction 特性,只支持同步 callback 函数,因此 autorun 发生嵌套时,很可能会打乱依赖绑定的顺序。解决方案是将嵌套的 autorun 放到执行队列尾部,如下图所示:
无数据快照
mutable 最被人诟病的一点就是无法做数据快照,不能像 redux 一样做时间回溯。有问题自然有人会解决,Mobx 作者的 Immer 库完美的解决了问题。
原理是通过 proxy 返回代理对象,在内部通过浅拷贝替代对对象的 mutable 更改。具体原理可以参考我的这篇文章:精读 Immer.js 源码
无副作用隔离
mvvm 函数的 Action 由于支持异步,许多人会在 Action 中发请求,同时修改 store,这样就无法将请求副作用隔离到 store 之外。同时对 store 的 mutable 修改,本身也是一种副作用。
虽然可以将请求函数拆分到另一个 Action 中,但人为因素无法完全避免。
自从有了 Immer.js 之后,至少从支持元编程的角度来看,mutable 并不一定会产生副作用,它可以是零副作用的:
function inc(obj) {
return produce(obj => obj.count++)
}
上面这种看似 mutable 的写法其实是零副作用的纯函数,和下面写法等价:
function inc(obj) {
return {
count: obj.count + 1,
...obj
}
}
而对副作用的隔离,也可以做出类似 dva 的封装:
Mvvm store 组织形式
Mvvm 在项目中 stores 代码结构也千变万化,这里列出 4 种常见形式。
对象形式,代表框架 – mobx
mobx 开创了最基本的 mvvm store 组织形式,基本也是各内置 mvvm 框架的 store 组织形式。
Class + 注入,代表框架 – dob
dob 在 store 组织形式下了不少功夫,通过依赖注入增强了 store 之间的关联,实现 stores -> action 多对一的网状结构。
数据结构化,代表框架 – mobx-state-tree
mobx-state-tree 是典型结构化 store 组织的代表,这种组织形式适合一体化 app 开发,比如很多页面之间细粒度数据需要联动。
约定与集成,代表框架 – 类 dva
类 dva 是一种集成模式,是针对 redux 复杂的样板代码,思考形成的简化方案,自然集成与约定是简化的方向。
另外这种方案更像一层数据 dsl,得益于此,同一套代码可以拥有不同的底层实现。
Mvvm vs Reactive programming
Mvvm 与 Reactive programming 都拥有 observable 特性,通过下面两张图可以轻松区分:
上面红线是 mvvm 的 observable 部分,这里指的是数据变化的 autorun 动作。
上面红线是 Reactive programming 的 observable 部分,指的是数据源派发流的过程。
Mvvm 与 Reactive programming 的结合
既然 redux 可以与 rxjs 结合(redux-observable),那么 mvvm 应该也可以如此。
下面是这种方案的构想:
rxjs 仅用来隔离副作用与数据处理,mvvm 拥有修改 store 的能力,并且精准更新使用的 View。
总结
根据业务场景指定数据流方案,数据流方案没有银弹,只有贴着场景走,才能找到最合适的方案。
了解到 mvvm 的发展与演进,让不同数据流方案组合,你会发现,数据流方案还有很多。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。