文章

手写简易 Vue3 响应式系统

手写简易 Vue3 响应式系统

一句话概括

手写 Vue 3 响应式系统的三个核心模块:reactive(Proxy 代理)、effect(副作用追踪)、computed(惰性求值),加起来不到 80 行代码,却是理解整个框架的钥匙。

核心知识点

1. reactive——代理 + 依赖收集 + 触发更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const targetMap = new WeakMap();
let activeEffect = null;

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      track(target, key);               // 收集依赖
      return (res && typeof res === 'object') ? reactive(res) : res; // 懒递归
    },
    set(target, key, value, receiver) {
      const old = target[key];
      const ok = Reflect.set(target, key, value, receiver);
      if (old !== value) trigger(target, key); // 触发更新
      return ok;
    }
  });
}

function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, (depsMap = new Map()));
  let deps = depsMap.get(key);
  if (!deps) depsMap.set(key, (deps = new Set()));
  deps.add(activeEffect);
  activeEffect.deps.push(deps);
}

function trigger(target, key) {
  targetMap.get(target)?.get(key)?.forEach(e => {
    if (e !== activeEffect) e.scheduler ? e.scheduler() : e();
  });
}

2. effect——自动追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function effect(fn, options = {}) {
  const _effect = () => {
    cleanup(_effect);
    activeEffect = _effect;
    fn();
    activeEffect = null;
  };
  _effect.deps = [];
  _effect.scheduler = options.scheduler;
  _effect();
  return _effect;
}

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

3. computed——惰性求值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function computed(getter) {
  let value, dirty = true;

  const _effect = effect(getter, {
    scheduler() {
      if (!dirty) { dirty = true; trigger(_result, 'value'); }
    }
  });

  const _result = {
    get value() {
      track(_result, 'value');
      if (dirty) { dirty = false; value = _effect(); }
      return value;
    }
  };
  return _result;
}

4. ref——原始值包装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ref(value) {
  return {
    get value() {
      track(this, 'value');
      return value;
    },
    set value(newVal) {
      if (newVal !== value) {
        value = newVal;
        trigger(this, 'value');
      }
    }
  };
}

5. watch——精确监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function watch(source, cb) {
  let getter = typeof source === 'function' ? source : () => source.value;
  let oldValue;

  const runner = effect(getter, {
    scheduler() {
      const newValue = runner();
      cb(newValue, oldValue);
      oldValue = newValue;
    }
  });

  oldValue = runner(); // 首次运行收集依赖
}

其实你每天都在用

  • 模板渲染<div>9</div> → render effect 自动收集 countcount++ 触发 diff 更新
  • v-model 双向绑定:输入一个字 → Proxy set → trigger → render effect 重跑 → DOM 更新
  • computed 派生const full = computed(() => first + last) → 第一/末名不变就不算
  • watch 自动请求watch(route.params.id, fetchData) → id 变了立刻发请求
  • 组件卸载清理onUnmountedstop(effect) → cleanup 清空所有 dep 引用

常见误解

  • ❌ 误区:「手写 reactive 只要拦截 get/set 就够了」 deleteProperty 也必须拦截。delete obj.key 不触发 set,但 Vue 2 完全做不到拦截,Vue 3 通过 deleteProperty 支持了。

  • ❌ 误区:「computed 的值在依赖变化时自动重新计算」 错!依赖变化只设置 dirty = true,真正计算发生在下次读取 .value。如果依赖变了但没人读,computed 永远不会重算——这就是惰性求值。

  • ❌ 误区:「手写版不需要 cleanUp」 绝对需要。不清理旧依赖会导致”幽灵依赖”——flag 从 true 切 false 后,改 a 仍然触发本不应该关联的 effect。

  • ❌ 误区:「ref 和 reactive 可以互相替换」 ref 解决的是 Proxy 只能代理对象的问题——ref(0) 的本质是 { value: 0 } 上的 getter/setter,套了层壳。对象级响应式用 reactive(Proxy),原始值用 ref。

一句话总结

80 行代码写出 Vue 3 响应式核心:Proxy 劫持 → track 记账 → trigger 催账 → scheduler 调度——从这里开始,computed / watch / 组件渲染都是这套流水线上的不同工位。

本文由作者按照 CC BY 4.0 进行授权