实现 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/setterObject.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() {// 初始化依赖数组,用于存储所有依赖该属性的Watcherthis.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 类:依赖管理器,管理某个属性的所有Watcherclass 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 的工作原理,并在需要时对其进行定制和优化。