MVVM模式到底是什么?实现原理剖析

Vue和React的兴起,MVVM模式已经成为主流开发思想,那这种模式的实现原理是什么?双向数据绑定是怎样工作的?发布订阅是什么?本文以Vue的设计思想带你解开这些迷团

Object.defineProperty()

Vue是不支持IE8以下的浏览器,因为它使用了IE8无法模拟的ECMAScript5特性:Object.defineProperty()

通常我们以字面量的方式定义一个对象,这种方式的属性是不存在getset方法的,并且对象的属性是可以随意更改或删除

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。Vue就是用了这个方法进行数据劫持,在把数据挂载到Vue实例上

var o = {}; // 创建一个新对象

// 在对象中添加一个属性与数据描述符的示例
Object.defineProperty(o, "a", {
  value : 37,
  writable : true,
  enumerable : true,
  configurable : true
});

// 对象o拥有了属性a,值为37
// 在对象中添加一个属性与存取描述符的示例
var bValue;
Object.defineProperty(o, "b", {
  get : function(){
    return bValue;
  },
  set : function(newValue){
    bValue = newValue;
  },
  enumerable : true,
  configurable : true
});

o.b = 38;
// 对象o拥有了属性b,值为38
// o.b的值现在总是与bValue相同,除非重新定义o.b
// 数据描述符和存取描述符不能混合使用
Object.defineProperty(o, "conflict", {
  value: 0x9f91102, 
  get: function() { 
    return 0xdeadbeef; 
  } 
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors
复制代码

更详细的Object.defineProperty() 解释,猛戳

发布订阅

先说一下发布订阅模式和观察者模式有什么区别

发布订阅模式是最常用的一种观察者模式的实现,并且从解耦和重用角度来看,更优于典型的观察者模式

发布订阅模式多了个事件通道在观察者模式中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出响应

通俗点说:

A告诉B去做三件事(买鞋、买裤子、买领带),每件事做完都要告诉A,

B买到鞋告诉A“鞋买到了”,A收到消息后发了个朋友圈;

B买到裤子后告诉A“裤子买到了”,A收到消息后发了个微博;

B买到领带后告诉A“领带买到了”,A收到消息后来了个自拍;

在发布订阅模式中,发布者和订阅者之间多了一个发布通道;一方面从发布者接收事件,另一方面向订阅者发布事件;订阅者需要从事件通道订阅事件以此避免发布者和订阅者之间产生依赖关系

通俗点说:

N个人关注了A的公众号 (订阅)

A写好了文章提交到微信公众号平台 (发布)

微信公众号平台推送到了这N个人的微信客户端 (广播)

所有的订阅者接到消息后可以选择自己的动作阅读/忽略/分享

我们接下来说的MVVM就是通过 Object.defineProperty()发布订阅实现的双向数据绑定

MVVM

先看一下MVVM的原理,接下来我们根据这个图一步一步深入

我们以 Vue的规则,自己写的 MyMVVM

<div id="app">
    <p>a的值{{a.a}}</p>
    <div>b的值{{b}}</div>
    <input type="text" v-model="b">
    {{hello}}
</div>
复制代码
  let myMVVM = new MyMVVM({
        el:"#app",
        data:{
            a:{a:"a"},
            b:"是b"
        }
    })
复制代码

new MyMVVM()

实例化的时候MyMVVM实例挂载了传入的data

function MyMVVM(options = {}) {
    this.$options = options; // 把所有属性挂载在$options
    let data = this._data = this.$options.data;
    // this 代理了this._data
    for(let key in data){
        Object.defineProperty(this,key,{
            enumerable:true,
            get(){
                return this._data[key]
            },
            set(newVal){
                this._data[key] = newVal
            }
        })
    }
}
复制代码

Compile编译模板

function Compile(el,vm) {
    // el表示替换的范围
    // DOM中的节点塞入fragment时,原节点会被删除
    vm.$el = document.querySelector(el);
    let fragment = document.createDocumentFragment();
    while(child = vm.$el.firstChild){ // 获取到的元素节点 塞入fragment 在内存中操作
        fragment.appendChild(child)
    }

    replace(fragment);
    function replace(fragment){
        Array.from(fragment.childNodes).forEach(function (node) { // 类数组转换为数组,循环
            let text = node.textContent;
            let reg = /\{\{(.*)\}\}/;
            // 数据渲染视图
            if(node.nodeType === 3 && reg.test(text)){ // 文本节点
                let arr = RegExp.$1.split('.');
                let val = vm;
                arr.forEach(function (k) { // 取this.a.a / this.b
                    val = val[k]
                });
                // 替换
                node.textContent = text.replace(reg,val) // 替换模板
            }
            // 视图更新数据,数据再渲染视图
            if(node.nodeType === 1){ // DOM节点 输入内容时数据一起更新
                let nodeAttrs = node.attributes;
                Array.from(nodeAttrs).forEach(function (attr) {
                    let name = attr.name; 
                    let exp = attr.value;
                    if(name.indexOf("v-") === 0){ // 带有v-指令的DOM节点
                        node.value = vm[exp];
                    }
                    node.addEventListener("input",function (e) {
                        let newVal = e.target.value;
                        vm[exp] = newVal  // 调用VM上data的set方法跟新视图数据
                    })
                })
            }
            if(node.childNodes){ // 节点深度递归
                replace(node)
            }
        });
    }
    vm.$el.appendChild(fragment)
}
复制代码

Observer 数据劫持

数据劫持的过程就是把传递给实例的对象通过Object.defineProperty()重新定义属性,这样就拥有了getset方法,便于我们后续观察数据的变化

vue特点是不能新增不存在的属性,因为不存在的属性在数据劫持的时候没法重新定义,也就不能增加getset

// 观察对象给对象增加ObjectDefineProperty
function Observe(data) {
    for(let key in data){  // 把data属性通过Object.defineProperty()的方式 定义属性
        let val = data[key];
        Object.defineProperty(data,key,{
            enumerable:true,
            get(){
                return val
            },
            set(newVal){
                if(val === newVal) {
                    return;
                }
                val = newVal;
            }
        })
    }
}
复制代码

Watcher

Watcher是一个类,通过这个类创建的实例都有update方法,用来执行数据发生变化后的更新动作

function Watcher(vm,exp,fn) {
    this.fn = fn;
    this.vm = vm;
    this.exp = exp;
    Dep.target = this;
    let val = vm;
    let arr = exp.split('.');
    arr.forEach(function (k) { // 目的是为了触发取值时的get方法,get方法中把watcher添加到队列里
        val = val[k]
    });
    Dep.target = null; // 添加成功后target置为null

}
Watcher.prototype.update = function () {
    let val =this.vm;
    let arr = this.exp.split('.');
    arr.forEach(function (k) {
        val = val[k]
    });
    this.fn(val);
};
复制代码

发布订阅

先有订阅再有发布

function Dep() {
    this.subs = []; // 存放订阅队列
}
Dep.prototype.addSub = function (sub) { // 往容器中存储订阅信息
    this.subs.push(sub)
};

Dep.prototype.notify = function () {  // 发布
    this.subs.forEach(sub => sub.update())
};
复制代码

总结

我们完成了MVVM框架,正确的调用组合这些方法就能实现数据的双向绑定了,源码请参考这里

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值