一,基础知识
1,何为MVVM(双向数据绑定)
双向数据绑定(MVVM):数据(M)发生变化时立即影响视图(V),而视图(V)发生改变也会立即影响数据(M)
2,实现数据绑定的方法
实现数据绑定的做法有大致如下几种:
1,观察者模式(backbone)
发布者发布事件,观察者监听事件。当某些方法被触发时,就通知观察者执行预定操作。
观察者可以使用自己写也可以使用es7最新添加的数据绑定方法Object.observe()。资料参考
https://www.w3ctech.com/topic/1097
2,脏值检查(angular)
angular就是通过脏值检测实现的。首先angular会解析dom中的命令,然后记录所有变量的当前值,当发生触发操作之后,通过apply或者digest进入脏检查环节。上次记录值与当前值是否一致。不一致就更新视图,然后再脏值检测一次直到数据不再发生变化。如果一致就不做任何操作
3,设置属性访问器(vue)
通过Object.defineProperty()来获取每个属性的setter,getter,在数据变动时通知订阅者进行处理。
二,动手实现一个MVVM框架
1,目标与使用规则
我们想要实现的效果是这样的:
使用类vue的定义方式,即通过自定义元素属性来进行数据标记。el绑定html的作用域,data绑定数据,@+事件名绑定方法。
任何一个绑定了数据"data"(data=“say”)的输入框的内容发生变动,都将即时更新所有绑定了"data"数据的网页元素。点击绑定了事件(@click=“go()”)的元素,会触发与点击事件绑定的方法(“go()”) 。
<!--html部分:-->
<div id="mvEl">
<p id="label" data="say">这个p标签绑定了数据"say",这里绑定的是innerText</p>
<input id="input" type="text" value='这个输入框的值也绑定了数据"say"' data="say" />
<div @click="go" data="say">这个div标签绑定了点击事件"go"。
<input id="input2" type="text" value="这是一个嵌套在div标签中的输入框,它也绑定了数据say,这里绑定的是value" data="say" />
</div>
</div>
<!--js部分:-->
<script type="text/javascript">
//实例化mvvm
var vm = new mvvm({
//绑定域
el: "mvEl",
//绑定数据
data: {
say: "这是数据1",
say1: "这是数据2"
},
//动作方法
action: {
go: () => {
vm.data.say+="废话";
console.info("这是个方法呀");
}
}
});
<script>
2,发布订阅
为每个绑定的元素设置访问器,当setter触发时就通知所有的订阅者,以此达到即时更新的效果。
//发布器
function observe(data, dep) {
if (!data || typeof data !== 'object') {
return;
}
// 获取data中的所有属性(say,say1)
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key], dep);
});
};
//为该对象设置属性,添加getter,setter,订阅者通知
function defineReactive(data, key, val, dep) {
// 监听子属性
observe(val);
//设置访问器及属性
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 不能再define
configurable: false,
//设置getter
get: () => {
return val;
},
//设置setter
set: (newVal) => {
//console.log(val, '=>', newVal);
//更新值
val = newVal;
// 通知所有订阅者
dep.notify();
}
});
}
//订阅器
function dep() {
//订阅数组
this.subs = [];
//添加订阅
this.addSub = (sub) => {
this.subs.push(sub);
};
//删除订阅
this.delSub = (key) => {
// //delete sub
};
//触发回调,通知所有订阅者,触发update()
this.notify = () => {
this.subs.forEach((sub) => {
sub.update();
});
}
};
let dep = new Dep();
observe(vm.data, dep);
3,html指令解析与属性处理
在html部分中,我们在元素标签上使用的自定义属性“data”与“@click”,下面的代码将演示如何处理自定义属性
要想读取每个元素的属性,首先必须获取所有网页元素,然后再遍历每个元素的属性。而dom对象的children属性会方便地告知你当前元素下有多少子元素。
//循环获取所有dom树
function mvvm(vm) {
//绑定域 获取主元素
let el = document.getElementById(vm.el);
//循环所有网页元素
function eachElDo(el, vm) {
//处理data属性
attrHandler_data(el, vm);
//处理事件方法
attrHandler_event(el, vm)
for (var i = 0; i < el.children.length; i++) {
eachElDo(el.children[i], vm);
}
}
eachElDo(el, vm)
}
遍历该元素下的所有属性,处理自定义属性。值得注意的是不同网页标签取值方式不同。比如p的内容是在标签内的,而获取input内容则需要使用value,由于div是布局元素,所以即使里面绑定了data也不会被显示出来。判定标签使用的是.localName。不是应该使用tagName么?localname默认都是小写,我也就直接拿来用了,,,
//处理data属性,并绑定数据
function attrHandler_data(el, vm) {
if (el.getAttribute("data")) {
//回调方法,设定发生数据更新时的动作
let action = {
update: () => {
//设定不同网页元素标签,使用不同的取值方式。
switch (el.localName) {
case "input":
el.value = vm.data[el.getAttribute("data")];
break;
case "div":
break;
default:
el.innerText = vm.data[el.getAttribute("data")];
}
}
}
//输入(事件)绑定 为绑定了data的元素添加事件。
switch (el.localName) {
case "input":
el.addEventListener('input', () => {
vm.data[el.getAttribute("data")] = el.value;
})
break;
case "div":
break;
default:
el.addEventListener('onchange', () => {
vm.data[el.getAttribute("data")] = el.innerText;
})
}
//添加观察者
dep.addSub(action);
}
}
解析事件指令
这里需要注意的是我们的指令是“@”+事件名。事件名就是html默认的时间click、onchange、touchover等等。写法是<input @click=“go” /> 。我们需要先把@与click分离,再绑定到事件方法adEventListener上。
function attrHandler_event(el, vm) {
//遍历元素所有属性
for (var i = 0; i < el.attributes.length; i++) {
//是否有事件属性
if (/@/i.test(el.attributes[i].nodeName)) {
//用正则分离"@"与事件名
let myEvent = el.attributes[i].nodeName.replace("@", "");
//绑定事件
el.addEventListener(myEvent, () => {
//事件回调方法
vm.action[el.getAttribute('@' + myEvent)]();
}, false)
}
}
}
到此为止,100行就实现了类vue的mvvm框架。
准备继续更新一些新的功能,有兴趣的同学可以 git:https://github.com/155366311/mvvm