0. 整体架构
这个 mini-vue 专注于“最小可解释闭环”:数据变化如何一步步驱动 DOM 更新。
先记住一句话:模板里用到的数据,会被记录;数据一变,只更新用到它的地方。
- 入口
Vue:初始化 data、代理属性、启动编译。 Observer:用Object.defineProperty拦截 getter / setter。Dep + Watcher:建立“谁依赖了谁”,并在变更时定点通知。Compiler:识别{{ }}与v-model,把数据和 DOM 绑定起来。
- 初始化:先把 data 变成响应式,再扫描模板,把绑定关系建好。
- 读数据:触发 getter,完成“依赖收集”。
- 写数据:触发 setter,通知相关 Watcher 更新 DOM。
1. proxyData:让 data 挂到实例上
把 vm.$data.message 简化成 vm.message,符合 Vue 的使用习惯。
这一章不负责更新视图,只负责“访问体验”:把 data 的键代理到 vm,后面模板和业务代码都能直接用 vm.xxx。
class Vue {
constructor(options) {
// 保存配置和挂载点
this.$options = options || {};
this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el;
// 原始 data 对象
this.$data = options.data || {};
// 1) 先做 data 代理:vm.xxx -> vm.$data.xxx
this.proxyData(this.$data);
// 2) 再把 data 变成响应式对象
new Observer(this.$data);
// 3) 最后编译模板,建立数据和 DOM 的绑定
new Compiler(this);
}
proxyData(data) {
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
// 访问 vm.xxx 时,转发到 vm.$data.xxx
get: () => data[key],
set: (newValue) => {
// 简单防抖:值变化才写入
if (data[key] !== newValue) {
data[key] = newValue;
}
}
});
});
}
}
- 读取
vm.message时,本质还是读vm.$data.message。 - 写入
vm.message时,本质还是写vm.$data.message。 - 作用是统一入口:后面 Compiler / Watcher 都按同一套 key 工作。
2. Observer:把普通对象转成响应式对象
每个属性都有独立 dep。getter 负责依赖收集,setter 负责通知更新。
你可以把 defineReactive 理解为给每个字段安装“传感器”:谁读取了、谁修改了,都能被捕获。
class Observer {
constructor(data) {
// 从根对象开始递归响应式化
this.walk(data, "");
}
walk(data, basePath) {
if (!data || typeof data !== "object") return;
Object.keys(data).forEach((key) => {
const path = basePath ? basePath + "." + key : key;
this.defineReactive(data, key, data[key], path);
});
}
defineReactive(obj, key, val, path) {
// 每个字段各自维护一个依赖集合
const dep = new Dep(path);
// 如果字段本身是对象,继续递归处理
this.walk(val, path);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
// 有活跃 watcher 时,把它收集进 dep
if (Dep.target) {
dep.addSub(Dep.target);
}
logger.push("read", "读取 " + path + " -> " + stringifyValue(val));
return val;
},
set: (newValue) => {
if (newValue === val) return;
const oldValue = val;
val = newValue;
// 新值可能是对象,继续递归响应式化
this.walk(newValue, path);
logger.push("write", "写入 " + path + ": " + stringifyValue(oldValue) + " -> " + stringifyValue(newValue));
// 通知依赖该字段的 watcher 执行更新
dep.notify();
}
});
}
}
- 递归处理嵌套对象,所以
person.name这类路径也能响应。 - getter 里只做一件事:如果当前有 Watcher,就把它收集进 dep。
- setter 里做两件事:更新值 + 调用
dep.notify()。
3. Dep / Watcher:依赖收集 + 精准更新
创建 Watcher 时将其挂到 Dep.target,读取数据触发 getter 并完成订阅。
Dep 是“发布中心”,Watcher 是“订阅者”。哪个模板片段用到某个字段,就会有对应 Watcher 订阅它。
class Dep {
constructor(path) {
this.path = path;
// Set 去重,避免重复订阅
this.subs = new Set();
}
addSub(watcher) {
if (!watcher || typeof watcher.update !== "function") return;
if (!this.subs.has(watcher)) {
this.subs.add(watcher);
logger.push("collect", watcher.label + " 订阅 -> " + this.path);
}
}
notify() {
logger.push("notify", this.path + " 触发更新,订阅数量: " + this.subs.size);
this.subs.forEach((watcher) => watcher.update());
}
}
Dep.target = null;
class Watcher {
constructor(vm, key, cb, source) {
this.vm = vm;
this.key = key;
this.cb = cb;
this.source = source || "unknown";
this.id = ++Watcher.uid;
this.label = "Watcher#" + this.id + "(" + this.key + ")";
// 依赖收集窗口开始:告诉 getter 当前是谁在读取
Dep.target = this;
// 读取一次 key,触发 getter,让 dep 记住我
this.oldValue = this.getValue(vm, key);
// 依赖收集结束
Dep.target = null;
logger.push("watcher", this.label + " 初始化,值: " + stringifyValue(this.oldValue));
}
update() {
const newValue = this.getValue(this.vm, this.key);
// 仅在值变化时执行回调,减少无效更新
if (newValue !== this.oldValue) {
const oldValue = this.oldValue;
this.oldValue = newValue;
logger.push("update", this.label + " 更新: " + stringifyValue(oldValue) + " -> " + stringifyValue(newValue));
this.cb(newValue);
}
}
getValue(vm, key) {
return key.split(".").reduce((data, currentKey) => {
if (data == null) return undefined;
return data[currentKey.trim()];
}, vm.$data);
}
}
Watcher.uid = 0;
- 关键点:
Dep.target让 getter 知道“当前是谁在读我”。 - 数据变化后,dep 只通知订阅了该字段的 Watcher,不会全量重渲染。
- Watcher 比较新旧值,不变就不触发回调,避免无效更新。
4. Compiler:处理插值和指令
文本节点匹配 {{ key }},元素节点匹配 v-model,两者都创建 Watcher。
Compiler 的目标是把“模板语法”变成“可执行绑定”。编译完成后,数据和 DOM 就建立了双向联系。
class Compiler {
constructor(vm) {
this.vm = vm;
this.el = vm.$el;
this.compile(this.el);
}
compile(el) {
// 扫描当前层所有子节点,分别处理文本和元素
const childNodes = Array.from(el.childNodes);
childNodes.forEach((node) => {
if (this.isTextNode(node)) {
this.compileText(node);
} else if (this.isElementNode(node)) {
this.compileElement(node);
}
if (node.childNodes && node.childNodes.length) {
// 递归处理下一层
this.compile(node);
}
});
}
compileElement(node) {
Array.from(node.attributes).forEach((attr) => {
const attrName = attr.name;
if (!this.isDirective(attrName)) return;
// v-model="message" -> directiveName=model, key=message
const directiveName = attrName.slice(2);
const key = attr.value.trim();
this.update(node, key, directiveName);
});
}
modelUpdater(node, vm, key) {
const updateFn = (value) => {
node.value = value == null ? "" : value;
};
// 数据 -> 视图:数据变化时自动更新 input.value
new Watcher(vm, key, updateFn, "v-model");
updateFn(this.getValue(vm, key));
node.addEventListener("input", () => {
// 视图 -> 数据:用户输入回写到 data
logger.push("input", "input 事件回写 " + key + " = " + stringifyValue(node.value));
this.setValue(vm, key, node.value);
});
}
compileText(node) {
const text = node.textContent;
if (!/\{\{([^}]+)\}\}/.test(text)) return;
node.originalTextContent = text;
const reg = /\{\{([^}]+)\}\}/g;
const keys = [];
text.replace(reg, (_, key) => {
const cleaned = key.trim();
if (!keys.includes(cleaned)) keys.push(cleaned);
return _;
});
keys.forEach((key) => {
// 每个插值 key 创建一个 watcher,任一变化就重算文本
new Watcher(this.vm, key, () => this.updateTextNode(node), "text");
});
this.updateTextNode(node);
}
}
{{ message }}属于“数据 -> 视图”:数据变了自动改文本。v-model额外加了“视图 -> 数据”:用户输入时回写 data。- 所以
v-model本质是两个单向绑定的组合。