实现 Vue 2 的简易双向数据绑定
Vue.js 是一个流行的前端框架,它以其简单易用的双向数据绑定而闻名。在下面的文章中,我们将探索 Vue 2 如何通过其响应式系统实现双向数据绑定,并尝试手动实现一个简化版本。
核心概念
Vue 2 的双向数据绑定基于几个关键概念:响应式系统、依赖收集和派发更新。它利用 JavaScript 的 Object.defineProperty
方法来跟踪数据的变化。
响应式系统的初始化
当一个 Vue 实例被创建时,它通过一个名为 observe
的函数递归地遍历所有的数据对象,并将这些对象的每个属性转换成 getter/setter。这是通过 defineReactive
函数实现的。
依赖收集和派发更新
每个组件实例都有一个相应的 Watcher
实例,它会在组件渲染过程中记录所有依赖(即数据属性)。当一个数据属性被修改时,它的 setter 会被触发,进而通知相关的 Watcher
更新视图。
实现步骤
以下是实现 Vue 2 双向绑定的简化步骤:
1. 实现 observe
函数
这个函数负责遍历并包装对象的每个属性。
function observe(obj) {
// 检查obj是否是对象,如果不是对象或者为null,则不需要做响应式处理
if (!obj || typeof obj !== 'object') return;
// 遍历对象的每个属性,对每个属性进行响应式处理
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
2. 定义 defineReactive
函数
这个函数使用 Object.defineProperty
将普通属性转换为响应式属性。
function defineReactive(obj, key, val) {
// 如果val本身还是对象,则需要递归处理,确保对象内的属性也是响应式的
observe(val);
// 创建一个依赖管理器实例,用于收集和派发当前属性的依赖
let dp = new Dep();
// 通过Object.defineProperty将属性转换为getter/setter
Object.defineProperty(obj, key, {
enumerable: true, // 属性可枚举
configurable: true, // 属性可配置
get: function reactiveGetter() {
// 收集依赖,当有Watcher读取该属性时,将Watcher添加到依赖列表中
if (Dep.target) dp.addSub(Dep.target);
return val;
},
set: function reactiveSetter(newVal) {
// 当属性值发生变化时,更新属性的值
val = newVal;
// 并通知所有依赖进行更新
dp.notify();
}
});
}
3. 创建 Dep
类
Dep
类是一个依赖管理器,它收集和派发依赖。
class Dep {
constructor() {
// 初始化依赖数组,用于存储所有依赖该属性的Watcher
this.subs = [];
}
// 添加一个新的依赖(Watcher)
addSub(sub) {
this.subs.push(sub);
}
// 当属性变化时,通知所有依赖执行更新操作
notify() {
this.subs.forEach(sub => sub.update());
}
}
// 全局属性,用于暂存当前正在计算的Watcher
Dep.target = null;
4. 定义 Watcher
类
Watcher
类为每个组件或指令创建一个观察者实例。
class Watcher {
constructor(obj, key, cb) {
// 将Dep.target指向自己,用于依赖收集
Dep.target = this;
this.cb = cb; // 回调函数,用于更新视图
this.obj = obj; // 监听的目标对象
this.key = key; // 监听的对象属性
this.value = obj[key]; // 触发属性的getter,进行依赖收集
Dep.target = null; // 收集完依赖后,将Dep.target重置
}
// 当属性变化时,调用回调函数更新视图
update() {
this.value = this.obj[this.key];
this.cb(this.value);
}
}
5. 测试案例
创建一个数据对象,并观察它的变化。
var data = { name: 'yck' };
// 对数据对象data进行响应式处理
observe(data);
// 更新DOM的函数
function update(value) {
document.querySelector('div').innerText = value;
}
// 创建一个Watcher实例,模拟对data.name的依赖收集和视图更新
new Watcher(data, 'name', update);
// 修改data.name的值,触发响应式更新
data.name = 'yyy';
完整代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue 2 Data Binding Example</title>
</head>
<body>
<div>{{name}}</div>
<script>
// observe 函数:使一个对象变成响应式
function observe(obj) {
if (!obj || typeof obj !== 'object') return;
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
// defineReactive 函数:定义一个响应式的属性
function defineReactive(obj, key, val) {
observe(val); // 递归处理子属性
const dep = new Dep(); // 为每个属性创建依赖管理器实例
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
if (Dep.target) {
dep.addSub(Dep.target); // 收集依赖
}
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify(); // 数据变化,通知所有依赖更新
}
});
}
// Dep 类:依赖管理器,管理某个属性的所有Watcher
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
Dep.target = null;
// Watcher 类:观察者,观察属性变化并执行回调
class Watcher {
constructor(obj, key, cb) {
Dep.target = this;
this.cb = cb;
this.obj = obj;
this.key = key;
this.value = obj[key]; // 触发getter,进行依赖收集
Dep.target = null;
}
update() {
this.value = this.obj[this.key];
this.cb(this.value); // 执行回调更新视图
}
}
// 测试代码
var data = { name: 'Vue' };
observe(data);
// 模拟解析到 `{{name}}`,创建一个Watcher来更新视图
new Watcher(data, 'name', function (value) {
document.querySelector('div').innerText = value;
});
// 修改data.name的值,触发响应式更新
setTimeout(() => {
data.name = 'Vue 2';
}, 2000);
</script>
</body>
</html>
结论
以上代码提供了一个简单的 Vue 2 双向绑定机制的实现。尽管实际的 Vue 源码要复杂得多,但这个简化版本揭示了 Vue 数据响应系统的核心原理:通过 Object.defineProperty
实现的数据监听、依赖收集、以及基于这些依赖的视图更新。
通过理解这些基础概念,我们可以更深入地理解 Vue 的工作原理,并在需要时对其进行定制和优化。