搞懂:MVVM模式和Vue中的MVVM模式
MVVM
- MVVM :
model - view - viewmodel
的缩写,说都能直接说出来model
:模型,view
:视图,view-Model
:视图模型 - 那就先说说前端场景:
- VM:
-
再看看现在VUE框架中怎么做到这种视图和模型的联动
//html <input v-model = 'val' placeholder = 'edit here'> //script export defaults{ data:function(){ return { val:'' } } }
很简单,很常用的v-model指令,那么在input值修改的时候,data中的val变量值也会改变,直接在js中改变val的值的时候,input中的value也会改变??我们做了什么,我们怎么将数据和视图联系起来的?自动会关联这两个东西
-
可能,这就是VM吧~
-
概念:视图模型层,是一个抽象化的逻辑模型,连接视图(
view
)和模型(model
),负责:数据到视图的显示,视图到数据的回写
@H_301_21@
@H_301_21@
-
-
具体是怎么做到的
ps:暂时先看vue2.x
-
第一步:监听对象所有属性值变化(
Observer
)var data = {test: '1'}; observe(data); data.test = '2'; // changed 1 --> 2 function observe(data) { if (!data || typeof data !== 'object') { return; } // 取出所有属性遍历 Object.keys(data).forEach(function(key) { defineReactive(data,key,data[key]); }); }; function defineReactive(data,val) { observe(val); // 监听子属性 Object.defineProperty(data,{ enumerable: true,// 可枚举 configurable: false,// 防止重复定义或者冲突 get: function() { return val; },set: function(newVal) { console.log('changed ',val,' --> ',newVal); val = newVal; } }); }
- 第二步:怎么做到对有绑定关系的节点进行更新和初始化值呢?如果一个数据对象绑定了多个dom节点,怎么统一通知所有dom节点呢,这就需要用到发布者-订阅者模式
-
这里是Observer作为一个察觉数据变化的发布者,发现数据变化时,触发所有订阅者(
Watcher
)的更新update
事件,首先要拥有一个能存储所有订阅者队列,并且能通知所有订阅者的中间件(消息订阅器Dep
)function Dep () { // 订阅者数组 this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); },notify: function() { //通知所有订阅者 this.subs.forEach(function(sub) { sub.update(); }); } };
-
并且在观察者
Observer
中修改当Object对象属性发生变化时,触发Dep
中的notify事件,所有订阅者可以接收到这个改变function defineReactive(data,val) { observe(val); var dep = new Dep(); Object.defineProperty(data,{ enumerable: true,configurable: false,get: function() { return val; },set: function(newVal) { //修改的在这里 if(newVal === val){ return } // 如果新值不等于旧值发生变化,触发所有订阅中间件的notice方法,所有订阅者发生变化 val = newVal console.log('changed ',newVal); dep.notify(); } }); }
-
但是有没有发现还有一个问题,Dep订阅中间件中的订阅者数组一直是空的,什么时候把订阅者添加进来我们的订阅中间件中间,哪些订阅者需要添加到我们的中间件数组中
- 1.我们希望的是订阅者Watcher在实例化的时候自动添加到Dep中
- 2.有且仅有在第一次实例化的时候添加进去,不允许重复添加
- 3.由于Dep在发布者数据变化时会触发所有订阅则的update事件,所以Watcher实例(订阅者)能够触发update事件,并进行相关操作
- 怎么能让Watcher在实例化的时候自动添加到Dep订阅者数组中
function Watcher(vm,exp,cb) { this.cb = cb; // 构造函数中执行,只有可能在实例化的时候执行一遍 this.vm = vm; this.exp = exp; this.value = this.get(); // 将自己添加到订阅器的操作---HACK开始 // 在构造函数中调用了一个get方法 } Watcher.prototype = { update: function() { this.run(); },run: function() { var value = this.vm.data[this.exp]; var oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm,value,oldVal); } },get: function() { //get方法中首先缓存了自己本身到target属性 Dep.target = this; // 获取了一下Observer中的值,相当于调用了一下get方法 var value = this.vm.data[this.exp] // get 完成之后清除了自己的target属性??? Dep.target = null; return value; } //很明显,get方法只在实例化的时候调用了,满足了只有在Watcher实例化第一次的时候调用 //update方法接收了发布者的notice 发布消息,并且执行回调函数,这里的回调函数还是通过外部定义(简化版) //但是,好像在get方法中有一个很神奇的操作,缓存自己,然后调用Observer的getter,然后清除自己 //这里其实是一步巧妙地操作把自己添加到Dep订阅者数组中,当然Observer 的getter方法也要变化如下 }; //Observer.js function defineReactive(data,configurable: true,get: function() { if (Dep.target) {. dep.addSub(Dep.target); // 关键的在这里,当第一次实例化时,调用Watcher的get方法,get方法内部会获取Object的属性,会触发这个get方法,在这里将Watcher 添加到Dep的订阅者数组中 } return val; },set: function(newVal) { if (val === newVal) { return; } val = newVal; dep.notify(); } }); } Dep.target = null;
@H_301_21@
-
看似好像发布者订阅者模式实现了,数据劫持也实现了,在数据改变的时候,触发Object.setProperty中定义的set函数,set函数触发Dep订阅者中间件的notice方法,触发所有订阅者的update方法,并且订阅者在实例化的时候就加入到了Dep订阅者的数组内部,让我们来看看怎么用
- html部分,
<body> <!-- 这里其实还是会直接显示{{name}} --> <h1 id="name">{{name}}</h1> </body>
- 封装一个方法(类)将Observer,Watcher,关联起来
function SelfVue (data,el,exp) { //初始化data属性 this.data = data; //将其设置为观察者 observe(data); //手动设置初始值 el.innerHTML = this.data[exp]; //初始化watcher,添加到订阅者数组中,并且回调函数是重新渲染页面,触发update方法时通过回调函数重写html节点 new Watcher(this,function (value) { el.innerHTML = value; }); return this; }
- 使用:
var ele = document.querySelector('#name'); var selfVue = new SelfVue({ name: 'hello world' },ele,'name'); //设定延时函数,直接修改数据值,看能否绑定到页面视图节点 window.setTimeout(function () { console.log('name值改变了'); selfVue.data.name = 'canfoo'; },2000);
@H_301_21@
- html部分,
-
到上面为止:基本实现了数据(
model
)到视图(view
)层的单向数据绑定,只有v-model是使用到了双向绑定,很多vue的数据绑定的理解,和难点也就在上面的单向绑定 -
那么:model->view单向绑定似乎已经成功了,那么view -> model呢?
- 这个在于如果视图层的value改变了,如何修改已经绑定的model层的对象属性呢?
- 这个指令在vue中是:v-model,指令部分会在之后的学习中继续讲解
- 但是,视图view节点在value属性改变时,一般会触发change或者input事件,而且也一般是一些可输入视图节点,直接将事件写在change事件或者input事件里面,并且修改Object里面的值
var dom = document.getElementById('xx') dom.addEventListener('input',function(e){ selfVue.data.xxx = e.target.value })
- 具体input事件和v-model指令这种用法怎么联系起来,之后会慢慢学习 @H_301_21@
@H_301_21@
@H_301_21@
-
@H_301_21@
- 第二步:怎么做到对有绑定关系的节点进行更新和初始化值呢?如果一个数据对象绑定了多个dom节点,怎么统一通知所有dom节点呢,这就需要用到发布者-订阅者模式
VUE中的MVVM(双向绑定)
vue框架中双向绑定是最常用的一个实用功能。实现的方式也网上很多文章,vue2.x是Object.DefineProperty,vue3.x是Es6语法的proxy
代理语法