Vue 3 带来了许多令人兴奋的新特性和改进,其中之一就是其双向数据绑定的实现方式。与 Vue 2 使用 Object.defineProperty
不同,Vue 3 利用了 JavaScript 的 Proxy
特性来创建响应式数据。在这篇博客中,我们将探讨 Vue 3 中双向数据绑定的基础原理,并尝试手动实现一个简化版的这一机制。
Vue 3 的双向绑定依赖于两个核心概念:响应式代理(Reactive Proxy) 和 Effect 依赖收集系统。
Vue 3 使用 Proxy
对象来创建响应式数据。这允许框架在不改变对象本身结构的情况下,拦截并跟踪属性的访问和修改。
Effect
依赖收集系统用于自动追踪响应式数据的使用情况,并在数据变化时重新执行副作用(如渲染函数)。
以下是手动实现 Vue 3 双向绑定的简化步骤:
reactive
函数这个函数用于将普通对象转换为响应式代理。
function reactive(target) {
// 使用 Proxy 对象创建响应式数据
return new Proxy(target, {
// 拦截对象属性的读取
get(target, key, receiver) {
console.log(`访问了属性:${key}`);
// 使用 Reflect.get 保证 this 指向正确
return Reflect.get(target, key, receiver);
},
// 拦截对象属性的设置
set(target, key, value, receiver) {
console.log(`设置了属性:${key},新值为:${value}`);
// 使用 Reflect.set 设置属性值
Reflect.set(target, key, value, receiver);
// 此处省略了依赖通知逻辑,实际 Vue 3 中会触发更新
return true;
}
});
}
effect
函数effect
函数用于注册副作用函数,并在响应式数据变化时执行这些函数。
let activeEffect = null;
function effect(fn) {
// 设置当前活动的副作用函数
activeEffect = fn;
// 立即执行函数,用于依赖收集
fn();
// 重置当前活动的副作用函数
activeEffect = null;
}
reactive
以收集依赖现在我们需要修改 reactive
函数,以支持依赖收集和派发更新。
const targetMap = new WeakMap();
function track(target, key) {
// 如果没有活动的副作用函数,直接返回
if (!activeEffect) return;
// 从 targetMap 中获取当前对象的所有依赖
let depsMap = targetMap.get(target);
if (!depsMap) {
// 如果没有,就创建新的 Map 并存入 targetMap
targetMap.set(target, (depsMap = new Map()));
}
// 获取当前属性的所有依赖
let dep = depsMap.get(key);
if (!dep) {
// 如果没有,就创建新的 Set 并存入 depsMap
depsMap.set(key, (dep = new Set()));
}
// 将当前活动的副作用函数添加到依赖集合中
dep.add(activeEffect);
}
function trigger(target, key) {
// 从 targetMap 中获取对象的所有依赖
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 获取当前属性的所有依赖
let dep = depsMap.get(key);
if (dep) {
// 对于每一个依赖(即副作用函数),执行它
dep.forEach(effect => effect());
}
}
// 更新 reactive 函数
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) {
// 设置属性值
Reflect.set(target, key, value, receiver);
// 触发更新
trigger(target, key);
return true;
}
});
}
创建一个响应式对象,并观察它的变化。
const state = reactive({ count: 0 });
// 注册一个副作用函数来模拟渲染逻辑
effect(() => {
console.log(`count is now: ${state.count}`);
});
// 更改响应式数据的属性,触发副作用函数
state.count++;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue 3 简易双向数据绑定测试</title>
</head>
<body>
<h1>Vue 3 简易双向数据绑定测试</h1>
<div id="app">
<p>Count: <span id="count">0</span></p>
<button id="increment">Increment</button>
</div>
<script>
// reactive 函数
function reactive(target) {
const handler = {
get(target, key, receiver) {
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver);
trigger(target, key);
return true;
}
};
return new Proxy(target, handler);
}
let activeEffect = null;
function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
const targetMap = new WeakMap();
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
let dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect());
}
}
// 测试代码
const state = reactive({ count: 0 });
effect(() => {
document.getElementById('count').innerText = state.count;
});
document.getElementById('increment').addEventListener('click', () => {
state.count++;
});
</script>
</body>
</html>
在这个文件中,我们创建了一个按钮,每当它被点击时,就会增加 state.count
的值。这个值的变化会触发绑定的副作用函数,从而更新显示在页面上的计数结论
以上代码展示了一个 Vue 3 中双向数据绑定的简化实现。通过 Proxy
和动态依赖收集系统,我们可以创建一个灵活且强大的响应式系统。这种实现方式使得 Vue 3 在处理复杂应用时更为高效和灵活。