适读人群

本文适合对MVVM有一定了解(如有主流框架ng,vue等使用经验配合本文服用则效果更佳),虽然会用这类框架,但是对框架底层核心实现又不太清楚,或者能说出个所以然,但是让他自己动手写又没有头绪的码友。如果还没听说过MVVM,不妨先收藏着。。。

名词定义

  • 先给低配版的库起一个响亮的名字,以便于开展教学,入乡随俗我们就叫ta -- SegmentFault.js 吧 (以下简称sf.js

  • 设置在DOM Element上的自定义属性前缀统一以 sf- 开头 (如 <input type="text" sf-value="xxx">)

为什么是低配版?

1. 没有sf-repeat
2. 不支持select,checkbox,radio等控件的双向绑定
3. 没有sf-if
4. 很多都没有

由于是教学向,力图用最简短易读的代码来实现MVVM最主要最基本的功能,故砍掉了部分实现。

先看演示图,图中就是使用sf.js写得DEMO

100多行的低配版不能要求太多,如果看不上低配版的库,请关闭本教程。

老生常谈

什么是双向绑定

首先明白一个概念,什么是双向绑定?在说双向绑定之前,我们先说说单向显示
单向显示 说白了就是view动态地显示变量。比如在ng或其它一些主流框架里类似这种写法

    //scope.message= "segmentfault";
    <h3 ng-bind="message"></h3>
    <!-- 运行时生成 -->
    <h3>segmentfault</h3>

为什么说是单向呢,因为都是 viewModel上某个变量(message) -> view (h3)的一个过程,viewModel上的变量被view所呈现。

再来看看 逆向修改
前面说了单向是viewMode->view的过程,那逆向就是 viewModel <- view的过程,换句话说就是viewModel被view修改的过程。例如angular中

<input type="text" ng-model="message">

一旦用户在input控件中输入值,便会实时地改变viewModel中message这个变量的值。这是一个view -> viewModel 的过程。

所谓的双向绑定就是一个 viewMode ->(显示) view ->(修改)viewModel 的过程。

如果整明白什么是双向绑定了,我们就来谈谈设计思路,没有整明白的同学请再阅读一遍.

单向显示的设计思路(viewModel -> view)

先看看API的设计

<!-- view -->
<div>
    <h3 sf-text="vm.message"></h3>
</div>

<script>
    // --- viewModel ---
    function ViewModel(){
        this.message = "segmentfault";
    }
    var vm = new ViewModel();
</script>

要实现这个功能,我们的sf库应该需要哪几步操作呢?(先自己想想,独立思考下)

1. 注册ViewModel,我们的库需要知道哪些object是viewModel
2. 扫描整个DOM Tree找到有哪些DOM节点上被配置了sf-xxxx这个attribute
3. 纪录这些被单向绑定的DOM节点和viewModel之间的映射关系
4. 使用DOM API, element.innerText = vm.prop, element.value = vm.prop, element.xxxx = vm.prop 来显示数据

思考题1

Q:如果我们要单向绑定不是innerText,value 而是作为样式的class,style呢?
A:没错,使用sf-class="vm.myClass" sf-style="vm.myStyle"就好了,其它原生属性也以此类推
"sf-" + native attribute is good!

逆向修改的设计思路(viewModel <- view)

主流的一些mvvm框架上一般这么设计API,还是拿angular举例子

<input type="text" ng-model="message">

所以,我们就设计一个叫做sf-value的attribute来做API

<input type="text" sf-value="vm.message">

拍脑袋想想,view要改变数据只可能发生在可以和用户交互的一些html控件上,比如input家族(text, radio, checkbox), select, textarea上。 像h1~hn家族,这辈子是没有机会的。

要实现view改写viewModel,我们的库应该需要哪几步操作呢?(也先自己想想,千万不要丢掉独立思考能力)

1.扫描整个DOM Tree,找到哪些INPUT,SELECT,TEXTAREA节点上被配置了sf-value这个attribute
2.纪录这些被双向绑定的DOM节点和viewModel之间的映射关系
3.sf.js库自动给这个写DOM加上onchange或者oninput的事件监听
4.一旦监听到change/input事件,立即获取这个DOM的value值,把这个element.value赋给与之绑定的viewModel的变量上。

思考题2

Q:那么问题来了,vm.message被input修改了,谁去通知其它同样绑定了vm.message的view呢?
A:请看下一段

同步机制

脏检查大法 这三个字想必大家已经如雷贯耳,我2年多前出去面试的时候被问及最多的就是angular的脏检查,什么是脏检查?angular脏检查的时机是什么?

这也是为什么,一旦你没有使用ng自带的$http,$interval,$timeout,ng-click这些angular自己封装的API去操作viewModel,angular都不会自动去同步view,因为已经超出他的管辖范围了,你必须手动调用apply函数去强制执行一次脏检查,以同步view。

setter大法
听说VUE是使用的这种同步机制,其核心原理就是使用Object.defineProperty(obj, prop, descriptor)(不了解defineProperty的请戳)这个API,在setter中加点料,一旦有任何地方执行 vm.message = "new value"语句,则setter都会被调用,由setter去触发重新渲染view的逻辑。

相较这两种同步机制,似乎setter更加轻便,性能更好。所以本文使用了setter的方式来实现同步机制(关键是实现setter机制使用的代码较少)。

设计思路

给setter加点料

http://jsbin.com/gosigoh/edit...

总体设计图

所以归纳来说一个MVVM库主要由3块组成
MVVM库 = 单向显示 + 逆向修改 + 同步机制
下图为SegmentFault.js的实现机制
其中Renderer负责单向显示和逆向修改,Watcher负责监视viewModel为同步机制的核心模块,
Scanner负责sf.js初始化时扫描DOM Tree生成view和viewModel的映射关系。
SegmentFault模块则负责维护view-viewModel Map,以及各个模块间的调度

思考题3

Q:了解了MVVM的实现机制,你能否自己动手也试着用百来行代码实现一个MVVM库呢?

好了!本教程第一部分设计篇就写到这里,具体coding请移步(下一篇 【教学向】150行代码教你实现一个低配版的MVVM库(2)- 代码篇
我会用Typescript给出一版实现。

写在最后

这篇文章的目的

2年前写了我受够了angular的笨重,学习曲线陡峭等缺点,自己一怒之下写下一个轻量的MVVM库,给她起名叫【Ukulele.js】(跟我一起念『尤克里里.杰爱死』,当然本文不是这个库的安利文,请安心服用),一开始写这个库出于好玩,后来也加入了越来越多的功能,诸如web component的支持,我渐渐发现,其实要写一个MVVM库也并不是很难,难的是你有没有决心敲下第一行代码。后来我把她和【精通angularjs】一起写在里简历里,然后就去找工作了。面试的时候被问及最多的问题就是:"说说MVVM的实现机制"。

我今天写下此文,1是希望有机会看到这篇文章的码友能真正掌握MVVM的核心机制,2是鼓励下大家能静下心来,自己动手写写库,写写框架,有些你现在觉得蛮高大上的东西,你仔细一分析,动动脑,真的没有那么高大上,普通的码农也能自己实现

相关阅读

【教学向】150行代码教你实现一个低配版的MVVM库(1)- 原理篇
【教学向】150行代码教你实现一个低配版的MVVM库(2)- 代码篇
【教学向】再加150行代码教你实现一个低配版的web component库(1) —设计篇
【教学向】再加150行代码教你实现一个低配版的web component库(2) —原理篇

03-05 18:22