引言
在前端开发中,MVVM(Model-View-ViewModel)框架如 Vue.js、React 等,极大地提升了布局与逻辑分离、数据驱动视图更新的效率。为了深入理解其背后的原理,我手动实现一个简化版 MVVM 框架。通过下面这个小项目,根据自己理解从零构建一个具备响应式数据绑定、模板编译与更新机制的 MVVM 框架。菜鸟入门,如有错误,大佬请指正(骂轻点)。
项目简介
-
技术栈:ES6+ JavaScript
-
功能:
- 数据响应式:对数据对象进行劫持,实现数据变化触发视图更新
- 模板编译:解析带有指令和插值的模板,绑定到数据
- 发布-订阅:实现依赖收集和通知机制
MVVM 原理概览
- Model(数据层) :原始数据对象,通过代理或定义访问器,实现监听与更新。
- View(视图层) :用户界面模板,通常是含有特殊指令或插值的 HTML。
- ViewModel(视图-数据连接层) :核心桥梁,完成模板编译、依赖收集,并在 Model 变化时更新 View。
关键技术点:
- 数据劫持(Object.defineProperty 或 Proxy)
- 依赖收集(Dep/Watcher)
- 模板编译(解析指令、插值表达式)
- 更新机制(批量更新或同步更新)
核心模块设计
1. Observer(数据劫持)
使用 Object.defineProperty
(兼容性更好)或 Proxy
对数据对象进行深度遍历,并在属性的 get
与 set
方法中:
get
时收集依赖set
时通知依赖更新
javascript
class Observer {
constructor(data) {
this.walk(data);
}
walk(obj) {
Object.keys(obj).forEach(key => this.defineReactive(obj, key, obj[key]));
}
defineReactive(obj, key, val) {
// 递归处理嵌套对象
if (typeof val === 'object' && val !== null) {
new Observer(val);
}
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// 新值为对象则继续监听
if (typeof newVal === 'object') {
new Observer(newVal);
}
dep.notify();
}
});
}
}
2. Dep 与 Watcher(发布-订阅)
- Dep :维护一个订阅者数组,提供
addSub
与notify
方法。 - Watcher:代表一个观察者。每当依赖属性变化,调用更新方法,触发视图更新。
javascript
class Dep {
constructor() {
this.subs = [];
}
addSub(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach(w => w.update());
}
}
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
Dep.target = this;
// 触发一次 getter,完成依赖收集
this.value = this.get();
Dep.target = null;
}
get() {
const keys = this.expr.split('.');
let val = this.vm.$data;
keys.forEach(k => val = val[k]);
return val;
}
update() {
const newVal = this.get();
const oldVal = this.value;
if (newVal !== oldVal) {
this.value = newVal;
this.cb(newVal);
}
}
}
3. Compiler(模板编译)
解析模板中所有文本节点与指令(如 v-model
、v-text
、v-on
等),并为每个绑定创建对应的 Watcher。
javascript
class Compiler {
constructor(el, vm) {
this.el = document.querySelector(el);
this.vm = vm;
this.fragment = this.node2Fragment(this.el);
this.compile(this.fragment);
this.el.appendChild(this.fragment);
}
node2Fragment(el) {
const frag = document.createDocumentFragment();
let child;
while (child = el.firstChild) {
frag.appendChild(child);
}
return frag;
}
compile(node) {
node.childNodes.forEach(child => {
if (child.nodeType === 1) {
// 元素节点
this.compileElement(child);
} else if (child.nodeType === 3) {
// 文本节点
this.compileText(child);
}
if (child.childNodes && child.childNodes.length) {
this.compile(child);
}
});
}
compileElement(node) {
Array.from(node.attributes).forEach(attr => {
const { name, value } = attr;
if (name.startsWith('v-')) {
const [, directive] = name.split('-');
CompilerUtil[directive](node, this.vm, value);
}
});
}
compileText(node) {
const reg = /{{(.+?)}}/g;
const text = node.textContent;
if (reg.test(text)) {
CompilerUtil['text'](node, this.vm, text);
}
}
}
const CompilerUtil = {
text(node, vm, expr) {
const value = expr.replace(/{{(.+?)}}/g, (_, g) => {
new Watcher(vm, g.trim(), newVal => {
this.updater.textUpdater(node, this.getVal(vm, g.trim()));
});
return this.getVal(vm, g.trim());
});
this.updater.textUpdater(node, value);
},
model(node, vm, expr) {
const val = this.getVal(vm, expr);
new Watcher(vm, expr, newVal => {
this.updater.modelUpdater(node, newVal);
});
node.addEventListener('input', e => {
this.setVal(vm, expr, e.target.value);
});
this.updater.modelUpdater(node, val);
},
getVal(vm, expr) {
return expr.split('.').reduce((data, key) => data[key], vm.$data);
},
setVal(vm, expr, value) {
expr.split('.').reduce((data, key, i, arr) => {
if (i === arr.length - 1) data[key] = value;
return data[key];
}, vm.$data);
},
updater: {
textUpdater(node, value) {
node.textContent = value;
},
modelUpdater(node, value) {
node.value = value;
}
}
};
4. Vue 类入口
javascript
class MyVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
// 1. 数据劫持
new Observer(this.$data);
// 2. 模板编译与挂载
new Compiler(this.$el, this);
}
}
项目结构示例
project
mvvm-demo/
├── index.html // 包含模板标记及引用脚本
├── mvvm.js // 包含 Observer, Dep, Watcher, Compiler, MyVue
└── README.md
使用示例
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>MVVM Demo</title>
</head>
<body>
<div id="app">
<p>{{ message }}</p>
<input type="text" v-model="message">
</div>
<script src="mvvm.js"></script>
<script>
const vm = new MyVue({
el: '#app',
data: {
message: 'Hello MVVM'
}
});
</script>
</body>
</html>
总结
通过以上步骤,我们实现了一个最简化的 MVVM 框架:它支持数据劫持、依赖收集、模板编译和双向绑定。这个小项目有助于理解 Vue.js 等框架内部的核心机制。未来可扩展指令系统、性能优化与批量更新策略,实现更完整的功能。