WSの小屋

前言

在上一篇文章中,我们深入分析了 Vue 3 响应式系统的设计原理。理论虽好,但要真正理解,没有什么比亲手实现一遍更有效了。本文将用约 100 行代码,从零实现一个 mini 响应式系统,涵盖 reactiverefeffectcomputedwatch 五大核心 API。


一、目标设计

我们将实现以下 API:

const state = reactive({ count: 0, name: 'Vue' });
const num = ref(10);

effect(() => {
  console.log('count is:', state.count);
});

const double = computed(() => state.count * 2);

watch(() => state.name, (newVal, oldVal) => {
  console.log(`name changed: ${oldVal} -> ${newVal}`);
});

二、核心数据结构

2.1 依赖关系图

targetMap (WeakMap)
├── target1 (Map)
│   ├── key1 -> dep (Set<ReactiveEffect>)
│   └── key2 -> dep (Set<ReactiveEffect>)
└── target2 (Map)
    └── key1 -> dep (Set<ReactiveEffect>)
  • targetMap:用 WeakMap 以对象为 key,避免内存泄漏
  • 每个对象的每个属性对应一个 dep(Set),存储所有依赖此属性的 effect

2.2 ReactiveEffect 类

class ReactiveEffect {
  constructor(fn, scheduler) {
    this.fn = fn;
    this.scheduler = scheduler;
    this.deps = [];   // 存储 effect 被收集到的所有 dep,用于清理
    this.active = true;
  }

  run() {
    if (!this.active) {
      return this.fn();
    }
    // 压栈
    shouldTrack = true;
    activeEffect = this;
    cleanupEffect(this);
    try {
      return this.fn();
    } finally {
      // 出栈
      activeEffect = this.parent;
      shouldTrack = false;
    }
  }

  stop() {
    if (this.active) {
      cleanupEffect(this);
      this.active = false;
    }
  }
}

三、从零实现:一步步搭建

3.1 全局状态与依赖收集

let activeEffect = null;
let shouldTrack = true;

const targetMap = new WeakMap();

/**
 * 依赖收集
 */
function track(target, key) {
  if (!shouldTrack || activeEffect === null) return;

  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }

  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }

  trackEffects(dep);
}

/**
 * 将当前 effect 加入 dep,并反向记录
 */
function trackEffects(dep) {
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}

/**
 * 派发更新
 */
function trigger(target, type, key, newValue) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  // 收集需要执行的所有 effect
  const effects = new Set();

  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(e => effects.add(e));
  }

  // 数组 length 修改时,触发所有索引 >= newValue 的 effect
  if (type === 'add' && Array.isArray(target)) {
    const lengthDep = depsMap.get('length');
    if (lengthDep) {
      lengthDep.forEach(e => effects.add(e));
    }
  }

  effects.forEach(effect => {
    if (effect !== activeEffect) { // 防止自我触发死循环
      if (effect.scheduler) {
        effect.scheduler();
      } else {
        effect.run();
      }
    }
  });
}

3.2 cleanupEffect:依赖清理

function cleanupEffect(effect) {
  effect.deps.forEach(dep => {
    dep.delete(effect);
  });
  effect.deps.length = 0;
}

每次 effect 执行前清理旧依赖,执行时重新收集。这解决分支切换问题:

const state = reactive({ flag: true, a: 1, b: 2 });

effect(() => {
  console.log(state.flag ? state.a : state.b);
});

state.flag = false;
// 重新收集依赖:现在只依赖 flag 和 b,不再依赖 a
state.a = 99; // 不会触发 effect

3.3 reactive:Proxy 代理对象

const reactiveMap = new WeakMap();
const RAW = Symbol('raw');

const baseHandlers = {
  get(target, key, receiver) {
    if (key === RAW) return target;

    const result = Reflect.get(target, key, receiver);

    // 依赖收集
    track(target, key);

    // 惰性代理嵌套对象
    if (result !== null && typeof result === 'object') {
      return reactive(result);
    }

    return result;
  },

  set(target, key, value, receiver) {
    const oldValue = target[key];
    const hadKey = Array.isArray(target)
      ? Number(key) < target.length
      : Object.prototype.hasOwnProperty.call(target, key);

    // 如果赋的值是响应式对象,取原始值
    if (value && value[RAW]) {
      value = value[RAW];
    }

    const result = Reflect.set(target, key, value, receiver);

    // 确保是原始对象的操作(避免原型链触发)
    if (target === receiver[RAW]) {
      if (!hadKey) {
        trigger(target, 'add', key, value);
      } else if (value !== oldValue && (oldValue === oldValue || value === value)) {
        trigger(target, 'set', key, value);
      }
    }

    return result;
  },

  deleteProperty(target, key) {
    const hadKey = Object.prototype.hasOwnProperty.call(target, key);
    const result = Reflect.deleteProperty(target, key);
    if (hadKey && result) {
      trigger(target, 'delete', key);
    }
    return result;
  },

  has(target, key) {
    const result = Reflect.has(target, key);
    track(target, key);
    return result;
  },

  ownKeys(target) {
    track(target, Array.isArray(target) ? 'length' : Symbol('iterate'));
    return Reflect.ownKeys(target);
  }
};

function reactive(target) {
  if (typeof target !== 'object' || target === null) return target;

  // 如果已经是代理,直接返回
  if (target[RAW]) return target;

  // 缓存检查
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target);
  }

  const proxy = new Proxy(target, baseHandlers);
  reactiveMap.set(target, proxy);
  return proxy;
}

3.4 ref:包装基本类型

class RefImpl {
  constructor(value) {
    this._rawValue = value;
    this._value = convert(value);
    this.dep = new Set();
    this.__v_isRef = true;
  }

  get value() {
    trackRefValue(this);
    return this._value;
  }

  set value(newVal) {
    if (newVal !== this._rawValue && (newVal === newVal || this._rawValue === this._rawValue)) {
      this._rawValue = newVal;
      this._value = convert(newVal);
      triggerRefValue(this);
    }
  }
}

function convert(value) {
  return typeof value === 'object' && value !== null ? reactive(value) : value;
}

function trackRefValue(ref) {
  if (shouldTrack && activeEffect) {
    trackEffects(ref.dep);
  }
}

function triggerRefValue(ref) {
  ref.dep.forEach(effect => {
    if (effect !== activeEffect) {
      if (effect.scheduler) {
        effect.scheduler();
      } else {
        effect.run();
      }
    }
  });
}

function ref(value) {
  if (value && value.__v_isRef) return value;
  return new RefImpl(value);
}

3.5 effect:副作用管理

function effect(fn, options = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler);

  if (!options.lazy) {
    _effect.run();
  }

  const runner = _effect.run.bind(_effect);
  runner.effect = _effect;
  return runner;
}

3.6 computed:惰性计算

class ComputedRefImpl {
  constructor(getter, setter) {
    this._setter = setter;
    this._dirty = true;
    this._value = undefined;
    this.dep = new Set();
    this.__v_isRef = true;

    this.effect = new ReactiveEffect(getter, () => {
      // 依赖变化时不立即计算,标记为脏值
      if (!this._dirty) {
        this._dirty = true;
        // 通知依赖 computed 的 effect
        triggerRefValue(this);
      }
    });
  }

  get value() {
    trackRefValue(this);
    if (this._dirty) {
      this._dirty = false;
      this._value = this.effect.run();
    }
    return this._value;
  }

  set value(newVal) {
    if (this._setter) {
      this._setter(newVal);
    }
  }
}

function computed(getterOrOptions) {
  let getter, setter;
  if (typeof getterOrOptions === 'function') {
    getter = getterOrOptions;
    setter = () => {
      console.warn('Write operation failed: computed value is readonly');
    };
  } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }

  return new ComputedRefImpl(getter, setter);
}

3.7 watch:监听变化

function watch(source, cb, options = {}) {
  let getter;
  if (typeof source === 'function') {
    getter = source;
  } else if (source && source.__v_isRef) {
    getter = () => source.value;
  } else if (typeof source === 'object') {
    getter = () => source;
  }

  let oldValue;
  let cleanup;

  const onCleanup = (fn) => {
    cleanup = fn;
  };

  const job = () => {
    if (cleanup) cleanup();
    const newValue = effect.run();
    cb(newValue, oldValue, onCleanup);
    oldValue = newValue;
  };

  const effect = new ReactiveEffect(getter, job);

  if (options.immediate) {
    job();
  } else {
    oldValue = effect.run();
  }
}

四、完整代码整合

将以上所有代码合并,总计约 200 行(含注释):

// ============ 全局状态 ============
let activeEffect = null;
let shouldTrack = false;
const targetMap = new WeakMap();
const reactiveMap = new WeakMap();
const RAW = Symbol('raw');

// ============ 依赖收集与触发 ============
function track(target, key) {
  if (!shouldTrack || !activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) { depsMap = new Map(); targetMap.set(target, depsMap); }
  let dep = depsMap.get(key);
  if (!dep) { dep = new Set(); depsMap.set(key, dep); }
  if (!dep.has(activeEffect)) { dep.add(activeEffect); activeEffect.deps.push(dep); }
}

function trigger(target, type, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  const effects = new Set();
  if (dep) dep.forEach(e => e !== activeEffect && effects.add(e));
  effects.forEach(e => e.scheduler ? e.scheduler() : e.run());
}

// ============ ReactiveEffect ============
class ReactiveEffect {
  constructor(fn, scheduler) {
    this.fn = fn; this.scheduler = scheduler;
    this.deps = []; this.active = true; this.parent = null;
  }
  run() {
    if (!this.active) return this.fn();
    this.parent = activeEffect;
    shouldTrack = true;
    activeEffect = this;
    this.deps.forEach(d => d.delete(this));
    this.deps.length = 0;
    try { return this.fn(); }
    finally { activeEffect = this.parent; shouldTrack = false; this.parent = null; }
  }
  stop() { if (this.active) { this.deps.forEach(d => d.delete(this)); this.active = false; } }
}

// ============ reactive ============
const baseHandlers = {
  get(t, k, r) {
    if (k === RAW) return t;
    const res = Reflect.get(t, k, r);
    track(t, k);
    if (res !== null && typeof res === 'object') return reactive(res);
    return res;
  },
  set(t, k, v, r) {
    const old = t[k];
    const had = Array.isArray(t) ? Number(k) < t.length : Object.prototype.hasOwnProperty.call(t, k);
    if (v && v[RAW]) v = v[RAW];
    const res = Reflect.set(t, k, v, r);
    if (t === r[RAW]) {
      if (!had) trigger(t, 'add', k);
      else if (v !== old && (old === old || v === v)) trigger(t, 'set', k);
    }
    return res;
  },
  deleteProperty(t, k) {
    const had = Object.prototype.hasOwnProperty.call(t, k);
    const res = Reflect.deleteProperty(t, k);
    if (had && res) trigger(t, 'delete', k);
    return res;
  }
};

function reactive(target) {
  if (typeof target !== 'object' || target === null) return target;
  if (target[RAW]) return target;
  if (reactiveMap.has(target)) return reactiveMap.get(target);
  const proxy = new Proxy(target, baseHandlers);
  reactiveMap.set(target, proxy);
  return proxy;
}

// ============ ref ============
class RefImpl {
  constructor(v) { this._raw = v; this._val = typeof v === 'object' && v ? reactive(v) : v; this.dep = new Set(); this.__v_isRef = true; }
  get value() { if (shouldTrack && activeEffect) { this.dep.add(activeEffect); activeEffect.deps.push(this.dep); } return this._val; }
  set value(n) { if (n !== this._raw) { this._raw = n; this._val = typeof n === 'object' && n ? reactive(n) : n; this.dep.forEach(e => e !== activeEffect && (e.scheduler ? e.scheduler() : e.run())); } }
}
function ref(v) { return v && v.__v_isRef ? v : new RefImpl(v); }

// ============ effect ============
function effect(fn, opts = {}) {
  const e = new ReactiveEffect(fn, opts.scheduler);
  if (!opts.lazy) e.run();
  const runner = e.run.bind(e); runner.effect = e; return runner;
}

// ============ computed ============
function computed(getter) {
  let val, dirty = true;
  const dep = new Set();
  const _eff = new ReactiveEffect(getter, () => { if (!dirty) { dirty = true; dep.forEach(e => e !== activeEffect && (e.scheduler ? e.scheduler() : e.run())); } });
  return {
    __v_isRef: true,
    get value() { if (shouldTrack && activeEffect) { dep.add(activeEffect); activeEffect.deps.push(dep); } if (dirty) { dirty = false; val = _eff.run(); } return val; },
    set value(v) { console.warn('computed is readonly'); }
  };
}

// ============ watch ============
function watch(source, cb, opts = {}) {
  const getter = typeof source === 'function' ? source : () => source.value;
  let old, cleanup;
  const onCleanup = fn => { cleanup = fn; };
  const job = () => { if (cleanup) cleanup(); const nv = _eff.run(); cb(nv, old, onCleanup); old = nv; };
  const _eff = new ReactiveEffect(getter, job);
  if (opts.immediate) { job(); } else { old = _eff.run(); }
}

五、测试验证

5.1 基础响应式

const state = reactive({ count: 0 });

effect(() => {
  console.log('count:', state.count);
});
// 输出: count: 0

state.count = 1;
// 输出: count: 1

state.count = 2;
// 输出: count: 2

5.2 嵌套对象

const state = reactive({
  user: { name: 'Vue', age: 3 }
});

effect(() => {
  console.log('name:', state.user.name);
});
// 输出: name: Vue

state.user.name = 'Vue 4';
// 输出: name: Vue 4

5.3 数组操作

const list = reactive([1, 2, 3]);

effect(() => {
  console.log('length:', list.length);
});
// 输出: length: 3

list.push(4);
// 输出: length: 4

list[0] = 99;
// 数组索引修改也能触发

5.4 computed 缓存

const state = reactive({ count: 1 });
let callCount = 0;

const double = computed(() => {
  callCount++;
  return state.count * 2;
});

console.log(double.value); // 2, callCount = 1
console.log(double.value); // 2, callCount = 1 (缓存命中)

state.count = 5;
console.log(double.value); // 10, callCount = 2
console.log(double.value); // 10, callCount = 2 (再次缓存)

5.5 分支切换

const state = reactive({ flag: true, a: 1, b: 2 });

effect(() => {
  console.log('result:', state.flag ? state.a : state.b);
});
// 输出: result: 1

state.b = 99;
// 无输出(不依赖 b)

state.flag = false;
// 输出: result: 2

state.a = 100;
// 无输出(切换后不再依赖 a)

5.6 watch

const state = reactive({ name: 'Vue' });

watch(
  () => state.name,
  (newVal, oldVal) => {
    console.log(`${oldVal} -> ${newVal}`);
  }
);

state.name = 'React';
// 输出: Vue -> React

state.name = 'Solid';
// 输出: React -> Solid

六、与 Vue 3 源码的对比

特性 我们的实现 Vue 3 源码
依赖存储 WeakMap → Map → Set 相同
effect 栈 parent 指针链表 相同
依赖清理 effect 执行前清除 相同
触发方式 创建副本遍历 toReactive + 调度器
数组支持 基本支持 完整(重写方法 + length 追踪)
集合支持 ✅ collectionHandlers
性能优化 effectScope、bitmask tracks
调度 直接 run queueJob 微任务合并
防递归 effect !== activeEffect 相同

我们的实现省略了一些工程细节,但核心机制与 Vue 3 一致。Vue 3 额外做了:

  1. 微任务调度:将多次状态变更合并为一次更新
  2. effectScope:批量管理 effect 生命周期
  3. 集合处理器:完整支持 Map/Set/WeakMap/WeakSet
  4. 性能优化:使用 bitwise tracks 减少不必要的依赖收集
  5. 类型系统:完整的 TypeScript 类型推导

七、总结

通过约 200 行代码,我们实现了一个包含 reactiverefeffectcomputedwatch 的完整响应式系统。核心机制包括:

  1. Proxy 代理 通过拦截 get/set 实现 reactive
  2. WeakMap → Map → Set 三层结构管理依赖关系
  3. track/trigger 负责在 get 时收集依赖,set 时派发更新
  4. parent 指针 实现 effect 嵌套
  5. cleanup 机制 解决分支切换时的冗余依赖
  6. dirty flag 实现 computed 的惰性计算与缓存
  7. scheduler 实现 watch 与组件更新的调度

理解这些核心原理后,再去看 Vue 3 的 reactivity 源码 就会豁然开朗。

Comments | 0条评论