MVVM的最大表现在于数据绑定。不同的框架实现数据绑定的方式有所不同,vue使用的是数据劫持。下面使用defineProperty,自己动手实现一个MVVM。
先上代码吧
<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>T-MVVM</title>
</head>
<body>
<div id="app">
<div>{{name}} ---- {{msg}}----<span t-text="name"></span></div>
<div t-text="name">13131</div>
<input id="inputBlock" t-model="msg"/>
<input id="inputBlock" t-model="msg"/>
<input id="inputBlock" t-model="name"/>
</div>
<script src="util.js"></script>
<script src="Dep.js"></script>
<script src="Watcher.js"></script>
<script src="Compile.js"></script>
<script src="Observe.js"></script>
<script src="Mvvm.js"></script>
<script>
let vm = new Mvvm('#app',{
data : {
name:'lala',
msg:'baba'
}
})
</script>
</body>
</html>
实现的效果如下:
结构图
在动手敲代码前,先构思个整体结构图,捋清思路再动手,可以提高敲代码的效率。
上图中,MVVM主要要实现两块:指令解析(Compile)和数据劫持(Observe)。
Compile用于初始化HTML代码中使用的指令;
Observe用于劫持数据的get、set方法;
Watcher用于链接数据与视图,Dep用于存储watcher。
下面,我们先从Observe下手
Observe
class Observe{
constructor(data){
this.$data = data;
// 劫持数据
this.observe(this.$data);
}
/**
* 递归对数据进行劫持
* @param {object} data 要进行劫持的数据对象
*/
observe(data){
if (!data || Object.prototype.toString.call(data).match(/\[object (.+)\]/)[1] !== 'Object') {
return;
}
let keys = Object.keys(data);
for(const key of keys){
this.observe(data[key]);
this.setDefine(data,key);
}
}
/**
* 对单个数据进行劫持
* @param {object} data 要进行劫持的数据
* @param {string} key 要进行劫持的数据的key
*/
setDefine(data,key){
let oldValue = data[key];
// 每个数据对应一个dep,当劫持到数据之后通知dep更新数据
let dep = new Dep();
Object.defineProperty(data,key,{
// 可配置,可以修改以及删除等
configurable: true,
// 可枚举
enumerable: true,
get:() => {
//Dep.target为watcher对象,初始化watcher时会给Dep.target赋值并触发这个get
Dep.target ? dep.addObserve(Dep.target) : '';
return oldValue;
},
set: (newValue) => {
if(oldValue !== newValue){
this.observe(newValue);
oldValue = newValue;
// 通知更新
dep.notify();
}
}
})
}
}
Compile
class Compile{
constructor(el,data){
this.$data = data;
this.$el = Compile.isElementNode(el) ? el : document.querySelector(el);
if(this.$el){
this.compile(this.$el);
}
}
/**
* 递归每个元素,解析含有指令的元素
* @param {object} el dom节点
*/
compile(el){
let text = el.textContent;
let child = el.childNodes;
for(const node of Array.from(child)){
// 如果是element节点,则进行attributes的解释,并进行下一步递归
if(Compile.isElementNode(node)){
this.compileElement(node);
this.compile(node);
}else if(Compile.isTextNode(node)){
// 如果是text节点,则进行t-text的处理
this.compileText(node);
}
}
}
/**
* 判断是否是元素节点
* @param {*} node dom节点
*/
static isElementNode(node){
return node.nodeType === 1;
}
/**
* 判断是否是文本节点
* @param {*} node dom节点
*/
static isTextNode(node){
return node.nodeType === 3;
}
/**
* 编译文本节点
* @param {*} node dom节点
*/
compileText(node){
let allContent = node.textContent;
let newRes = allContent.replace(/\{\{([^}]+)\}\}/g,(word,content,i,str) => {
let watcher = new Watcher(this.$data,content,(newValue) => {
node.textContent = util.replaceValueByoldData(allContent,this.$data);
})
return util.getValueByKeyFromData(content,this.$data);
});
node.textContent = newRes;
}
/**
* 编译元素节点(遍历编译attribt)
* @param {*} node dom节点
*/
compileElement(node){
let reg = /^t-/;
for(const attr of node.attributes){
if(reg.test(attr.nodeName)){
let key = attr.nodeValue;
switch(attr.nodeName){
case 't-text':
new Watcher(this.$data,key,(newValue) => {
node.textContent = util.getValueByKeyFromData(key,this.$data);
})
node.textContent = util.getValueByKeyFromData(key,this.$data);
break;
case 't-model':
let that = this;
node.value = util.getValueByKeyFromData(attr.nodeValue,this.$data);
new Watcher(this.$data,key,(newValue) => {
node.value = util.getValueByKeyFromData(attr.nodeValue,this.$data);
})
node.addEventListener('input',function(e){
util.setDataByKey(that.$data,key,e.target.value)
})
}
}
}
}
}
下面就要实现连接Compile和Observe的Watcher和Dep了
Watcher
class Watcher{
constructor(data,key,callback){
this.data = data
this.key = key;
this.callback = callback;
Dep.target = this;
// 这一步会触发Observe劫持到的set方法
this.oldVale = util.getValueByKeyFromData(this.key,data);
Dep.target = null;
}
upData(){
let newValue = util.replaceValueByoldData(this.key,this.data)
this.callback ? this.callback(newValue) : '';
}
}
Dep
class Dep{
static target = null;
constructor(){
/** 观察列表,用于存储多个warcher */
this.observes = [];
}
addObserve(watcher){
this.observes.push(watcher);
}
notify(){
for(const watcher of this.observes){
watcher.upData();
}
}
}
Util
util存放公用的函数
const util = {
/**
* 通过key获取data中的响应数据
* @param {string} content 数据的key,例如"name"、"animal.cat"
* @param {object} data 数据集
*/
getValueByKeyFromData(content,data){
let keys = content.split('.');
return keys.reduce((pre,next,index) => {
return data[next]
},data)
},
/**
* 解析content中带有双括号绑定的内容,返回替换后的内容
* @param {string} content 要替换的字符串,例如"{{name}}"、"{{animal.cat}}-----{{name}}"
* @param {object} data 数据集
*/
replaceValueByoldData(content,data){
let newRes = content.replace(/\{\{([^}]+)\}\}/g,(word,p1,i,str) => {
return util.getValueByKeyFromData(p1,data);
});
return newRes;
},
/**
* 更新data中的值
* @param {object} data 数据集
* @param {string} key 数据的key,例如"name"、"animal.cat"
* @param {string} newValue 替换值
*/
setDataByKey(data,key,newValue){
let keys = key.split('.');
let setData = keys.reduce((pre,next,index) => {
if(index === keys.length - 1){
return data[next] = newValue;
}
return data[next]
},data)
}
};
MVVM
class Mvvm{
constructor(dom,per){
this.$dom = dom;
this.$per = per;
new Observe(this.$per.data);
new Compile(this.$dom,this.$per.data)
}
}
总结
到这里,一个简单的MVVM就基本实现了。详细的解释都写在代码注释里面了。代码还有很多不完善的地方,还请小伙伴们能指出,一起学习一起进步。