MVVM模式是什么?你是怎么理解MVVM原理的?理解它不只是应付面试,对VUE、Backbone.js、angular、Ember、avalon框架的设计模式也会有更进步一步的理解,有可能下一个流行框架就是你的杰作~~本篇文章最后也会实现了一个属于自己的简易MVVM库,里面实现了一个mvvm库应有基本功能~
一、MVVM的概念
Mvvm定义MVVM是Model-View-ViewModel的简写。是一个软件架构设计模式,由微软 WPF 和 Silverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。由 John Gossman(同样也是 WPF 和 Silverlight 的架构师)于2005年在他的博客上发表。即模型-视图-视图模型。
二、MVVM的发展史
var dom = document.getElementById('name');
dom.innerHTML = 'Homer';
dom.style.color = 'red';
复制代码
$('#name').text('Homer').css('color', 'red');
MVVM最早由微软提出来,它借鉴了桌面应用程序的MVC思想,在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。
ViewModel如何编写?需要用JavaScript编写一个通用的ViewModel,这样,就可以复用整个MVVM模型了。
三、正式的MVVM理解
- MVVM模式
MVVM 的出现促进了 GUI 前端开发与后端业务逻辑的分离,极大地提高了前端开发效率。MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。如下图所示:
- MVVM组成部分
# View 层
View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建,为了更方便地展现 ViewModel 或者 Model 层的数据,已经产生了各种各样的前后端模板语言,比如FreeMarker、Marko、Pug、Jinja2等等,各大 MVVM 框架如 avalon,Vue,Angular 等也都有自己用来构建用户界面的内置模板语言。
# Model 层
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,主要围绕数据库系统展开。
# ViewModel 层
ViewModel 是由前端开发人员组织生成和维护的视图数据层。mvvm模式的核心,它是连接view和model的桥梁。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,那一块展示什么这些都属于视图状态(展示),而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。由于实现了双向绑定,ViewModel 的内容会实时展现在 View 层,这是激动人心的,因为前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新,真正实现数据驱动开发。看到了吧,View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。
- MVVM设计模式的优缺点:
1、当然是最主要的双向绑定技术,单向绑定与双向绑定。
Hello, LEE! You are 18.
用jQuery修改name和age节点的内容:
var name = '修改';
var age =100;
$('#name').text(name);
$('#age').text(age);
复制代码
var person = {
name: 'LEEt',
age: 18
};
复制代码
要把显示的name从LEE改为修改,把显示的age从18改为100,我们并不操作DOM,而是直接修改JavaScript对象:
person.name = '修改';
person.age = 100;
复制代码
MVVM的设计思想:关注Model的变化,让MVVM框架去自动更新DOM的状态,从而把发者从操作DOM的繁琐步骤中解脱出来!
2、由于控制器的功能大都移动到View上处理,大大的对控制器进行了瘦身。
3、可以对View或ViewController的数据处理部分抽象出来一个函数处理model。这样它们专职页面布局和页面跳转,它们必然更一步的简化。
4、提高可维护性
5、可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。
6、低耦合可重用:视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定不同的"View"上,当View变化的时候Model不可以不变,当Model变化的时候View也可以不变。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
- Bug很难被调试。因为使用双向绑定的模式,当你看到界面异常了,有可能是你View的代码有Bug,也可能是Model的代码有问题。数据绑定使得一个位置的Bug被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。另外,数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的。
- 一个大的模块中model也会很大,虽然使用方便了也很容易保证了数据的一致性,当时长期持有,不释放内存就造成了花费更多的内存。
- 对于大型的图形应用程序,视图状态较多,ViewModel的构建和维护的成本都会比较高。
- MVVM的适用范围
Angular:Google出品,名气大,但是学习难度有些大;适合PC,代码结构会比较清晰;
Backbone.js:入门非常困难,因为自身API太多;
Ember:一个大而全的框架,想写个Hello world都很困难。
Avalon:属于轻量级的,并且对老的浏览器支持程度较高,最低支持到IE6,所以适合兼容老刘浏览器的项目;
Vue:主打轻量级,仅作为MV*中的视图部分使用,优点轻量级,易学易用,缺点是大项目的时候还要配合其他框架或者库来使用,比较麻烦
四、实现MVVM的js库
- 脏值检测(angular):
- l脏检测机制并不是使用定时检测。
- l脏检测的时机是在数据发生变化时进行。
- l angular对常用的dom事件,xhr事件等做了封装, 在里面触发进入angular的digest流程。
- l在digest流程里面, 会从rootscope开始遍历, 检查所有的watcher。 (关于angular的具体设计可以看其他文档,这里只讨论数据绑定),那我们看下脏检测该如何去做:主要是通过设置的数据来需找与该数据相关的所有元素,然后再比较数据变化,如果变化则进行指令操作。
3.发布-订阅模式(backbone):通过发布消息,订阅消息进行数据和视图的绑定监听。
1、实现一个Observer,对数据进行劫持,通知数据的变化(将使用的要点为:Object.defineProperty()方法)
2、实现一个Compile,对指令进行解析,初始化视图,并且订阅数据的变更,绑定好更新函数
3、实现一个Watcher,将其作为以上两者的一个中介点,在接收数据变更的同时,让Dep添加当前Watcher,并及时通知视图进行update
4、实现一些VUE的其他功能(Computed、menthods)
5、实现MVVM,整合以上几点,作为一个入口函数
以下为代码部分:
Html:
"en">
"UTF-8">
"viewport" content="width=device-width, initial-scale=1.0">
"X-UA-Compatible" content="ie=edge">
实现MVVM的js库(模拟vue实现功能)
"app">
type="text" v-model="person.name">
hello,{{person.name}}
You are:{{person.age}}
{{getNewName}}
复制代码
js:
// 2019-4-4
// lee
// 草履虫的思考
// 简单模拟vue实现MVVM
/**
* 实现一个Vue的类
* 1、实现一个Observer,对数据进行劫持,通知数据的变化(将使用的要点为:Object.defineProperty()方法)
2、实现一个Compile,对指令进行解析,初始化视图,并且订阅数据的变更,绑定好更新函数ComplieUtil解析指令的公共方法
3、实现一个Watcher,将其作为以上两者的一个中介点,在接收数据变更的同时,让Dep添加当前Watcher,并及时通知视图进行update
4、实现一些VUE的其他功能(Computed、menthods)
*/
// 观察者模式(发布订阅)
class Dep {
constructor() {
this.subs = []; //存放所有watcher
}
// 订阅 添加watcher
addSub(watcher) {
this.subs.push(watcher);
}
// 发布
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
// 观察者 vm.$watch(vm,'person.name',(newVal)=>{ })
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 默认存储一个老值
this.oldValue = this.get();
}
get() {
Dep.target = this;
// 取值 把这个观察者和数据关联起来
let val = ComplieUtil.getVal(this.vm,this.expr);
Dep.target = null;
return val;
}
// 更新操作 数据变化后 会调用观察者中的update方法
update() {
let newVal = ComplieUtil.getVal(this.vm,this.expr);
if (newVal !== this.oldValue) {
this.cb(newVal);
}
}
}
// 实现数据劫持作用
class Observer {
constructor(data) {
this.observer(data);
}
observer(data) {
// 如果是对象才观察
if (data && typeof data === 'object') {
for (let key in data) {
this.defineReactive(data, key, data[key])
}
}
}
defineReactive(obj, key, value) {
this.observer(value);
// 给每个属性 都加上具有发布订阅的功能
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可重新定义
get() {
// 创建watcher时 会取到对应的内容,并且把watcher放到全局上
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newVal) => { // {person:{name:'lee'}
// 数据没有变不需要更新
if (newVal != value) {
// 需要递归
this.observer(newVal);
value = newVal;
dep.notify();
}
}
})
}
}
// 编译器
class Complier {
constructor(el, vm) {
// 判断el属性是不是一个元素 如果不是元素 那就获取他 (因为在vue的el中可能是el:'#app'
// 或者document.getElementById('app')
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 把当前节点中的元素 获取到 放到内存中
let fragment = this.nodeFragMent(this.el);
// 把节点中的内容进行替换
// 编译模板 用数据编译
this.complie(fragment);
// 把内容在塞到页面中
this.el.appendChild(fragment);
}
isElementNode(node) { //是不是元素节点
return node.nodeType === 1;
}
// 把节点移动到内存中
nodeFragMent(node) {
let frag = document.createDocumentFragment();
let firstChild;
while (firstChild = node.firstChild) {
// appendChild 具有移动性
frag.appendChild(firstChild);
}
return frag;
}
// 是不是指令
isDirective(attrName) {
return attrName.startsWith('v-');
}
// 编译元素
complieElement(node) {
let attr = node.attributes;
[...attr].forEach(item => {
// item 有key = value ,type="text" v-model="person.name"
let {
name,
value: expr
} = item;
if (this.isDirective(name)) {
// v-mode v-html v-bind...
let [, directive] = name.split('-');
let [directiveName,eventName] = directive.split(':');
console.log(node, expr, this.vm, eventName);
// ComplieUtil[directive](node, expr, this.vm);
ComplieUtil[directiveName](node, expr, this.vm, eventName);
}
})
}
// 编译文本
// 判断当前文本节点中内容是否包括{{}}
complieText(node) {
let content = node.textContent;
var reg = /\{\{(.+?)\}\}/;
if (reg.test(content)) {
ComplieUtil['text'](node, content,this.vm); //{{}}
}
}
// 用来编译内存中的dom节点
complie(node) {
let childNode = node.childNodes;
// childNode 是类数组 转换为数组
[...childNode].forEach(item => {
// 元素 查找v-开头
if (this.isElementNode(item)) {
this.complieElement(item);
//如果是元素的话 需要把自己传进去
// 在去遍历子节点
this.complie(item);
// 文本 查找{{}}内容
} else {
this.complieText(item);
}
})
}
}
// 编译工具
ComplieUtil = {
// 解析v-model指令
// node是节点 expr是表达式 vm是实例 person.name vm.$data 解析v-model
model(node, expr, vm) {
// 给输入框赋予value属性 node.value = xxx
let fn = this.updater['modelUpdater'];
let val = this.getVal(vm, expr);
// 给输入框加一个观察者 如果稍后数据更i性能了会触发此方法,数据会更新
new Watcher(vm, expr, (newVal) => {
fn(node, newVal);
});
// 输入事件
node.addEventListener('input',(e)=>{
let val = e.target.value; //获取用户输入的内容
this.setVal(vm, expr, val);
});
fn(node, val);
},
html() {
},
// 返回了一个全的字符串
getContentVal(vm, expr) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1]);
});
},
text(node, expr, vm) { //expr {{a}} {{b}} {{person.name}}
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
//给表达式{{}}都加上观察者
new Watcher(vm, args[1], () => {
fn(node, this.getContentVal(vm, expr));
});
return this.getVal(vm, args[1]);
});
let fn = this.updater['textUpdater'];
fn(node, content);
},
on(node, expr, vm,eventName){ //v-on:click
console.log(node, expr, vm, eventName);
node.addEventListener(eventName,(e)=>{
vm[expr].call(vm,e );
});
},
updater: {
modelUpdater(node, value) {
node.value = value;
},
htmlUpdater() {},
// 处理文本节点
textUpdater(node, value) {
node.textContent = value;
}
},
//根据表达式取到的对应的数据 vm.$data expr是如 'person.name'
getVal(vm, expr) {
return expr.split('.').reduce((data, cur) => {
return data[cur];
}, vm.$data);
},
setVal(vm, expr,value){
expr.split('.').reduce((data, cur,index,arr) => {
if(index == arr.length-1){ //索引是最后一项
return data[cur] = value;
}
return data[cur];
}, vm.$data);
}
}
class Vue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
let computed = options.computed;
let methods = options.methods;
// 根元素存在在编译模板
if (this.$el) {
// 把数据 全部转化成用Object.defineProperty来定义
new Observer(this.$data);
// 实现methods中的方法
for (let key in methods) {
Object.defineProperty(this, key, {
get() {
return methods[key]; //进行了转化操作
}
});
}
// 实现computed中的方法
for (let key in computed) { //有依赖关系
Object.defineProperty(this.$data, key, {
get() {
return computed[key].call(this); //进行了转化操作
}
});
}
// 把数据获取操作 都代理到vm.$data
this.proxy(this.$data);
new Complier(this.$el, this);
}
}
// 代理 去掉$data
proxy(data){
for(let key in data){
Object.defineProperty(this,key,{
get(){
return data[key]; //进行了转化操作
}
});
}
}
}
复制代码