MVVM定义:
MVVM是Model-View-ViewModel的简写。
MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。
当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑
实现:
1、数据劫持:获取和修改数据的时候,对其劫持,从而做到对数据变动的监听
所谓数据劫持就是给对象增加get,set
使用Object.defineProperty()实现
2、数据代理:实现this.***直接取值
我们每次拿data里的数据时,不用每次都写一长串,如mvvm._data.a.b这种,我们其实可以直接写成mvvm.a.b这种显而易见的方式
3、数据编译:把dom中{{}}里面的内容解析为对应的data属性值
需要编译一下了,把{{}}里面的内容解析出来
4、发布订阅:数据变动,触发dom中对应的值也跟着变动
发布订阅主要靠的就是数组关系,订阅就是放入函数,发布就是让数组里的函数执行
5、数据双向绑定:input等输入框的值与data对应的值绑定,双向触发修改
//创建一个Mvvm函数
function Mvvm(options) {
// vm.$options Vue上是将所有属性挂载到上面
// 所以我们也同样实现,将所有属性挂载到了$option
this.$options = options || {};
let data = this._data = this.$options.data;
//数据劫持
observe(data);
//把this代理options.data,方便直接使用this取值
for(let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get: function() {
return this._data[key]
},
set: function(newVal) {
this._data[key] = newVal;
}
});
}
initComputed.call(this); //作用域指向mvvm对象
new Compile(options.el, this);
this.$options.mounted.call(this);
}
function initComputed() {
let vm = this;
let computed = this.$options.computed;
for(let key in computed) {
//将计算属性添加到mvvm的代理上
Object.defineProperty(vm, key, {
// 这里判断是computed里的key是对象还是函数
// 如果是函数直接就会调get方法
// 如果是对象的话,手动调一下get方法即可
// 如: sum() {return this.a + this.b;},他们获取a和b的值就会调用get方法
// 所以不需要new Watcher去监听变化了
get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
set: function() {
}
});
}
}
function observe(data) {
// 如果不是对象的话就直接return掉
// 防止递归溢出
if(!data || typeof data !== 'object') {
return
} else {
return new Observe(data);
}
}
// 创建一个Observe构造函数
// 写数据劫持的主要逻辑
function Observe(data) {
// 所谓数据劫持就是给对象增加get,set
// 先遍历一遍对象再说
for(let key in data) {
let val = data[key];
let dep = new Dep();
//给data的属性添加get/set方法
Object.defineProperty(data, key, {
configurable: true,
get: function() {
if(Dep.target) {
dep.addSub(Dep.target); //添加订阅,将watcher添加到订阅事件中 [watcher]
}
return val;
},
set: function(newVal) { //设置值的时候
if(val === newVal) { // 设置的值和以前值一样就不理它
return
} else {
val = newVal; // 如果以后再获取值(get)的时候,将刚才设置的值再返回去
observe(newVal); // 当设置为新值后,如果新值是对象,也需要把新值再去定义成属性
dep.notify(); //触发发布, 让所有watcher的update方法执行即可
}
}
});
// 如果val是个对象,递归继续向下找,实现深度的数据劫持
observe(val);
}
}
// 创建Compile构造函数
//el--》dom的id,vm--》Mvvm对象
function Compile(el, vm) {
vm.$el = document.querySelector(el); // 将el挂载到实例上方便调用
// 可以选择移到内存中去然后放入文档碎片中,节省开销
let fragment = document.createDocumentFragment();
while(child = vm.$el.firstChild) {
fragment.appendChild(child); //如果使用appendChid方法将原dom树中的节点添加到DocumentFragment中时,会删除原来的节点
}
// 对el里面的内容进行替换
function replace(flag) {
//对子节点遍历
Array.from(flag.childNodes).forEach(function(node) {
let txt = node.textContent; //返回节点及其后代的文本内容
let reg = /\{\{(.*?)\}\}/g; //正则匹配{{}}
//nodeType节点类型:1代表元素,2代表属性,3代表元素或属性中的文本内容
if(node.nodeType == 1) {
let nodeAttrs = node.attributes; // 获取dom上的所有属性,是个类数组
Array.from(nodeAttrs).forEach(function(attr) {
let name = attr.name; //属性名
let exp = attr.value; //属性值
let val = exp.split(".").reduce(function(data, key) {
return data[key];
}, vm)
if(name == 'v-model') {
node.value = val;
}
// 监听数据变化
new Watcher(vm, exp, function(newVal) {
node.value = newVal; // 当watcher触发时会自动将内容放进输入框中
})
//添加输入监听
node.addEventListener('input', function(e) {
let newVal = e.target.value;
// 相当于给this.c赋了一个新值
// 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
//vm[exp] = newVal;
let arr = exp.split(".");
if(arr.length == 1) {
vm[exp] = newVal;
} else if(arr.length > 1) {
let n = 0;
arr.reduce(function(val, key) {
n++;
if(n == arr.length) {
val[key] = newVal
}
return val[key]
}, vm)
}
})
})
} else if(node.nodeType == 3 && reg.test(txt)) { //reg.test(txt) 有大括号的情况{{}}
function replaceText() {
node.textContent = txt.replace(reg, function(matched, placeholder) {
//console.log(matched, placeholder) //{{album.theme}} album.theme
new Watcher(vm, placeholder, replaceText); // 添加监听变化,进行匹配替换内容
//reduce累计计算
return placeholder.split('.').reduce(function(val, key) {
return val[key];
}, vm);
});
}
replaceText();
}
// 如果还有子节点,继续递归replace
if(node.childNodes && node.childNodes.length) {
replace(node);
}
});
}
replace(fragment); // 替换内容
vm.$el.appendChild(fragment); // 再将文档碎片放入el中
}
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub)
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
})
}
}
//监听函数
function Watcher(vm, exp, fn) {
this.fn = fn;
this.vm = vm;
this.exp = exp;
Dep.target = this;
let arr = exp.split(".");
arr.reduce(function(val, key) {
return val[key]; //这里是关键,这里会调用属性的get方法,所以会订阅此监听
}, vm);
Dep.target = null;
}
Watcher.prototype.update = function() {
let arr = this.exp.split(".");
let vm = this.vm;
//得到新设置的值
let newVal = arr.reduce(function(val, key) {
return val[key]; //这里是关键,这里会调用属性的get方法,所以会订阅此监听
}, vm);
this.fn(newVal);
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<h1>{{singer}}{{song}}</h1>
<p>《{{album.name}}》是{{singer}}2005年11月发行的专辑</p>
<p>主打歌为{{album.theme}}</p>
<p>作词人为{{singer}}等人。</p>
为你弹奏肖邦的{{album.theme}}
<p>发布时间:<input v-model="publishDate" type="text" /></p>
<p>曲名字:<input v-model="album.name" type="text" /></p>
<p>评价:{{commented}}</p>
<p>{{pingfeng}}</p>
</div>
<!--实现的mvvm-->
<script src="mvvm.js"></script>
<script>
// 写法和Vue一样
let mvvm = new Mvvm({
el: '#app',
data: { // Object.defineProperty(obj, 'song', '发如雪');
song: '发如雪',
album: {
name: '十一月的萧邦',
theme: '夜曲'
},
singer: '周杰伦',
publishDate: "2018-09-09"
},
computed: {
commented: function() {
return this.singer + "是个不错的歌手";
},
pingfeng: function() {
return this.album.name + "评分90"
}
},
mounted:function(){
let self = this;
setTimeout(() => {
self.song = "xiugai_青花瓷";
self.singer = "xiugai_周杰伦";
self.publishDate = "2016-12-23"
console.log('所有事情都搞定了');
}, 1500);
}
});
</script>
</body>
</html>