1524 字
8 分钟
Vue2 响应式系统

题目描述#

实现 Vue2 的核心响应式系统,要求:

  1. 使用 Object.defineProperty 实现数据劫持
  2. 实现依赖收集(Dep)和依赖触发(Watcher)机制
  3. 支持嵌套对象的深层响应式处理
  4. 实现数组方法的劫持(push/pop/shift/unshift/splice/sort/reverse)
  5. 处理属性新增和删除的响应式更新
  6. 实现计算属性和侦听器基础功能

要求完整实现响应式逻辑,并通过测试用例验证。

解题思路#

  1. 数据劫持:递归使用 Object.defineProperty 劫持对象属性
  2. 依赖收集:在 getter 中收集依赖(Dep -> Watcher)
  3. 依赖触发:在 setter 中通知变更(Watcher.update)
  4. 数组处理:拦截数组变异方法并手动触发更新
  5. 嵌套对象:递归处理对象属性实现深层响应
  6. 计算属性:基于 Watcher 实现惰性求值

关键洞察#

  1. 响应式核心:通过 getter/setter 实现数据变化侦测
  2. 依赖追踪:Dep 收集 Watcher,Watcher 订阅 Dep
  3. 数组劫持:重写数组原型方法触发更新
  4. 深层监听:递归处理嵌套对象属性
  5. 惰性计算:计算属性缓存机制避免重复计算
  6. 批量更新:异步更新队列避免重复渲染

代码流程#

flowchart TD 
	A[Observer 监听数据] --> B{数据类型} 
	B -->|对象| C[递归 defineProperty] 
	B -->|数组| D[重写数组方法] 
	C --> E[getter 收集依赖] 
	E --> F[Dep 添加 Watcher] 
	D --> G[手动触发更新] 
	H[数据变更] --> I[setter 通知 Dep] 
	I --> J[Watcher 更新] 
	J --> K[执行视图更新]

代码实现#

// 依赖收集器
class Dep {
constructor() {
this.subs = new Set();
}
addSub(sub) {
this.subs.add(sub);
}
removeSub(sub) {
this.subs.delete(sub);
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
const subs = [...this.subs];
subs.forEach(sub => sub.update());
}
}
Dep.target = null;
const targetStack = [];
function pushTarget(target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
// 观察者
class Watcher {
constructor(vm, expOrFn, cb, options) {
this.vm = vm;
this.cb = cb;
this.deps = new Set();
this.depIds = new Set();
this.getter = typeof expOrFn === 'function'
? expOrFn
: () => vm[expOrFn];
if (options) {
this.lazy = !!options.lazy;
}
this.value = this.lazy ? undefined : this.get();
}
get() {
pushTarget(this);
const value = this.getter.call(this.vm);
popTarget();
return value;
}
addDep(dep) {
if (!this.depIds.has(dep.id)) {
this.depIds.add(dep.id);
this.deps.add(dep);
dep.addSub(this);
}
}
update() {
if (this.lazy) {
this.dirty = true;
} else {
queueWatcher(this);
}
}
run() {
const value = this.get();
const oldValue = this.value;
this.value = value;
this.cb.call(this.vm, value, oldValue);
}
}
// 异步更新队列
let queue = [];
let flushing = false;
let waiting = false;
function queueWatcher(watcher) {
if (!queue.includes(watcher)) {
queue.push(watcher);
}
if (!waiting) {
waiting = true;
nextTick(flushQueue);
}
}
function flushQueue() {
flushing = true;
queue.forEach(w => w.run());
queue = [];
flushing = false;
waiting = false;
}
// 将数组方法转换为响应式
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
const original = arrayProto[method];
arrayMethods[method] = function(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
// 处理新增元素
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
ob.dep.notify();
return result;
};
});
// 观察者类
class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
def(value, '__ob__', this);
if (Array.isArray(value)) {
value.__proto__ = arrayMethods;
this.observeArray(value);
} else {
this.walk(value);
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
observeArray(items) {
items.forEach(item => observe(item));
}
}
// 响应式绑定
function defineReactive(obj, key, val) {
const dep = new Dep();
let childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
// 数组依赖收集
if (Array.isArray(val)) {
dependArray(val);
}
}
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
childOb = observe(newVal);
dep.notify();
}
});
}
function dependArray(value) {
for (let i = 0, l = value.length; i < l; i++) {
const e = value[i];
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
dependArray(e);
}
}
}
function observe(value) {
if (!value || typeof value !== 'object') return;
return new Observer(value);
}
// Vue 实例简化版
class Vue {
constructor(options) {
this.$options = options;
this._data = options.data;
// 数据响应化
observe(this._data);
// 代理data到实例
Object.keys(this._data).forEach(key => {
proxy(this, '_data', key);
});
}
$watch(expOrFn, cb) {
return new Watcher(this, expOrFn, cb);
}
}
// 数据代理
function proxy(target, sourceKey, key) {
Object.defineProperty(target, key, {
get() {
return target[sourceKey][key];
},
set(newVal) {
target[sourceKey][key] = newVal;
}
});
}
// 计算属性实现
function computed(getter) {
const watcher = new Watcher(this, getter, null, { lazy: true });
Object.defineProperty(this, key, {
get() {
if (watcher.dirty) {
watcher.evaluate();
}
return watcher.value;
}
});
}
// 下一个tick实现
function nextTick(cb) {
const timerFn = () => {
cb();
};
if (typeof Promise !== 'undefined') {
Promise.resolve().then(timerFn);
} else if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(timerFn);
const textNode = document.createTextNode('1');
observer.observe(textNode, { characterData: true });
textNode.data = '2';
} else {
setTimeout(timerFn, 0);
}
}

使用示例#

// 创建Vue实例
const vm = new Vue({
data: {
count: 0,
items: [1, 2, 3],
person: {
name: 'Alice',
age: 30
}
}
});
// 监听数据变化
vm.$watch('count', (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`);
});
// 监听数组变化
vm.$watch(() => vm.items.length, (newLen, oldLen) => {
console.log(`Array length changed from ${oldLen} to ${newLen}`);
});
// 测试响应式
vm.count++; // 输出: Count changed from 0 to 1
// 测试数组响应式
vm.items.push(4); // 输出: Array length changed from 3 to 4
// 测试嵌套对象
vm.person.age = 31; // 触发更新
// 测试新增属性(需要特殊处理)
// Vue2 需要使用 Vue.set 或 this.$set
Vue.set(vm.person, 'job', 'Engineer');
// 测试计算属性
vm.computed = computed(() => vm.count * 2);
console.log(vm.computed); // 2 (vm.count=1)
vm.count++;
console.log(vm.computed); // 4

业务场景#

  1. 数据绑定:表单输入与状态实时同步
  2. 复杂状态管理:Vue组件内部状态响应式更新
  3. 列表渲染:数组变化时自动更新DOM列表
  4. 计算属性:基于其他状态自动计算派生值
  5. 侦听器:监听数据变化执行特定逻辑
  6. 状态依赖:组件间状态自动同步更新

设计挑战

  • 对象属性新增/删除需要通过 Vue.set/Vue.delete 处理
  • 数组变化检测需要重写原型方法
  • 深层嵌套对象需要递归处理
  • 异步更新队列防止重复渲染
  • 计算属性的缓存优化机制

相似题目#

  1. Vue3 响应式实现(困难) 核心:Proxy 代替 Object.defineProperty
  2. React Hooks 实现(困难) 核心:闭包 + 链表管理状态
  3. 实现观察者模式(中等) 核心:Subject/Observer 接口实现
  4. 实现 Promise A+规范(困难) 核心:状态机 + 异步回调队列
  5. 实现前端路由系统(中等) 核心:History API + 组件切换

设计局限与解决方案

flowchart LR 
	A[Object.defineProperty局限] --> B[无法检测对象属性添加/删除] 
	A --> C[无法检测数组索引变更] 
	A --> D[性能消耗较大] 
	B --> E[Vue.set/Vue.delete API] 
	C --> F[数组方法重写] 
	D --> G[Vue3改用Proxy]
Vue2 响应式系统
https://website-truelovings-projects.vercel.app/posts/code/vue/vue2-响应式系统/
作者
欢迎来到StarSky的网站!
发布于
2025-08-23
许可协议
CC BY-NC-SA 4.0