之前百度前端训练营有个作业是实现简单的MVVM框架,刚开始初学时,对我来说比较困难, 之后就在学习中,就没有再提 这个,昨天又看到了MVVM框架的一些问题,想弥补上之前没有实现的遗憾,于是就用JavaScript简单实现一下。后续会再用typescript再尝试实现。
具体代码:GitHub - TechGuo/MVVM: 简单实现MVVM框架
主要是实现了MVVM中的双向绑定、数据劫持以及发布订阅
MVVM(Model-View-ViewModel)是一种设计模式,它将用户界面(视图)和业务逻辑(模型)分离,使用中间层(视图模型)来连接它们。MVVM框架是实现MVVM设计模式的框架,常见的MVVM框架有Vue、Angular、React等。
MVVM框架的主要优势有:
- 数据驱动:MVVM框架采用数据驱动的方式,只需要更新数据就能自动更新视图,减少了手动DOM操作,提高了开发效率。
- 双向绑定:MVVM框架支持双向数据绑定,可以通过在视图模型中的属性上使用v-model指令实现视图和模型之间的双向绑定,当视图中的数据发生改变时,模型也会自动更新,反之亦然。
- 组件化:MVVM框架支持组件化开发,将界面拆分为多个独立的组件,每个组件有自己的视图、模型和行为,方便重用和维护。
- 易于测试:MVVM框架将业务逻辑和界面分离,使得业务逻辑可以更容易地进行单元测试。
- 优化性能:MVVM框架可以通过虚拟DOM技术、异步渲染等方式优化性能,提高应用的响应速度和渲染效率。
下面是我的代码的主要结构
上面是封装了五个类,其中Compile中使用的到的一些方法是直接存放在了一个对象中,方便一些方法的复用。
下面是index.html的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MVVM简单实现</title>
<script src="./MVVM.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="message">
<div>{{message}}和{{message}}</div>
<input type="text" v-model="gender.male">
<ul>
<li><span></span></li>
<li>
<div>{{gender.fale}}</div>
</li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<script>
let mvvm = new MVVM({
el: '#app',
data: {
message: 'hello, MVVM',
gender: {
fale: '男',
male: '女'
}
}
})
</script>
</body>
</html>
上面引入的文档是我们自定义封装的MVVM文档。MVVM框架的实现思路是结合着下面这张图来进行的:
初始化一个MVVM类
class MVVM {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
//对模板进行编译
if (this.$el) {
// 数据劫持 就是把对象的所有属性, 改成get方法和set发那个发
new Observer(this.$data)
this.proxyData(this.$data);
// 用数据和 元素进行编译
new Compile(this.$el, this);
}
}
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data(key)
},
set(newValue) {
data[key] = newValue;
}
})
})
}
}
对node元素和文本进行编译(此时尚未进行监听)
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
// 如果el元素存在,能够获取到,才进行编译
// 1先将真事的dom存入到内存当中
let fragment = this.node2frament(this.el)
// 编译 提取想要的元素结点 v-model和文本结点{{}}
this.compile(fragment);
// 把编译好的fragment再塞回页面中
this.el.appendChild(fragment);
}
}
/* 辅助函数区域 */
// 判断当前元素是不是node结点
isElementNode(node) {
return node.nodeType === 1;
}
/* 核心方法区 */
node2frament(el) {
// 将el内容全部都放进到内存中
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
};
return fragment;//内存中的结点
}
compile(fragment) {
let childNodes = fragment.childNodes;
// 需要递归,去拿到所有文本结点,也就是子结点的子结点,子结点的子结点
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
// 元素结点
// 这里需要编译元素
this.compileElment(node);
this.compile(node);
} else {
// 文本结点
// 这里需要编译文本
this.compileText(node);
}
})
}
compileElment(node) {
// 编译带有 v-model v-for v-if ...属性的元素
let attrs = node.attributes;
Array.from(attrs).forEach(attr => {
// 判断属性名字是不是包含v-
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 取到对应的值放在结点中
let expr = attr.value;
let type = attrName.slice(2);
CompileUtil[type](node, this.vm, expr);
}
})
}
compileText(node) {
// 判断文本是否含有{{}}
let expr = node.textContent;//获取结点的文本
let reg = /\{\{([^}]+)\}\}/g
if (reg.test(expr)) {
CompileUtil['text'](node, this.vm, expr);
}
}
isDirective(name) {
return name.includes('v-');
}
}
CompileUtil = {
//文本处理,对文本情况是gender.male 的,获取到male的value值,并返回
getVal(vm, expr) {
return expr.split('.').reduce((prev, next) => {
return prev[next]
}, vm.$data);
},
setVal(vm, expr, value) {
expr = expr.split('.');
return expr.reduce((prev, next, currentIndex) => {
if (currentIndex === expr.length - 1) {
return prev[next] = value;
}
return prev[next]
}, vm.$data)
},
getTextVal(vm, expr) {
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
return this.getVal(vm, arguments[1]);
})
},
text(node, vm, expr) {
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(vm, expr);
// console.log(value);
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
// console.log(arguments[1] + 'hhhh');
new Watcher(vm, arguments[1], (newValue) => {
//如果数据变化了,文本结点需要重新获取依赖的数据更新文本中的内容
updateFn && updateFn(node, this.getTextVal(vm, expr))
})
})
updateFn && updateFn(node, value)
},
model(node, vm, expr) {//输入框处理
let updateFn = this.updater['modelUpdater'];
new Watcher(vm, expr, (newValue) => {
//当值变化后会调用cb 将新的值传递过来
updateFn && updateFn(node, this.getVal(vm, expr))
})
node.addEventListener('input', (e) => {
let newValue = e.target.value;
this.setVal(vm, expr, newValue)
})
updateFn && updateFn(node, this.getVal(vm, expr))
},
updater: {
textUpdater(node, value) {
// console.log(value);
node.textContent = value
},
modelUpdater(node, value) {
node.value = value
}
}
}
上面代码中需要注意的是,先判断el是不是目标结点,不是就获取到目标结点,然后拿到目标结点后进行编译操作,
因为需要同时对node元素、文本元素进行编译,所以在编译中分别调用了compileElement()方法和conpileText()方法。node元素中包含的可能存在子节点,所以需要进行递归操作保证能够对每一个结点进行编译,而文本元素就不要进行递归。
劫持数据,在使用或者设置某的对象的属性的时候,控制对象属性的设置和读取,通过Object.defineProperty来劫持对象属性的setter和getter操作。
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
// 要对这个data数据将原有的属性改成set和get的形式
if (!data || typeof data !== 'object') {
// 如果数据不存在,或者不是对象数据,就什么都不操作
return;
}
// 是对象类型,就要一一劫持
Object.keys(data).forEach(key => {
// console.log(data);
//定义劫持的响应式
this.defineReactive(data, key, data[key]);//形参分别是:对象,key关键字,key对应的value值
// 还需要进行深度劫持,就是将对象中的对象添加上set和get方法,这里可以直接进行递归
this.observe(data[key])
})
}
// 定义劫持的响应式
defineReactive(obj, key, value) {
let that = this;
// 创建订阅
let dep = new Dep();//每个变化的数据,都会对应一个数组,这个数组是存放所有更新的操作
// 在获取某个值的适合,想弹窗
Object.defineProperty(obj, key, {
configurable: true,//指定通过循环可以拿到
enumerable: true,//指定可以删除
get() {
Dep.target && dep.addSub(Dep.target)
return value;
},
set(newValue) {
if (newValue != value) {
that.observe(newValue)//这里是对newvalue也进行盘算是否是对象,并进行劫持
value = newValue;
dep.notify();//通知所有人 数据更新了
}
}
})
}
}
上面代码中需要对data进行判断,如果类型不是对象,就不操作,如果是,就对添加set()和get()方法。这里用到的也是Object.defineProperty()。
对数据变化进行监听
// 观察者的目的,就是给需要变化的那个元素添加一个观察者
// 当数据变化后执行对应的方法
class Watcher {
constructor(vm, expr, cd) {//vm是实例,expr是监听的key,cd就是返回函数
this.vm = vm;
this.expr = expr;
this.cd = cd;
// 先获取一个老的值
this.value = this.get();
}
getVal(vm, expr) {
return expr.split('.').reduce((prev, next) => {
return prev[next]
}, vm.$data);
}
get() {
Dep.target = this;
let value = this.getVal(this.vm, this.expr);
Dep.target = null;
return value
}
// 对外暴露的方法
updata() {
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value;
if (newValue != oldValue) {
this.cd(newValue);//调用watch的callback
}
}
}
上面就是创建了watcher类,用于对数据变化进行监听,这里主要就是对CompleElement和CompileText中设置了监听器,代码部分就呈现在了compileUtil中的text()和model()中。
订阅类
/ 订阅类
class Dep {
constructor() {
// 订阅的数组
this.subs = []
}
// 添加订阅的方法
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => watcher.updata())
}
}