MVC 和 MVVM 架构模式/设计思想

一、MVC(model View Controller)架构模式

MVC是比较直观的架构模式。

  1. 视图层(View)是提供给用户的操作界面,是程序的外壳。
  2. 数据层(Model)是程序需要操作的数据或信息。
  3. 控制层(Controller)负责根据用户从视图层(View)输入的指令,选取数据层中的数据进行对应的操作,简单来说就是处理业务逻辑的。

执行流程:

用户操作 ➡ View(负责接收用户的输入操作)➡ Controller(业务逻辑处理)➡ Model(数据持久化)➡ View(将结果反馈给View)。


mvc.png

业务逻辑全部分离到 Controller 中,View 只负责展示,Model 只是数据的存储,两者都是相互独立的,模块化程度高。

当业务逻辑变更的时候,不需要变更 View 和 Model,只需要 Controller 换成另一个 Controller,也就达到了解耦和重用的目的。

二、MVVM(Model View ViewModel)架构模式

MVVM 是相对于 MVC 改进的核心思想 。在开发过程中,由于需求的变更或添加,项目的复杂度越来越高,代码量越来越大,我们会发现 MVC 维护起来有点吃力。

由于 Controller 主要是用来处理各种逻辑和数据转化,业务逻辑复杂的 Controller 非常庞大,维护困难。所以有人想到把 Controller 的数据和逻辑处理部分从中抽离出来,用一个专门的对象去管理,这个对象也就是 ViewModel,是 Model 和 View 的一座桥梁。

M 表示模型 Model,V 表示视图 View ,VM 表示数据和模型。

VM 是 MVVM 模式的核心,它是连接 View、Model的桥梁。有两个方向:

  1. 将模型转换为视图,也就是将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。

  2. 将视图转换为模型,也就是将所看到的页面转化为后端的数据。实现方式是:DOM 事件监听

也就是视图数据的变化会同时修改数据源,而数据源数据的变化也会立即反应到视图上,我们称之为双向绑定。

在 MVVM 的框架下视图和模型是不能直接通信的。它们通过 ViewModel 来通信,ViewModel 通常要实现一个 observer 观察者,当数据发生改变,ViewModel 能够监听到数据的这种变化,然后通知到对应的视图做自动更新。而当用户操作视图,ViewModel 也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定。

三、观察者 - 订阅者(数据劫持)

Vue observer 数据监听器,把一个普通的 JavaScript 对象传给了 Vue 实例的 data 选项,Vue 将遍历此对象所有属性,并使用 Object.defineProperty() 方法把这些属性全部转成 setter、getter 方法。当 data 的某个属性被访问的时,则会调用 getter 方法,当 data 中的属性被改变时,则会调用 setter 方法。

observer.png

当执行 new Vue() 时,Vue 就进入初始化阶段。一方面 Vue 会遍历 data 选项中的属性,并用 Object.defineProperty() 将它们转为 getter/setter,实现数据变化监听功能。另一方面,Vue 的指令编译器 complie 对元素节点的指令进行解析,初始化视图,并订阅 Watcher 来更新视图,此时 watcher 会将自己添加到消息订阅器(Dep)初始化完毕。当数据发生变化时,observer 中的 setter 方法被触发,setter 会立即调用 Dep.notify(),Dep 开始遍历的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行响应的更新。

四、实现一个简单 Vue 双向绑定

新建一个文件,名为 myvue.html,大致如下所示:

<body>
    <div id="app">
        我是{{message}}内容
        <div>
            {{message}}
        </div>
        <span>
            <span>{{message}}</span>
        </span>
        {{bindData}}
    </div>
</body>
<script>
let vm = new Vue({
    el: '#app',
    data: {
        message: '测试数据',
        bindData: '绑定的数据'
    }
})
</script>

接下来我们实现一个 Vue 的双向绑定。

class Vue extends EventTarget {
    constructor(options) {
        super()
        this.$options = options;
        this.observer(this.$options.data);
        this.compile();
    }
    // 数据监听器
    observer(data) {
        let keys = Object.keys(data);
        keys.forEach(key => {
            let value = data[key];
            // 订阅器
            let dep = new Dep();
            Object.defineProperty(data, key, {
                configurable: true,
                enumerable: true,
                // 获取
                get() {
                    // 收集订阅者
                    if (Dep.target) {
                        dep.addSub(Dep.target);
                    }
                    return value;
                },
                // 设置更新
                set(newValue) {
                    // 更新
                    dep.notify(newValue);
                    if (newValue !== value) {
                        value = newValue;
                    }
                }
            })
        });
    }
    // 指令编译器
    compile() {
        let ele = document.querySelector(this.$options.el);
        this.compileNode(ele);
    }
    // 子节点
    compileNode(ele) {
        let childNodes = ele.childNodes;
        childNodes.forEach(node => {
            if (node.nodeType === 1) {
                // 如果是元素节点找到所有
                this.compileNode(node);
            } else if (node.nodeType === 3) {
                // 文本节点
                let reg = /\{\{\s*(\S+)\s*\}\}/g;
                if (reg.test(node.textContent)) {
                    let $1 = RegExp.$1; // 匹配
                    // 替换
                    node.textContent = node.textContent.replace(reg, this.$options.data[$1]);
                    // 订阅者
                    new Watcher(this.$options.data, $1, (newValue) => {
                        let oldValue = this.$options.data[$1];
                        reg = new RegExp(oldValue, 'g');
                        node.textContent = node.textContent.replace(reg, newValue);
                    });
                };
            };
        });
    }
};


// 订阅管理器
class Dep {
    constructor() {
        this.subs = [];
    }
    // 存储
    addSub(sub) {
        this.subs.push(sub);
    }
    // 更新
    notify(newValue) {
        this.subs.forEach(sub => {
            sub.update(newValue);
        })
    }
}

// 订阅者
class Watcher {
    constructor(data, key, cb) {
        Dep.target = this;
        this.cb = cb;
        data[key];
        Dep.target = null;
    }
    update(newValue) {
        this.cb(newValue)
    }
}

五、Vue2.0 使用 defineProperty 存在缺陷,Vue3.0 已改为使用 proxy

  • Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
  • Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

六、MVC 和 MVVM 的区别

MVC 和 MVVM 的区别并不是VM完全取代了C,ViewModel 存在目的在于抽离Controlle r中展示的业务逻辑,而不是替代 Controller,其它视图操作业务等还是应该放在 Controller 中实现。也就是说 MVVM 实现的是业务逻辑组件的重用。由于 MVC 出现的时间比较早,前端并不那么成熟,很多业务逻辑也是在后端实现,所以前端并没有真正意义上的 MVC 模式。而我们今天再次提起 MVC ,是因为大前端的来到,出现了 MVVM 模式的框架,我们需要了解一下 MVVM 这种设计模式是如何一步步演变过来的。

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