搞懂:MVVM模型以及VUE中的数据绑定数据劫持发布订阅模式
搞懂:MVVM模式和Vue中的MVVM模式
MVVM
- MVVM :
model - view - viewmodel
的缩写,说都能直接说出来model
:模型,view
:视图,view-Model
:视图模型- V:视图,即浏览器最前端渲染的页面
- M:模型,数据模型,就是后端页面渲染依赖的数据
- VM:稍后再说,因为暂时还不知道怎么工作,什么场景,直接解释有点没用
- 那就先说说前端场景:
- 如果数据改变,想要前端页面做出相应的改变,有几种方法:
- 1.使用原生js
var dom = document.getElementById('xxx') dom.value = xxx; // 直接修改值 dom.innerHtml = xxx; //改变开始 和 结束标签中的html
- 2.使用jquery
$('#name').text('Homer').css('color', 'red');
- 1.使用原生js
- 上面可以看出来,jquery确实在dom操作方面简化了很多,链式调用和更加人性化的api在没有mvvm模型出世之前,使用率极高
- 但是,也可以看出来,数据和页面视图之间存在断层,数据影响视图,甚至是视图中的节点改变数据,这都是极其频繁的页面操作,虽然一再简化这个面向过程的逻辑操作,但是还是避免不了手动修改的弊端。
- 有没有一种更好的方式,可以实现这种视图(
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吧~
- vm:viewModel视图模型,就是将数据model和视图view关联了起来,负责将
model
数据同步到view
显示,也同时把view
修改的数据同步到model
,我们无需关心中间的逻辑,开发者更多的是直接操作数据,至于更新视图或者会写model,都是我们写好的视图模型(viewModel
)帮我们处理
- vm:viewModel视图模型,就是将数据model和视图view关联了起来,负责将
-
概念:视图模型层,是一个抽象化的逻辑模型,连接视图(
view
)和模型(model
),负责:数据到视图的显示,视图到数据的回写
-
VUE中的MVVM(双向绑定)
vue框架中双向绑定是最常用的一个实用功能。实现的方式也网上很多文章,vue2.x是Object.DefineProperty,vue3.x是Es6语法的proxy
代理语法
-
具体是怎么做到的
ps:暂时先看vue2.x
- Object.setProperty(),设置和修改Javascript中对象属性值,定义对象属性的get和set方法,可以在对象获取值和修改值时触发回调函数,实现数据劫持,并且拿到新的改变后的值
- 需要根据初始化对象值和修改之后拿到改变后的值,对已绑定模板节点进行数据更新。
-
第一步:监听对象所有属性值变化(
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, key, val) { observe(val); // 监听子属性 Object.defineProperty(data, key, { 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, key, val) { observe(val); var dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: false, get: function() { return val; }, set: function(newVal) { //修改的在这里 if(newVal === val){ return } // 如果新值不等于旧值发生变化,触发所有订阅中间件的notice方法,所有订阅者发生变化 val = newVal console.log('changed ', val, ' --> ', 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, key, val) { observe(val); var dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, 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;
-
看似好像发布者订阅者模式实现了,数据劫持也实现了,在数据改变的时候,触发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, exp, 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);
- 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指令这种用法怎么联系起来,之后会慢慢学习
-
- 第二步:怎么做到对有绑定关系的节点进行更新和初始化值呢?如果一个数据对象绑定了多个dom节点,怎么统一通知所有dom节点呢,这就需要用到发布者-订阅者模式
总结:
- MVVM其实是现在很多前端框架的实现基础,除了vue 的数据劫持和观察订阅模式,其他框架的例如脏数据检测,或者直接使用观察者订阅者模式,都是一些很巧妙的实现方式,使程序员能够更多的关注数据层面或者逻辑层面的代码,而不需要手动去做更新两者之间关系的繁琐操作
- vue的数据劫持和发布者订阅者模式理解起来一开始看起来理解有点费劲,大概了解如何做的,学习其方法,当然手写完全流程的写出来,我也很难
- 学习的路上,大家一起加油,多问一个为什么
非常感谢:下面的文章给了我很多的帮助,感谢各位前行者的辛苦付出,可以点击查阅更多信息