一、核心实现原理
1. 数据劫持(Data Observation)
Vue 2 通过 Object.defineProperty
对对象属性进行劫持,将其转换为 getter/setter
,从而在数据被访问或修改时触发依赖收集和更新通知。
function defineReactive(obj, key, val) {
// 递归劫持嵌套对象
observe(val);
const dep = new Dep(); // 每个属性对应一个依赖管理器
Object.defineProperty(obj, key, {
get() {
if (Dep.target) { // 当前正在计算的 Watcher(依赖)
dep.depend(); // 将 Watcher 添加到依赖列表
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify(); // 通知所有依赖更新
}
});
}
// 遍历对象所有属性进行劫持
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
new Observer(obj);
}
二、依赖收集与派发更新
1. 依赖收集(Dep 类)
- Dep (Dependency):每个响应式属性对应一个
Dep
实例,用于管理所有依赖它的Watcher
。class Dep { constructor() { this.subs = []; // 存储 Watcher 列表 } depend() { if (Dep.target) { this.subs.push(Dep.target); // 添加当前 Watcher } } notify() { this.subs.forEach(watcher => watcher.update()); // 触发更新 } }
2. 观察者(Watcher 类)
- Watcher:代表一个具体的依赖(如组件渲染函数、计算属性),当数据变化时触发回调更新视图。
class Watcher { constructor(vm, key, cb) { Dep.target = this; // 标记当前 Watcher this.cb = cb; vm._data[key]; // 触发 getter 收集依赖 Dep.target = null; // 重置 } update() { this.cb(); // 执行更新回调 } }
三、双向绑定实现(v-model)
v-model
是语法糖,本质是 value
属性绑定 + input
事件监听的组合:
<!-- 模板 -->
<input v-model="message">
<!-- 等价于 -->
<input
:value="message"
@input="message = $event.target.value"
>
实现步骤:
- 初始化阶段:通过
Object.defineProperty
劫持message
的getter/setter
。 - 模板编译:解析
v-model
,为input
元素添加value
属性和input
事件监听。 - 输入事件触发:用户输入时,通过
input
事件修改message
的值。 - 响应式更新:
message
的setter
触发,通知所有依赖的Watcher
更新视图。
四、数组的响应式处理
Vue 2 无法直接监听数组索引操作,通过 重写数组方法 实现响应式:
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
// 重写变异方法(push/pop/shift/unshift/splice/sort/reverse)
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
const original = arrayProto[method];
def(arrayMethods, method, function(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
ob.dep.notify(); // 手动触发更新
return result;
});
});
五、Vue 2 双向绑定的局限性
对象属性新增/删除无法检测
需通过Vue.set(obj, 'newKey', value)
或Vue.delete(obj, 'key')
触发更新。数组索引和长度修改无法监听
直接通过索引修改元素(如arr[0] = 1
)或修改length
属性不会触发响应式。性能问题
初始化时递归遍历所有属性,大型对象性能较差。
六、手动触发更新的场景
// 对象新增属性(非响应式)
this.user.profile = { age: 25 }; // ❌ 不触发更新
Vue.set(this.user, 'profile', { age: 25 }); // ✅ 触发更新
// 数组索引修改(非响应式)
this.items[0] = 'new'; // ❌ 不触发更新
Vue.set(this.items, 0, 'new'); // ✅ 触发更新
Vue 2 双向绑定流程
- 数据初始化:递归遍历对象,通过
Object.defineProperty
劫持属性。 - 模板编译:解析模板中的
v-model
指令,绑定value
属性和input
事件。 - 依赖收集:渲染过程中触发
getter
,将Watcher
注册到Dep
。 - 数据更新:用户输入触发
input
事件修改数据,setter
通知Dep
派发更新。 - 视图更新:
Watcher
执行回调,触发组件重新渲染。