Vue 3 的双向绑定是其响应式系统的核心,它通过 Proxy API 实现数据劫持,结合依赖收集 与触发更新 机制,自动同步数据与视图。下面我将手把手带你实现一个简化版 Vue 3 双向绑定,涵盖响应式数据、依赖追踪和模板指令解析(如 v-model
)。整个过程基于以下关键架构:
组件 | 职责 | 核心实现 |
---|---|---|
响应式系统 | 数据劫持与依赖管理 | 使用 Proxy 拦截对象操作,通过 track 和 trigger 管理依赖 |
副作用处理 | 关联数据变化与视图更新 | effect 函数注册副作用,在数据变化时重新执行 |
模板编译 | 解析指令并绑定事件 | 递归遍历 DOM,处理 v-model 等指令,建立数据与 DOM 的联系 |
🔧 核心代码实现
我们将实现一个迷你版 Vue 类(如 MyVue
),包含响应式处理、依赖收集和模板编译。完整代码如下:
xml
<!-- index.html -->
<div id="app">
<input v-model="message" />
<p>{{ message }}</p>
</div>
<script type="module">
import { MyVue } from './my-vue.js';
const app = new MyVue({
el: '#app',
data: {
message: 'Hello Vue 3!'
}
});
</script>
ini
// my-vue.js
// 1. 依赖管理:存储全局依赖关系
const targetMap = new WeakMap(); // 弱引用,避免内存泄漏
let activeEffect = null; // 当前活跃的副作用函数
// 2. 依赖收集(track)与触发更新(trigger)
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect); // 将当前副作用函数添加到依赖集合
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect()); // 执行所有关联的副作用函数
}
}
// 3. 响应式数据创建(核心:Proxy)
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key); // 读取属性时收集依赖
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 属性值变化时触发更新
}
return result;
}
});
}
// 4. 副作用函数(effect)
function effect(fn) {
activeEffect = fn;
fn(); // 执行函数,触发 getter,从而收集依赖
activeEffect = null;
}
// 5. 迷你 Vue 类(整合响应式与模板编译)
export class MyVue {
constructor(options) {
this.$el = document.querySelector(options.el);
this.$data = reactive(options.data); // 数据响应化
this.compile(this.$el); // 编译模板
}
// 模板编译方法
compile(node) {
if (node.nodeType === Node.TEXT_NODE) {
// 处理文本插值 {{ }}
const text = node.textContent;
const regex = /{{\s*(\w+)\s*}}/;
if (regex.test(text)) {
const key = RegExp.$1.trim();
effect(() => {
node.textContent = this.$data[key]; // 数据变化时更新文本
});
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
// 处理元素节点和指令
const attributes = node.attributes;
for (let attr of attributes) {
if (attr.name === 'v-model') {
const key = attr.value;
effect(() => {
node.value = this.$data[key]; // 数据 -> 视图
});
node.addEventListener('input', (e) => {
this.$data[key] = e.target.value; // 视图 -> 数据
});
}
}
// 递归处理子节点
if (node.childNodes.length) {
node.childNodes.forEach(child => this.compile(child));
}
}
}
}
⚙️ 关键原理解析
-
响应式数据(
reactive
函数):- 使用
Proxy
代理目标对象,拦截get
和set
操作。 -
get
拦截器 :当读取属性时,调用track
函数,将当前活跃的副作用函数(activeEffect
)收集到该属性的依赖集合中(通过targetMap
结构管理)。 -
set
拦截器 :当修改属性时,调用trigger
函数,通知所有依赖该属性的副作用函数重新执行,从而更新视图。
- 使用
-
依赖管理(
track
与trigger
):targetMap
是一个WeakMap
,键是响应式对象,值是一个Map
(记录对象属性与依赖集合的映射)。- 依赖集合使用
Set
存储副作用函数,确保唯一性。 - 这种结构允许精确知道哪个对象的哪个属性被哪些副作用函数依赖。
-
副作用函数(
effect
):effect
接收一个函数(如渲染函数或更新函数),执行时设置activeEffect
为该函数。- 当函数内部访问响应式数据时,触发
get
拦截器,完成依赖收集。 - 数据变化时,通过
trigger
重新执行所有依赖函数,实现自动更新。
-
模板编译(
compile
方法):-
递归遍历 DOM 树,处理文本节点(
{{ }}
插值)和元素节点(如v-model
指令)。 -
对于
v-model
:- 为
input
元素绑定input
事件,在用户输入时更新数据(视图 → 数据)。 - 使用
effect
函数建立响应式关联,数据变化时自动更新input
的值(数据 → 视图)。
- 为
-
💡 进阶优化与注意事项
- 数组与嵌套对象 :上述简化版未处理数组和深层对象。Vue 3 的
reactive
会递归代理嵌套对象,并通过重写数组方法(如push
)确保响应式。 - 性能优化 :真实 Vue 3 使用异步更新队列(如
nextTick
)批量处理多次数据变化,避免重复渲染。 - Ref 与 Reactive 区别 :
reactive
适用于对象,而基本类型需使用ref
(通过.value
访问)。 - 解构响应式对象 :直接解构会失去响应性,需使用
toRefs
转换。
💎 总结
通过以上代码,我们实现了一个 Vue 3 双向绑定的核心骨架:Proxy 数据劫持 + 依赖收集/触发 + 模板指令解析 。虽然简化版未覆盖全部边界情况(如组件、计算属性),但它清晰揭示了响应式系统的本质:在数据读取时收集依赖,在数据修改时通知依赖更新 。这种设计使 Vue 3 在性能与功能上显著优于 Vue 2(基于 Object.defineProperty
)。 希望这个手写实现帮助你深入理解 Vue 3 的响应式魔法!如需进一步探讨虚拟 DOM 或 Diff 算法,我们可以继续展开。