普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月20日首页

深入 Vue3 响应式系统:手写 Computed 和 Watch 的奥秘

作者 云枫晖
2025年11月20日 13:36

在 Vue3 的响应式系统中,计算属性和监听器是我们日常开发中频繁使用的特性。但你知道它们背后的实现原理吗?本文将带你从零开始,手写实现 computed 和 watch,深入理解其设计思想和实现细节。

引言:为什么需要计算属性和监听器?

在Vue应用开发中,我们经常遇到这样的场景:

  • 派生状态:基于现有状态计算新的数据
  • 副作用处理:当特定数据变化时执行相应操作

Vue3提供了computedwatch来优雅解决这些问题。但仅仅会使用还不够,深入理解其底层原理能让我们在复杂场景下更加得心应手。

手写实现Computed

computed的核心特性包括:

  • 惰性计算:只有依赖的响应式数据变化时才重新计算
  • 值缓存:避免重复计算提升性能
  • 依赖追踪:自动收集依赖关系

computed函数接收一个参数,类型函数或者一个对象,对象包含getset方法,get方法是必须得。基本框架就出来了:

export function computed(getterOrOptions) {
  let getter;
  let setter = undefined;
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
  } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }
}

当你使用过computed函数时,你会发现会返回一个ComputedRefImpl类型的实例。代码就可以进一步写成下面的样子:

export class ComputedRefImpl {
  constructor(getter, setter) {
    this.getter = getter;
    this.setter = isFunction(setter) ? setter : undefined;
  }
}
export function computed(getterOrOptions) {
  /* 上述代码实现省略 */
  const cRef = new ComputedRefImpl(getter, setter);
  return cRef;
}

ComputedRefImpl的实现

ComputedRefImpl类中有几个主要的属性:

  • _value:缓存的计算结果
  • _v_isRef:表示这是一个ref对象,可以通过.value访问
  • effect 响应式副作用实例
  • _dirty 脏值标记,true表示需要重新计算
  • dep 依赖收集容器,存储依赖当前计算属性的副作用 在初始化的时候,将会创建一个ReactiveEffect实例,此类型在手写Reactive中实现了。
class ComputedRefImpl {
  effect = undefined; // 响应式副作用实例
  _value = undefined; // 缓存的计算结果
  __v_isRef = true; // 标识这是一个ref对象,可以通过.value访问
  _dirty = true; // 脏值标记,true表示需要重新计算
  dep = undefined; // 依赖收集容器,存储依赖当前计算属性的副作用

  /**
   * 构造函数
   * @param {Function} getter - 计算属性的getter函数
   * @param {Function} setter - 计算属性的setter函数
   */
  constructor(getter, setter) {
    this.getter = getter;
    this.setter = isFunction(setter) ? setter : () => {};

    // 创建响应式副作用实例,当依赖的数据变化时会触发调度器
    this.effect = new ReactiveEffect(getter, () => {
      // 调度器函数 后续处理
    });
  }
}

通过get valueset value手机依赖和触发依赖

class ComputedRefImpl {
  /* 上述代码实现省略 */
  /**
   * 计算属性的getter
   * 实现缓存机制和依赖收集
   */
  get value() {
    // 如果存在激活的副作用,则进行依赖收集
    if (activeEffect) {
      trackEffects(this.dep || (this.dep = new Set()));
    }

    // 如果是脏值,则重新计算并缓存结果
    if (this._dirty) {
      this._value = this.effect.run(); // 执行getter函数获取新值
      this._dirty = false; // 清除脏值标记
    }

    return this._value; // 返回缓存的值
  }

  /**
   * 计算属性的setter
   * @param {any} newValue - 新的值
   */
  set value(newValue) {
    // 如果有setter函数,则调用它
    if (this.setter) {
      this.setter(newValue);
    }
  }
}

当依赖值发生变化后,将触发副作用的调度器,触发计算属性的副作用更新。

constructor(getter, setter) {
  this.getter = getter;
  this.setter = isFunction(setter) ? setter : () => {};

  // 创建响应式副作用实例,当依赖的数据变化时会触发调度器
  this.effect = new ReactiveEffect(getter, () => {
    // 调度器函数:当依赖变化时执行
    this._dirty = true; // 标记为脏值,下次访问时需要重新计算
    triggerEffects(this.dep); // 触发依赖当前计算属性的副作用更新
  });
}

完整代码及用法示例

import { isFunction } from "./utils";
import {
  activeEffect,
  ReactiveEffect,
  trackEffects,
  triggerEffects,
} from "./effect";

/**
 * 计算属性实现类
 * 负责管理计算属性的getter、setter以及缓存机制
 */
class ComputedRefImpl {
  effect = undefined; // 响应式副作用实例
  _value = undefined; // 缓存的计算结果
  __v_isRef = true; // 标识这是一个ref对象,可以通过.value访问
  _dirty = true; // 脏值标记,true表示需要重新计算
  dep = undefined; // 依赖收集容器,存储依赖当前计算属性的副作用

  /**
   * 构造函数
   * @param {Function} getter - 计算属性的getter函数
   * @param {Function} setter - 计算属性的setter函数
   */
  constructor(getter, setter) {
    this.getter = getter;
    this.setter = isFunction(setter) ? setter : () => {};

    // 创建响应式副作用实例,当依赖的数据变化时会触发调度器
    this.effect = new ReactiveEffect(getter, () => {
      // 调度器函数:当依赖变化时执行
      this._dirty = true; // 标记为脏值,下次访问时需要重新计算
      triggerEffects(this.dep); // 触发依赖当前计算属性的副作用更新
    });
  }

  /**
   * 计算属性的getter
   * 实现缓存机制和依赖收集
   */
  get value() {
    // 如果存在激活的副作用,则进行依赖收集
    if (activeEffect) {
      trackEffects(this.dep || (this.dep = new Set()));
    }

    // 如果是脏值,则重新计算并缓存结果
    if (this._dirty) {
      this._value = this.effect.run(); // 执行getter函数获取新值
      this._dirty = false; // 清除脏值标记
    }

    return this._value; // 返回缓存的值
  }

  /**
   * 计算属性的setter
   * @param {any} newValue - 新的值
   */
  set value(newValue) {
    // 如果有setter函数,则调用它
    if (this.setter) {
      this.setter(newValue);
    }
  }
}

/**
 * 创建计算属性的工厂函数
 * @param {Function|Object} getterOrOptions - getter函数或包含get/set的对象
 * @returns {ComputedRefImpl} 计算属性引用实例
 */
export const computed = (getterOrOptions) => {
  let getter; // getter函数
  let setter = undefined; // setter函数

  // 根据参数类型确定getter和setter
  if (isFunction(getterOrOptions)) {
    // 如果参数是函数,则作为getter
    getter = getterOrOptions;
  } else {
    // 如果参数是对象,则分别获取get和set方法
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }

  // 创建并返回计算属性实例
  const cRef = new ComputedRefImpl(getter, setter);
  return cRef;
};

示例用法:

import { reactive, computed } from "./packages/index";
const state = reactive({
  firstName: "tom",
  lastName: "lee",
  friends: ["jacob", "james", "jimmy"],
});
const fullName = computed({
  get() {
    return state.firstName + " " + state.lastName;
  },
  set(newValue) {
    [state.firstName, state.lastName] = newValue.split(" ");
  },
});
effect(() => {
  app.innerHTML = `
    <div> Welcome ${fullName.value} !</div>
  `;
});
setTimeout(() => {
  fullName.value = "jacob him";
}, 1000);
setTimeout(() => {
  console.log(state.firstName, state.lastName); // firstName: jacob lastName: him 
}, 2000);

手写实现Watch和WatchEffect

watch函数接收三个参数:

  • source:要监听的数据源,可以是响应式对象或函数
  • cb:数据变化时执行的回调函数
  • options 配置选项:immediate:是否立即执行,deep:是否深度监听等
export function watch(source, cb, {immediate = false} = {}) {
 // 待后续实现
}

1. watch的实现

首先source是否可以接受多种监听的数据源:响应式对象、多个监听数据源的数组、函数。将不同方式统一起来。

export function watch(source, cb, { immediate = false } = {}) {
  let getter;
  if (isReactive(source)) {
    // 如果是响应式对象 则调用traverse
    getter = () => traverse(source);
  } else if (isFunction(source)) {
    // 如果是函数 则直接执行
    getter = source;
  } else if (isArray(source)) {
    // 处理数组类型的监听源
    getter = () =>
      source.map((s) => {
        if (isReactive(s)) {
          return traverse(s);
        } else if (isFunction(s)) {
          return s();
        }
      });
  }
}
/**
 * 遍历对象及其嵌套属性的函数
 * @param {any} source - 需要遍历的源数据
 * @param {Set} s - 用于记录已访问对象的集合,避免循环引用
 * @returns {any} 返回原始输入数据
 */
export function traverse(source, s = new Set()) {
  // 检查是否为对象类型,如果不是则直接返回
  if (!isObject(source)) {
    return source;
  }
  // 检测循环引用,如果对象已被访问过则直接返回
  if (s.has(source)) {
    return source;
  }
  // 将当前对象加入已访问集合
  s.add(source);
  // 递归遍历对象的所有属性
  for (const key in source) {
    traverse(source[key], s);
  }
  return source;
}

处理完souce参数后,创建一个ReactiveEffect实例,对监听源产生响应式的副作用。

export function watch(source, cb, { immediate = false } = {}) {
  /* 上述代码以实现省略 */
  let oldValue;
  // 定义副作用执行的任务函数
  const job = () => {
    let newValue = effect.run(); // 获取最新值
    cb(oldValue, newValue); // 触发回调
    oldValue = newValue; // 新值赋给旧值
  };

  // 创建响应式副作用实例
  const effect = new ReactiveEffect(getter, job);
  if (immediate) {
    job();
  } else {
    oldValue = effect.run();
  }
}

⚠️ 性能注意

traverse函数会递归遍历对象的所有嵌套属性,在大型数据结构上使用深度监听(deep: true)时会产生显著性能开销。建议:

  • 只在必要时使用深度监听
  • 尽量使用具体的属性路径而非整个对象
  • 考虑使用计算属性来派生需要监听的数据

2. watchEffect的实现

实现了watch函数后,watchEffect的实现就容易了。

// watchEffect.js
import { watch } from "./watch";
export function watchEffect(effect, options) {
  return watch(effect, null, options);
}
// watch.js
const job = () => {
  if (cb) {
    let newValue = effect.run(); // 获取最新值
    cb(oldValue, newValue); // 触发回调
    oldValue = newValue; // 新值赋给旧值
  } else {
    effect.run(); // 处理watchEffect
  }
};

用法示例

watch([() => state.lastName, () => state.firstName], (oldValue, newValue) => {
  console.log("oldValue: " + oldValue, "newValue: " + newValue);
});
setTimeout(() => {
  state.lastName = "jacob";
}, 1000);
setTimeout(() => {
  state.firstName = "james";
}, 1000);
/*
1秒钟后:oldValue: lee,tom newValue: jacob,tom
2秒钟后:oldValue: jacob,tom newValue: jacob,james
*/

总结

本文核心内容

通过手写实现Vue3的computedwatch,我们深入理解了:

  • 计算属性的惰性计算、值缓存和依赖追踪机制
  • 监听器的多数据源处理和深度监听原理
  • 响应式系统中副作用调度和依赖收集的完整流程

代码地址

📝 本文完整代码
[GitHub仓库链接] | [github.com/gardenia83/…]

下篇预告

在下一篇中,我们将继续深入Vue3响应式系统,手写实现:

《深入 Vue3 响应式系统:从ref到toRefs的完整实现》

  • refshallowRef的底层机制
  • toReftoRefs的响应式转换原理
  • 模板Ref和组件Ref的特殊处理
  • Ref自动解包的神秘面纱

敬请期待! 🚀


掌握底层原理,让我们的开发之路更加从容自信

昨天以前首页

Vue3 响应式原理:从零实现 Reactive

作者 云枫晖
2025年11月18日 16:32

前言

还记得第一次使用 Vue 时的那种惊艳吗?数据变了,视图自动更新,就像魔法一样!但作为一名有追求的前端开发者,我们不能只停留在"会用"的层面,更要深入理解背后的原理。

今天,我将带你从零实现一个 Vue3 的响应式系统,手写代码不到 200 行,却能覆盖核心原理。读完本文,你将彻底明白:

  • 🤔 为什么 Vue3 放弃 Object.defineProperty 选择 Proxy?
  • 🔥 依赖收集和触发更新的精妙设计
  • 🎯 数组方法的重写背后隐藏的智慧
  • 💡 Vue3 响应式相比 Vue2 的性能优势

什么是响应式?

简单来说,响应式是当数据变化时,自动执行依赖数据的代码

const state = reactive({ count: 0 });
effect(() => {
  console.log(`count值变化:${state.count}`);
});
state.count++; // count值变化:1
state.count++; // count值变化:2

vue2和vue3响应式区别

特性 vue2(Object.defineProperty) vue3(proxy)
对象新增属性 $set api实现响应式 直接支持
对象删除属性 $delete api 实现响应式 直接支持
数组拦截 改写数组原型方法 原生支持,重新包装
性能 递归遍历所有属性 懒代理,访问时才代理

综上所述Proxy的优势非常的明显,这就是Vue3选择重构响应式系统的根本原因。

手写实现:从零构建响应式

1. 项目结构

├── reactive.js           // reactive 核心
├── effect.js             // 副作用管理
├── baseHandler.js        // Proxy 处理器
├── arrayInstrumentations.js // 数组方法重写
├── utils.js              // 工具函数
└── index.js              // 入口文件

响应式入口

我们先从reactive函数着手,使用过vue3应该对reactive并不陌生。此函数接收一个对象,然后返回一个代理对象。

// reactive.js
export function reactive(target) {
  // 判断target是否一个对象
  if (!isObject(target)) {
    return target;
  }
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      // 后续收集依赖
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      if (oldValue != value) {
        // 后续触发更新
      }
    }
  })
  return proxy;
}

目前已经搭建了reactive函数的框架,但是目前还有些问题:

  1. 同一个对象代理多次,会返回不同的代理对象,这样性能上带来不必要的开销。
const originalObj = { name: 'Vue', version: 3 }; // 第一次调用 reactive 
const proxy1 = reactive(originalObj); // 第二次调用 reactive(传入同一个对象)
const proxy2 = reactive(originalObj); // 验证两个代理是同一个实例
console.log(proxy1 === proxy2); // false

可以通过缓存代理对象解决此类问题,采用WeakMap来缓存代理对象,keytarget,value为代理对象。

// 缓存代理对象,避免重复代理
const reactiveMap = new WeakMap();
export function reactive(target) {
  // 判断target是否一个对象
  if (!isObject(target)) {
    return target;
  }
  // 将target是否已经代理过,如果代理则返回缓存的代理对象。
  const existsProxy = reactiveMap.get(target);
  if (existsProxy) {
    return existsProxy;
  }
  const proxy = new Proxy(target, {
    /* 此处暂时省略 */
  })
  // 缓存代理对象
  reactiveMap.set(target, proxy);
  return proxy;
}

💡 提示
在上述代码中之所以采用WeakMap主要考虑key是一个对象并且WeakMap可以当target不再引用时会自动清理。

  1. 当已经被reactive处理后,再次调用reactive时,又被代理。
const originalObj = { count: 1 }; // 第一次创建响应式对象 
const proxy1 = reactive(originalObj);
const proxy2 = reactive(proxy1); // 将代理对象再次代理

Vue3的源码中通过__v_isReactive标记来判断:

export const ReactiveFlags = {
  IS_REACTIVE: "__v_isReactive",
};
export function reactive(target) {
  // 判断target是否一个对象
  if (!isObject(target)) {
    return target;
  }
  // 避免重复代理
  if (target[ReactiveFlags.IS_REACTIVE]) {
    return target;
  }
  // 将target是否已经代理过,如果代理则返回缓存的代理对象。
  const existsProxy = reactiveMap.get(target);
  if (existsProxy) {
    return existsProxy;
  }
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      if (key === ReactiveFlags.IS_REACTIVE) {
        return true;
      }
    },
    /* 此处暂时省略 */
  })
  // 缓存代理对象
  reactiveProxy.set(target, proxy);
  return proxy;
}
  • 当第一次调用reactive时,检查target中是否已经存在__v_isReactive标记,正常情况下是undefined,返回一个Proxy代理对象。
  • 如果将返回的Proxy代理对象,再次调用reactive函数,再次检查__v_isReactive是否存在,将会进入Proxy代理对象的get方法中,进入判断返回true。从而达到无论将相同代理对象调用多少次reactive都不会产生多层代理对象嵌套。

Vue3getset包裹的对象是抽离到一个单独的文件baseHandlers中的,我们也进行相同调整:

// baseHandlers.js
import { ReactiveFlags } from "./reactive"; 
export const mutableHandlers = {
  get(target, key, receiver) {
    // 1. 响应式标识判断(Vue3 源码标准逻辑)
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true;
    }
    /* 后续实现依赖收集 */
  },

  set(target, key, value, receiver) {
    const oldValue = target[key];
    if (oldValue !== value) {
     // 后续触发更新
    }
  },
};
// reactive.js
import { mutableHandlers } from "./baseHandler.js";
export const ReactiveFlags = {
  IS_REACTIVE: "__v_isReactive",
};
export function reactive(target) {
  // 判断target是否一个对象
  if (!isObject(target)) {
    return target;
  }
  // 避免重复代理
  if (target[ReactiveFlags.IS_REACTIVE]) {
    return target;
  }
  // 将target是否已经代理过,如果代理则返回缓存的代理对象。
  const existsProxy = reactiveMap.get(target);
  if (existsProxy) {
    return existsProxy;
  }
  const proxy = new Proxy(target, mutableHandlers)
  // 缓存代理对象
  reactiveProxy.set(target, proxy);
  return proxy;
}

副作用管理

Vue3中提供了一个effect函数,接收一个函数,提供给用户获取数据渲染视图,数据变化后再次调用该函数更新视图。effect具体实现如下:

// 当前响应器
export let activeEffect;
// 清理依赖
export function cleanupEffect(effect) {
  effect.deps.forEach((dep) => {
    dep.delete(effect);
  });
  effect.deps.length = 0;
}
class ReactiveEffect {
  active = true; // 是否激活状态
  deps = []; // 依赖集合数组
  parent = undefined; // 父级effect 处理嵌套effect
  constructor(fn, scheduler) {
    this.fn = fn; // 用户提供的函数
    this.scheduler = scheduler // 调度器(用于computed、watch)
  }
  run() {
    if (!this.active) {
      return this.fn();
    }
    try {
      // 建立effect的父子关系 确保依赖收集的准确性
      this.parent = activeEffect;
      activeEffect = this;
      // 清除旧依赖 避免不必要的更新
      cleanupEffect(this);
      return this.fn();
    } finally {
      // 恢复父级effect
      activeEffect = this.parent;
      this.parent = undefined;
    }
  }
}
export function effect(fn, options = {}) {
  const e = new ReactiveEffect(fn, options.scheduler);
  e.run();
  // 给到用户自行控制响应
  const runner = e.run.bind(e); // 确保this的指向
  runner.effect = e;
  return runner;
}
// 收集依赖函数
export function track(target, key) {}
// 触发依赖
export function trigger(target, key) {}

实现收集依赖

const state = reactive({ name: 'jim '});
effect(() => {
  document.getElementById('app').innerHTML = `${state.name}`;
})

当调用effect函数时,将会执行用户提供的函数逻辑,如上述代码执行state.name时将会进入代理对象的get方法,该方法中进行依赖收集。即调用track函数。

// baseHandler.js
import { isObject } from "./utils";
import { ReactiveFlags, reactive } from "./reactive";
import { track } from "./effect";
export const mutableHandlers = {
  get(target, key, receiver) {
    // 响应式标识判断(Vue3 源码标准逻辑)
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true;
    }
    // 收集依赖(所有属性访问都需要追踪)
    track(target, key);
    // 执行原生 get 操作 
    const result = Reflect.get(target, key, receiver);
    // 深层响应式:嵌套对象/数组自动转为响应式(Vue3 懒代理特性)
    if (result && isObject(result)) {
      return reactive(result);
    }

    return result;
  },
  /* set方法在此省略 */
};

effect.jstrack函数中实现依赖收集

// 当前响应器
export let activeEffect;
export const targetMap = new WeakMap(); //  收集依赖
export function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set())); // Vue3内部是一个Dep类
  }
  trackEffects(dep);
}
export function trackEffects(dep) {
  let shouldTrack = !dep.has(activeEffect);
  if (shouldTrack) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep); // 双向记录
  }
}

收集完毕后的依赖关系结构:

WeakMap {
  target1: Map {
    key1: Set[effect1, effect2],
    key2: Set[effect3]
  },
  target2: Map { ... }
}

实现触发依赖

当用户对数据进行了修改时,需要根据收集的依赖自动对应执行effect的用户函数。

state.name = 'tom'

baseHandle.js中调用trigger函数。该函数实现具体的触发依赖

// baseHandle.js
import { isObject } from "./utils";
import { ReactiveFlags, reactive } from "./reactive";
import { track, trigger } from "./effect";
export const mutableHandlers = {
  /* get方法实现省略 */
  set(target, key, value, receiver) {
    const oldValue = target[key];
    const success = Reflect.set(target, key, value, receiver);
    // 7. 只有值变化且是自身属性时,才触发更新(避免原型链干扰)
    if (success && oldValue !== value) {
      trigger(target, key); // 触发依赖
    }
    return success;
  },
};

effect.js中实现trigger函数的实现

// 触发依赖
export function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  let dep = depsMap.get(key);
  if (dep) triggerEffects(dep);
}
export function triggerEffects(dep) {
  const effects = [...dep]; // 避免在遍历 Set 过程中修改 Set 本身导致的迭代器异常问题
  effects.forEach((effect) => {
    // 避免无限递归:当前正在执行的effect不再次触发
    if (effect != activeEffect) {
      if (!effect.scheduler) {
        effect.run();
      } else {
        effect.scheduler();
      }
    }
  });
}

对数组响应式处理

Vue3源码中单独一个文件arrayInstrumentations对数组的方法重新包装了一下。我的处理与源码有点不同毕竟是简易版本,但是原理都是一样的

// arrayInstrumentations.js
import { reactive } from "./reactive";
import { trigger } from "./effect";
import { isArray } from "./utils";

// 需要特殊处理的数组修改方法(Vue3 源码中也是用 Set 存储)
export const arrayInstrumentations = new Set([
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
]);

/**
 * 包装数组修改方法,添加响应式能力
 * @param {string} method - 数组方法名
 * @returns 包装后的函数
 */
function createArrayMethod(method) {
  // 获取原生数组方法
  const originalMethod = Array.prototype[method];

  return function (...args) {
    // 1. 执行原生数组方法(保证原有功能不变)
    const result = originalMethod.apply(this, args);

    // 2. 处理新增元素的响应式转换(push/unshift/splice 可能添加新元素)
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args; // 这两个方法的参数就是新增元素
        break;
      case "splice":
        inserted = args.slice(2); // splice 第三个参数及以后是新增元素
        break;
    }
    // 新增元素转为响应式(递归处理对象/数组)
    if (inserted) {
      inserted.forEach((item) => {
        if (typeof item === "object" && item !== null) {
          reactive(item);
        }
      });
    }

    // 3. 触发依赖更新(Vue3 源码中会触发 length 和对应索引的更新)
    trigger(this, "length");
    return result;
  };
}

// 生成所有包装后的数组方法(键:方法名,值:包装函数)
export const arrayMethods = Object.create(null);
arrayInstrumentations.forEach((method) => {
  arrayMethods[method] = createArrayMethod(method);
});

/**
 * 判断是否是需要拦截的数组方法
 * @param {unknown} target - 目标对象
 * @param {string} key - 属性名/方法名
 * @returns boolean
 */
export function isArrayInstrumentation(target, key) {
  return isArray(target) && arrayInstrumentations.has(key);
}

然后在baseHandler中添加数组情况下的逻辑

// baseHandler.js
import { isObject } from "./utils";
import { ReactiveFlags, reactive } from "./reactive";
import { track, trigger } from "./effect";
// 引入抽离的数组工具
import { isArrayInstrumentation, arrayMethods } from "./arrayInstrumentations";

export const mutableHandlers = {
  get(target, key, receiver) {
    // 1. 响应式标识判断(Vue3 源码标准逻辑)
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true;
    }
    // 2. 收集依赖(所有属性访问都需要追踪)
    track(target, key);
    // 3. 执行原生 get 操作
    const result = Reflect.get(target, key, receiver);
    // 4. 数组方法拦截:如果是需要处理的数组方法,返回包装后的函数
    if (isArrayInstrumentation(target, key)) {
      // 绑定 this 为目标数组,确保原生方法执行时上下文正确
      return arrayMethods[key].bind(target);
    }
    // 5. 深层响应式:嵌套对象/数组自动转为响应式(Vue3 懒代理特性)
    if (result && isObject(result)) {
      return reactive(result);
    }

    return result;
  },

  set(target, key, value, receiver) {
    const oldValue = target[key];
    const isArrayTarget = Array.isArray(target);
    // 6. 执行原生 set 操作
    const success = Reflect.set(target, key, value, receiver);
    // 7. 只有值变化且是自身属性时,才触发更新(避免原型链干扰)
    if (success && oldValue !== value) {
      // 数组索引设置:触发对应索引和 length 更新(Vue3 源码逻辑)
      if (isArrayTarget && key !== "length") {
        const index = Number(key);
        if (index >= 0 && index < target.length) {
          trigger(target, key); // 触发索引更新
          trigger(target, "length"); // 触发长度更新
          return success;
        }
      }
      // 普通对象/数组 length 设置:触发对应 key 更新
      trigger(target, key);
    }
    return success;
  },
};

完整代码使用示例

import { reactive, effect } from "./packages/index";
const state = reactive({
  name: "vue",
  version: "3.4.5",
  author: "vue team",
  friends: ["jake", "james"],
});
effect(() => {
  app.innerHTML = `
    <div> Welcome ${state.name} !</div>
    <div> ${state.friends} </div>
  `;
});
setTimeout(() => {
  state.name = "vue3"; 
  state.friends.push("jimmy");
}, 1000);
// 一开始显示:
//    Welcome vue 
//    'jake,james'
// 1秒钟后:
//    Welcome vue3
//    'jake,james,jimmy'

总结

通过这 200 行代码,我们实现了一个完整的 Vue3 响应式系统核心:

  • 响应式代理: 基于 Proxy 的懒代理机制
  • 依赖收集: 精准的 effect 追踪
  • 批量更新: 避免重复执行的调度机制
  • 数组处理: 重写数组方法保持响应性
  • 嵌套支持: 自动的深层响应式转换

完整代码和资源 本文所有代码已开源,包含详细注释和测试用例:

GitHub 仓库:github.com/gardenia83/…

这为我们理解 Vue3 的响应式原理提供了坚实的基础,也为学习更高级的特性如 computed、watch 等打下了基础。

❌
❌