实现一个简易的MVVM
手写实现一个包含数据劫持、依赖收集和视图更新的简易MVVM框架
问题
MVVM(Model-View-ViewModel)是一种软件架构模式,是实现数据与视图的双向绑定。当数据发生变化时,视图自动更新;当视图发生变化时,数据也会同步更新。
本题需要实现一个简易的MVVM框架,包含以下功能:
- 数据劫持:监听数据的变化
- 依赖收集:收集数据与视图的依赖关系
- 模板编译:解析模板指令(如
v-model、v-text、{{}}) - 视图更新:数据变化时自动更新视图
- 双向绑定:表单输入时同步更新数据
解答
// 观察者类 - 负责更新视图
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 保存旧值
this.oldValue = this.getOldValue();
}
// 获取旧值
getOldValue() {
Dep.target = this; // 将当前watcher挂载到Dep.target
const oldValue = this.getVMValue(this.vm, this.expr);
Dep.target = null; // 清空Dep.target
return oldValue;
}
// 更新视图
update() {
const newValue = this.getVMValue(this.vm, this.expr);
if (newValue !== this.oldValue) {
this.cb(newValue);
}
}
// 获取VM中的数据值
getVMValue(vm, expr) {
let data = vm.$data;
expr.split('.').forEach(key => {
data = data[key];
});
return data;
}
}
// 依赖收集类
class Dep {
constructor() {
this.subs = []; // 存储所有的watcher
}
// 添加watcher
addSub(watcher) {
this.subs.push(watcher);
}
// 通知所有watcher更新
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
// 数据劫持类
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (data && typeof data === 'object') {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
}
// 定义响应式数据
defineReactive(obj, key, value) {
// 递归观察子属性
this.observe(value);
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
// 收集依赖
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newVal) => {
if (newVal !== value) {
// 如果新值是对象,也要进行观察
this.observe(newVal);
value = newVal;
// 通知所有watcher更新
dep.notify();
}
}
});
}
}
// 编译类 - 负责编译模板
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 1. 将真实DOM移入到内存中(fragment)
const fragment = this.node2Fragment(this.el);
// 2. 编译模板
this.compile(fragment);
// 3. 将编译好的fragment放回页面
this.el.appendChild(fragment);
}
// 编译方法
compile(fragment) {
const childNodes = fragment.childNodes;
[...childNodes].forEach(child => {
if (this.isElementNode(child)) {
// 元素节点,编译元素
this.compileElement(child);
} else {
// 文本节点,编译文本
this.compileText(child);
}
// 递归编译子节点
if (child.childNodes && child.childNodes.length) {
this.compile(child);
}
});
}
// 编译元素节点
compileElement(node) {
const attributes = node.attributes;
[...attributes].forEach(attr => {
const { name, value } = attr;
if (this.isDirective(name)) {
const [, directive] = name.split('-');
const [dirName, eventName] = directive.split(':');
// 更新数据,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
// 移除指令属性
node.removeAttribute('v-' + directive);
} else if (this.isEventName(name)) {
// @click="handleClick"
const [, eventName] = name.split('@');
compileUtil['on'](node, value, this.vm, eventName);
}
});
}
// 编译文本节点
compileText(node) {
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil['text'](node, content, this.vm);
}
}
// 判断是否是指令
isDirective(attrName) {
return attrName.startsWith('v-');
}
// 判断是否是事件名
isEventName(attrName) {
return attrName.startsWith('@');
}
// 将DOM节点移入内存
node2Fragment(el) {
const fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
// 判断是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
}
// 编译工具对象
const compileUtil = {
// 获取数据值
getVal(expr, vm) {
return expr.split('.').reduce((data, currentVal) => {
return data[currentVal];
}, vm.$data);
},
// 设置数据值
setVal(expr, vm, inputVal) {
expr.split('.').reduce((data, currentVal, index, arr) => {
if (index === arr.length - 1) {
data[currentVal] = inputVal;
}
return data[currentVal];
}, vm.$data);
},
// 获取文本内容(处理{{}})
getContentVal(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1].trim(), vm);
});
},
// v-text指令
text(node, expr, vm) {
let value;
if (expr.indexOf('{{') !== -1) {
// 处理 {{person.name}} -- {{person.age}}
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 为每个{{}}创建watcher
new Watcher(vm, args[1].trim(), () => {
this.updater.textUpdater(node, this.getContentVal(expr, vm));
});
return this.getVal(args[1].trim(), vm);
});
} else {
// 处理 v-text="person.name"
value = this.getVal(expr, vm);
new Watcher(vm, expr, (newVal) => {
this.updater.textUpdater(node, newVal);
});
}
this.updater.textUpdater(node, value);
},
// v-html指令
html(node, expr, vm) {
const value = this.getVal(expr, vm);
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal);
});
this.updater.htmlUpdater(node, value);
},
// v-model指令
model(node, expr, vm) {
const value = this.getVal(expr, vm);
// 数据 => 视图
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal);
});
// 视图 => 数据
node.addEventListener('input', (e) => {
this.setVal(expr, vm, e.target.value);
});
this.updater.modelUpdater(node, value);
},
// v-on指令
on(node, expr, vm, eventName) {
const fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false);
},
// 更新器
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
}
}
};
// MVVM类
class MVVM {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1. 实现数据劫持
new Observer(this.$data);
// 2. 实现模板编译
new Compiler(this.$el, this);
// 3. 代理$data,使得可以通过this.xxx访问数据
this.proxyData(this.$data);
}
}
// 代理数据
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newVal) {
data[key] = newVal;
}
});
});
}
}
使用示例
// HTML结构
/*
<div id="app">
<h2>{{person.name}} -- {{person.age}}</h2>
<h3 v-text="person.name"></h3>
<div v-html="htmlStr"></div>
<input type="text" v-model="message">
<p>{{message}}</p>
<button @click="handleClick">点击</button>
</div>
*/
// 创建MVVM实例
const vm = new MVVM({
el: '#app',
data: {
person: {
name: '张三',
age: 18
},
message: 'Hello MVVM',
htmlStr: '<span style="color: red;">这是HTML内容</span>'
},
methods: {
handleClick() {
console.log('按钮被点击了');
this.person.name = '李四';
this.message = '数据已更新';
}
}
});
// 测试数据响应式
setTimeout(() => {
vm.person.name = '王五'; // 视图会自动更新
vm.person.age = 20;
}, 2000);
// 可以直接通过vm访问数据
console.log(vm.person.name); // '张三'
console.log(vm.message); // 'Hello MVVM'
关键点
-
数据劫持(Observer):使用
Object.defineProperty劫持数据的 getter 和 setter,在 getter 中收集依赖,在 setter 中触发更新 -
依赖收集(Dep):每个响应式属性都有一个 Dep 实例,用于收集依赖该属性的所有 Watcher,当属性变化时通知所有 Watcher 更新
-
观察者(Watcher):连接数据和视图的桥梁,当数据变化时执行回调函数更新视图。通过
Dep.target实现依赖收集 -
模板编译(Compiler):解析模板中的指令(v-model、v-text、v-html、@click等)和插值表达式({{}}),为每个指令创建对应的 Watcher
-
双向绑定:v-model 指令通过监听 input 事件实现视图到数据的更新,通过 Watcher 实现数据到视图的更新
-
数据代理:将
$data中的属性代理到 MVVM 实例上,使得可以通过this.xxx直接访问数据 -
DocumentFragment:使用文档碎片在内存中操作 DOM,减少页面回流和重绘,提高性能
-
递归处理:对嵌套对象进行递归观察,对子节点进行递归编译,确保所有数据和节点都被正确处理
目录