整体分析
- MVVM (Model-View-ViewModel) 是 Vue2 的核心设计模式,核心目标是数据驱动视图:
- Model (数据) 变化时,ViewModel 自动更新 View (视图);
- View 交互时,ViewModel 同步更新 Model;
- Vue2 的 MVVM 核心依赖:
- Observer (数据劫持):通过
Object.defineProperty劫持数据的get/set,实现数据监听,收集依赖、触发更新; - Dep (依赖收集):管理
Watcher,数据变化时通知所有相关Watcher执行更新; - Watcher (观察者):连接
Dep和视图,订阅数据变化,触发视图更新; - Compiler (模板编译):解析模板中的指令和插值表达式,初始化视图,绑定数据与视图的关联;
- MVVM 入口:整合上述模块,作为对外暴露的核心类;
- Observer (数据劫持):通过
- 核心流程:
完整代码实现
目录结构
├── index.html // 测试页面
└── mvvm/
├── MVVM.js // 核心入口类
├── Observer.js // 数据劫持
├── Dep.js // 依赖收集器
├── Watcher.js // 订阅者
└── Compiler.js // 模板解析
Dep.js(依赖收集器)
Dep 类:每个数据属性对应一个 Dep,负责收集依赖 (Watcher),数据变化时调用 notify 通知所有 Watcher 更新;
// 依赖收集器:收集 Watcher,数据变化时通知所有 Watcher
class Dep {
constructor() {
// 存储所有订阅者(Watcher 实例)
this.subs = [];
}
// 添加订阅者
addSub(sub) {
// 只收集 Watcher 实例
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 通知所有订阅者更新
notify() {
this.subs.forEach(sub => sub.update());
}
// 依赖收集:在 get 中调用
depend() {
// Dep.target 是当前活跃的 Watcher
if (Dep.target) {
this.addSub(Dep.target);
}
}
}
// 静态属性:存储当前正在执行的 Watcher
Dep.target = null;
export default Dep;
Observer.js(数据劫持)
Observer 类:通过 Object.defineProperty 劫持数据的 get/set,get 时收集依赖,set 时通知依赖更新;
import Dep from './Dep.js';
// 数据劫持:遍历 data,为每个属性添加 get/set
class Observer {
constructor(data) {
this.data = data;
// 遍历数据,实现劫持
this.walk(data);
}
// 遍历对象所有属性,递归劫持
walk(data) {
if (!data || typeof data !== 'object') {
return;
}
// 遍历对象属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
// 定义响应式属性(核心)
defineReactive(obj, key, val) {
const that = this;
// 递归劫持子属性(如 data.obj.name)
this.walk(val);
// 为每个属性创建专属 Dep
const dep = new Dep();
// 劫持 get/set
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可配置
// 读取属性时触发:收集依赖
get() {
// 收集依赖(Dep.target 是当前 Watcher)
dep.depend();
return val;
},
// 修改属性时触发:通知依赖更新
set(newVal) {
if (newVal === val) return;
val = newVal;
// 新值如果是对象,需要递归劫持
that.walk(newVal);
// 通知所有订阅者更新
dep.notify();
}
});
}
}
export default Observer;
Watcher.js(订阅者)
Watcher 类:初始化时触发数据的 get,将自身挂载到 Dep.target,被 Dep 收集;数据变化时执行 update 触发视图更新;
import Dep from './Dep.js';
// 订阅者:连接数据和视图,接收 Dep 通知并更新视图
class Watcher {
/**
* @param {Object} vm MVVM 实例
* @param {String} expr 数据表达式(如 'name'/'obj.age')
* @param {Function} cb 回调函数(更新视图)
*/
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 存储旧值(用于对比更新)
this.oldVal = this.getOldVal();
}
// 获取旧值(触发 get,收集依赖)
getOldVal() {
// 将当前 Watcher 挂载到 Dep.target
Dep.target = this;
// 解析表达式并获取值(如 'obj.age' → vm.obj.age),获取数据会被 Object.defineProperty 的 get 劫持
const oldVal = this.getVal(this.vm, this.expr);
// 清空 Dep.target,避免重复收集
Dep.target = null;
return oldVal;
}
// 解析表达式,获取对应数据(支持多层属性)
getVal(vm, expr) {
return expr.split('.').reduce((data, key) => data[key], vm.$data);
}
// 更新方法(Dep 通知时调用)
update() {
// 获取新值
const newVal = this.getVal(this.vm, this.expr);
// 新旧值不同时执行回调(更新视图)
if (newVal !== this.oldVal) {
this.oldVal = newVal;
this.cb(newVal);
}
}
}
export default Watcher;
Compiler.js(模板解析)
Compiler 类:解析模板中的指令和插值表达式,初始化视图,创建 Watcher 绑定数据与视图;处理 v-model 实现双向绑定;
import Watcher from './Watcher.js';
// 模板解析:解析指令/插值表达式,初始化视图,创建 Watcher
class Compiler {
constructor(vm) {
this.vm = vm;
this.el = vm.$el;
// 编译模板
this.compile(this.el);
}
// 编译模板(核心)
compile(el) {
const childNodes = el.childNodes;
// 遍历所有子节点
Array.from(childNodes).forEach(node => {
// 元素节点(如 <div>、<input>)
if (this.isElementNode(node)) {
this.compileElement(node);
}
// 文本节点(包含 {{}})
else if (this.isTextNode(node)) {
this.compileText(node);
}
// 递归编译子节点
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
// 编译元素节点(解析指令,如 v-model)
compileElement(node) {
// 获取所有属性
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
const attrName = attr.name;
// 判断是否是指令(以 v- 开头)
if (this.isDirective(attrName)) {
// 提取指令名(如 v-model → model)
const dirName = attrName.slice(2);
const expr = attr.value; // 指令值(如 'name')
// 执行对应指令的编译方法
this[dirName](node, expr);
}
});
}
// 编译 v-model 指令
model(node, expr) {
// 初始化视图:设置 input 值
this.update(node, expr, 'model');
// 监听 input 事件,实现双向绑定
node.addEventListener('input', e => {
const newVal = e.target.value;
// 更新 data 数据(触发 set,通知 Watcher)
this.setVal(this.vm, expr, newVal);
});
}
// 编译文本节点(解析 {{}})
compileText(node) {
// 提取 {{}} 中的表达式(如 {{name}} → name)
const reg = /\{\{(.+?)\}\}/;
const textContent = node.textContent;
if (reg.test(textContent)) {
const expr = RegExp.$1.trim();
// 替换 {{}} 为真实数据,初始化视图
node.textContent = textContent.replace(reg, this.getVal(this.vm, expr));
// 创建 Watcher,数据变化时更新文本
new Watcher(this.vm, expr, newVal => {
node.textContent = textContent.replace(reg, newVal);
});
}
}
// 通用更新方法(初始化视图 + 创建 Watcher)
update(node, expr, dir) {
// 获取更新方法(如 modelUpdater)
const updateFn = this[`${dir}Updater`];
// 初始化视图
updateFn && updateFn(node, this.getVal(this.vm, expr));
// 创建 Watcher,数据变化时更新视图
new Watcher(this.vm, expr, newVal => {
updateFn && updateFn(node, newVal);
});
}
// model 指令更新器(更新 input 值)
modelUpdater(node, value) {
node.value = value;
}
// 解析表达式,获取数据(同 Watcher)
getVal(vm, expr) {
return expr.split('.').reduce((data, key) => data[key], vm.$data);
}
// 设置 data 数据(支持多层属性)
setVal(vm, expr, value) {
expr.split('.').reduce((data, key, index, arr) => {
if (index === arr.length - 1) {
data[key] = value;
}
return data[key];
}, vm.$data);
}
// 判断是否是指令(v- 开头)
isDirective(attrName) {
return attrName.startsWith('v-');
}
// 判断是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
// 判断是否是文本节点
isTextNode(node) {
return node.nodeType === 3;
}
}
export default Compiler;
MVVM.js(核心入口)
MVVM 类:入口类,做数据代理 (简化访问)、初始化 Observer 和 Compiler;
import Observer from './Observer.js';
import Compiler from './Compiler.js';
// MVVM 核心类:整合所有模块
class MVVM {
constructor(options) {
// 挂载配置项
this.$options = options || {};
this.$data = options.data || {};
this.$el = typeof options.el === 'string'
? document.querySelector(options.el)
: options.el;
// 1. 数据代理:将 data 挂载到 vm 上(可通过 vm.name 访问 vm.$data.name)
this._proxyData(this.$data);
// 2. 数据劫持:监听 data 变化
new Observer(this.$data);
// 3. 模板解析:解析指令/插值表达式
new Compiler(this);
}
// 数据代理:简化 data 访问
_proxyData(data) {
Object.keys(data).forEach(key => {
// 此处写成 this 是为了实现「vm.xxx」直接访问「vm._data.xxx」
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(newVal) {
if (newVal === data[key]) return;
data[key] = newVal;
}
});
});
}
}
export default MVVM;
测试页面 index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Vue2 MVVM 手写实现</title>
</head>
<body>
<div id="app">
<h1>{{name}}</h1>
<h2>{{age}}</h2>
<h3>{{obj.gender}}</h3>
<input type="text" v-model="name">
<input type="text" v-model="obj.gender">
</div>
<script type="module">
import MVVM from './mvvm/MVVM.js';
// 初始化 MVVM
const vm = new MVVM({
el: '#app',
data: {
name: 'Vue2 MVVM',
age: 8,
obj: {
gender: '男'
}
}
});
// 测试数据更新(触发视图更新)
setTimeout(() => {
vm.name = '手写 MVVM 成功!';
vm.obj.gender = '女';
}, 2000);
</script>
</body>
</html>
总结
- Vue2 MVVM 的核心是数据劫持 (Object.defineProperty) + 依赖收集 + 观察者模式,实现数据驱动视图;
- 核心模块分工:Observer 监听数据、Dep 管理依赖、Watcher 触发更新、Compiler 解析模板、MVVM 整合所有模块;
- 双向绑定的本质是:v-model 既通过 Watcher 绑定数据 → 视图,又通过输入事件绑定视图 → 数据;
TypeScript👉 类 (class) 初探
上一篇