vue.js 双向绑定 DOM 和 View Model
我们从这段代码开始
App.vue
<template>
<div>
<h3>Todo</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.js 提供的价值是
- 当 HTML 页面上的输入框变化的时候,同步其内容到 newItem
- 当代码里更新了 newItem 的内容的时候,同步反映到 HTML 页面上
这种双向绑定,使得“业务逻辑”代码 addNewItem 与具体的输入输出技术无关。在这里,其外部呈现是一个 HTML 页面,我们同样可以让相同的代码绑定到 Android 的 UI 上。从 DDD 到六边形架构,从六边形架构到洋葱架构,从洋葱架构到Clean Architecture,各位老程序员孜孜以求的就是这样的架构属性,让“业务逻辑”与特定的输入输出技术无关。
![99b5da117db94c1e7764a7c7b3d94a00.png](https://i-blog.csdnimg.cn/blog_migrate/9f63a291cb1d6ac3ab66a611f01a818f.jpeg)
通过双向绑定,用 ViewModel 完全代表外部的 HTML DOM,参与到业务逻辑的交互中来。
“世界”随之更新
我们再来看第二段代码
<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>
与上面那段代码的区别是增加了 Todo: {{ items.length }}。在 addNewItem 更新了 this.items 之后
- 列表会被刷新,反映最新的 items 情况
- 标题会被刷新,反映 items 的总数
vue.js 提供的价值在于 “世界”都随之更新。你不用管这个世界如何运转,你只管做你想做的,世界会自己更新自己。为什么我们会觉得这样的编程体验很爽?因为懒。这种写法更符合人的认知习惯。
![3de20428c76187484cbbde01d2f71eeb.png](https://i-blog.csdnimg.cn/blog_migrate/a80c18e5d6484309831a15b446c3b6e5.jpeg)
“全局”状态刷新
把这种爽的编程体验更往上推进一层的是 mobx + vue。vue 提供的是对单个组件的响应式绑定。mobx 提供的是全局的响应式绑定。当全局状态更新的时候,所有绑定在其上的组件都会随之更新。
限于篇幅这里就不贴全部的源代码了(taowen/toolchain)
<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,ItemsList 和 NewItemPanel 三个组件而已。
![4023a599f986c4f2d9a6c28404fe8b27.png](https://i-blog.csdnimg.cn/blog_migrate/cf54541e78885a4da322dccffebd13fa.jpeg)
Mobx + Vue 打破了 Vue 对状态绑定的限制,从而使得我们可以彻底地把业务逻辑和输入输出技术分开。
![e98b1b7236e53093d55b9f0a3090fccd.png](https://i-blog.csdnimg.cn/blog_migrate/8c8cd69837c882fe778cdc947f70060e.jpeg)
vue.js 会是那颗银弹吗?
听起来 vue.js 已经完美解决了老一辈程序员毕生奋斗去简化编程体验的目标。当然仅仅是“听起来”。故事远没有那么简单。首先我们回顾一下什么叫“银弹”
在 Fred Brooks http://worrydream.com/refs/Brooks-NoSilverBullet.pdf 中,提到的银弹是有明确定义的:
there is no single development, in either technology or management technique, which by itself promises even one order-of-magnitude improvement within a decade in producitivity, in reliability, in simplicity.
简单来说就是可以把开发效率提升10倍的工具或者管理手段。
Fred Brooks 本人在 2007 年的 OOPSLA 会议上,参与了一个由 Martin Fowler 等人参与的高端圆桌会议。他回顾了 no silver bullet 这篇文章
I made a bold statement in the paper: no single technique will cause an order of magnitude improvement in productivity in the next ten years (1986-1996). This has turned out true. If there is one technique that has made a significant improvement, it is OO programming.
也就是他语言的 1986 ~ 1996 这个时间段内,整个行业确实是没有技术提供了10倍的效率改进。
所以不要望文生义了,银弹不是说要把成本降为0,它有一个明确的又随意的指标,10倍。
问题的核心在于现在的总耗时里面,哪部分是有最大水分,最有提高空间的。我认为由复杂性引发的程序员本人的认知困难,进而因为认知困难引起的团队沟通困难是最有改进空间的地方。其本质在于,我们对于“程序员认知软件行为”这个过程是缺乏认知的。
我在 陶文:如何提高代码的可读性 中,从这个角度对程序员的认知行为做过一些分析。在这里,重复几个关键的观点。
![ea9305a2ca7e701db364f6acba1ad00a.png](https://i-blog.csdnimg.cn/blog_migrate/6d34eafb0588fe036e62a9589585b68c.jpeg)
我认为,人在思考计算机行为的时候,其底层操作系统是真实世界模拟。我们在脑海中假设自己是一只小白鼠,去探索一个迷宫。这个对于“我”的认知,就映射为指向当前程序所执行到的位置的那个指针。而“我”只有一个,所以我们的思维始终是单线程的。
![eed04bcaf723c2a0f298b8692aacb1bf.png](https://i-blog.csdnimg.cn/blog_migrate/ed9b2a0cfbda2d5efc4d2e41af18043a.jpeg)
第二个关键点是“我”与“世界”。人类会把自己放在一个环境中去思考。想象你在一个公园里面漫步,你对这个公园会由一个地图的概念。如果只有“我”,没有“世界”。或者“世界”的今天和明天差别很大,那就会颠倒三观,感到认知遇到困难。
![6392901c387046ebcea0985257479b78.png](https://i-blog.csdnimg.cn/blog_migrate/9e8a66612364fab8f8b942a70b07439b.jpeg)
第三个关键点是我们预期这个世界有其底层运行规律。比如,图中的秋千你碰了它,它就会摆动。我们需要去安排每个原子如何运行么?不需要,我们只需要参与其中,每个原子都会根据物理规律自己安排自己的命运。就是,我们只管干我们自己的事情,这个世界会自己运转。
![30cb341d19301c464ff8ffa538856b5e.png](https://i-blog.csdnimg.cn/blog_migrate/f6db13104b1f159ead91f9493b87d162.jpeg)
也就是说,你有你自己的agenda,你会有一个原因到结果的 causality line,因果关系线索。其他人,其他事物,也各自有各自的agenda,它们有自己的 causality line。
所以什么叫符合人类的认知习惯?就是分开描述因果关系。每个事物各自都有自己的因果关系线。因为这是这个物理世界的运作机制,也是人类的认知能力的来源。从控制复杂度的角度来说,分开描述,每条因果关系先自身可以做到很简单。比如牛顿的三大定律,就描述了普适而又简单的因果关系。
vue.js 做得好的地方恰恰是这一点。它构建了一个世界,世界本身有自己的运作机制(绑定关系)。然后 UI 事件触发的 method,就像一个小老鼠,在这个世界的迷宫里进行探索,与这个世界发生相互作用。这种与人类认知模式的深层契合,是其力量的根本源泉。
与 vue.js 相对应的就是 jquery 命令式的逻辑表达方式。我们要知道输入,然后一个命令一个命令地去更新多个地方,以维护“业务规则的平衡”。比如 ul 列表里的 li 数量,要和标题上的 todo 个数对应。回到这个隐喻
![ea9305a2ca7e701db364f6acba1ad00a.png](https://i-blog.csdnimg.cn/blog_migrate/6d34eafb0588fe036e62a9589585b68c.jpeg)
我们不再是那只轻盈的小老鼠去探索这个世界。而是把整个世界扛在了自己背包里,一路走,一路去把世界装进背包,又从背包里还原这个世界。
![734bf99a71853c4ff5a01905d9039a58.png](https://i-blog.csdnimg.cn/blog_migrate/c46031e52159dd8b32023897b0162807.jpeg)
更本质上来说,就是把状态带着走,然后“沿路”更新的模式导致了很高的认知负担(cognitive load)。那么问题来了,为什么要把状态带在身上走呢?这就要回到文章的标题,vue.js 是不是那颗银弹。为什么 vue.js 没有完全解决这个问题。这个模型在推广到更广的场景里会遇到什么困难。
一切都是有原因的
如果这个问题简单,早就被解决了。一切都是有原因的。
![a2d80437b68ca32eeeac14489a59e225.png](https://i-blog.csdnimg.cn/blog_migrate/ee8259a2ba75b2feb30202260d7bb210.jpeg)
我们知道这个模式很有效果。那么把它推广到与后端交互过程中去呢?
![20736be369c69ecc75b50b6bf9d9108b.png](https://i-blog.csdnimg.cn/blog_migrate/56bf5fb6d997e6a5d4e4d25f384f7645.jpeg)
或者按照六边形架构的画图艺术
![1e87f4c6f6040de4e119097efdf67767.png](https://i-blog.csdnimg.cn/blog_migrate/32c297b6fbed6c88d17cb71ea1a6974c.jpeg)
为什么不能用双向绑定来屏蔽这些特定的输入输出技术?原因如下:
- 内存限制:我们不可能把整个数据库映射成内存中的对象。那么内存中的对象都是全局状态的一个选择性的投影。
- I/O速度限制:不可能想更新的时候就更新。必须在一次 round trip 里尽可能多地完成状态地交换。
- I/O不会永远成功:与 vue.js 更新显示器的像素不同,我们不能假设所有的更新都一定会成功。除了偶然的网路错误,还有两个人并发更新了同一条数据库记录这样的情况。那么这种需要在 I/O 操作后面根据不同的结果有不同的后续逻辑,怎么表达?
- 为了效率,需要点对点的直连:理论来讲可以把所有的两个I/O外设的交互都通过中间的Domain Model来进行,从而像外汇结算那样都先兑换成美元。但是我们不可能用这种方式来实现两个 mysql 表的拷贝,这样太慢了。为了效率,点对点的直连仍然是必须的。这个时候,model 与 model 的映射关系,需要借助编译技术来表达为特定 I/O 技术到特定 I/O 技术之间的直连逻辑表达。
这些实际的问题都是 vue.js 没有解决的。只是 vue.js 所处的浏览器的泡泡是一个很诗情画意很田园的泡泡。在那个环境里,我们可以简化很多事情。
所以实际中,这个 Domain Model 不会是六边形架构的图中画得这么美好。
![33d05a1f7201f5afa745d193754850c1.png](https://i-blog.csdnimg.cn/blog_migrate/6b5b425a4b54217a3c8c1a7f4daad48f.jpeg)
服务端的 Model 和客户端的 Model 要通过一个狭窄逼仄地管道挤过去。随着微服务地普遍流行,这样地管道会越来越普遍,把你的Model切得支离破碎。同时因为数据库无法完整做双向绑定到Domain Model上,那么必然需要通过用类似 SQL 的技术,去选择性地绑定双边的状态。但是这个“选择性”意味着不通用。一不小心,业务逻辑就会通过SQL的形式外泄到binding这一层去。
在这些 I/O 技术的约束下,我们会越来越放弃构建一个model的想法,而是以command control为中心。也就是放弃构建一个世界,而是把所有的状态放在小老鼠的背包里。让小老鼠扛着这个世界负重前行。这才是 transaction script 相对于 rich domain model 的本质区别。在于我们是有多个 causality line 来表达这个世界,还是把所有的业务逻辑,都合并到一个 transaction script 的单线条里描述,从而导致复杂度的组合爆炸,从而引起程序员的认知困难,从而引起沟通的灾难。
所以最本质的做法就是要对业务逻辑进行 divide and conqur。领域模型也好,六边形架构也好,vue.js双向绑定也好都是a means to the end,只是一个手段。最本质的是对于所有要描述的因果关系,我们是不是能够做到分而治之,用多个并行独立的causality line来描述它们。
比如说,当我们有一个新需求,需要在展示货物的时候,与结账的时候更添加一些交互逻辑,前后有“因果关系”。我们是在所谓“订单”上添加一个字段来表达这样的前后因果关系。还是添加一个新的流程单据,来承载这个因果关系。是在订单的处理代码里写所有的业务逻辑,还是在新的流程单据的流程代码里独立写这个因果关系。能不能做到分而治之,这才是决定了最后,我们这些小白鼠们,能不能从迷宫里走出来的关键之处。
结论
vue.js 不是银弹,没有提供 10 倍的开发效率提升。但是指明了一个很好的方向。
求前端人才
我们为顶尖工程师提供了与之相配的技术发挥空间、无后顾之忧的宽松工作环境以及有竞争力的薪酬福利。同时也为高潜质的行业新人提供充分的学习和成长机会。
这里,没有996,崇尚高效。
这里,话语权不靠职级和任命,靠的是代码的说服力。
这里,不打鸡血,我们用理性和内驱力去征服各种挑战。
这里,也会有项目排期,但不怕delay,我们有充足的时间,做到让自己更满意。
工作地点在北京西二旗,薪酬待遇见招聘链接:https://www.zhipin.com/job_detail/?query=%E4%B9%98%E6%B3%95%E4%BA%91&city=101010100&industry=&position=