深入浅出Vue响应式原理:手把手解析Proxy与依赖追踪

hello,大家好。我是你们的ys指风不买醉

今天我们来看看reactive 在vue中怎么实现响应式的。什么是响应式?在Vue.js中,响应式指 vue实例数据变化触发相关联dom更新的特性,这样我们程序员就不用手动操作dom,这样方便代码维护。

Vue 响应式视图可以通过es5 object.defineProperty 或者 es6 proxy 来实现,会劫持数据变化,追踪数据变化。当数据变化后,以依赖收集的发布-订阅模式,触发相关联数据更新。想知道es5 object.defineProperty 或者 es6 proxy区别 ,文末给出了答案哦。

首先,可以这样回答 refreactive 的区别:

  • reactive 是基于 ES6 中的 Proxy 代理实现的响应式系统。它通过代理一个引用类型对象(如对象或数组),对对象的属性进行拦截。当属性值被读取时,会触发依赖收集,将当前的 effect(副作用函数)与该属性关联;当属性值被修改时,会触发更新,通知所有关联的 effect 重新执行。
  • ref 则是一个更通用的响应式封装方式,既可以用于原始数据类型(如数字、字符串等),也可以用于引用数据类型(如对象、数组等)。当代理原始数据类型时,ref 会返回一个 RefImpl 实例,通过 value 属性来读取和修改值,并利用 getset 方法实现响应式更新。当代理引用数据类型时,ref 会将其内部数据通过 reactive 进行代理,从而复用 reactive 的响应式机制。

二、手撕源码:实现mini版reactive

2.1 代理工厂函数(含详细注释)

js 复制代码
// reactive.js
// WeakMap优势:1.键必须是对象 2.弱引用(不影响垃圾回收)
import {
    mutableHandlers,
} from './baseHandlers'
const reactiveMap = new WeakMap(); // 存储已代理对象的缓存池

export function reactive(target) {
  // 创建代理对象的标准化流程
  return createReactiveObject(
    target,
    reactiveMap,
    mutableHandlers // 基础拦截器
  );
}

function createReactiveObject(target, proxyMap, handlers) {
  // 类型安全检查:只代理对象类型
  if (typeof target !== 'object' || target === null) {
    console.warn(`无法代理非对象类型:${target}`);
    return target;
  }

  // 检查是否已有缓存(避免重复代理)
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    console.log('检测到重复代理,直接返回缓存');
    return existingProxy;
  }

  // 创建代理核心操作
  const proxy = new Proxy(target, handlers);
  
  // 缓存代理结果
  proxyMap.set(target, proxy);
  console.log('新建代理对象并缓存', proxy);
  return proxy;
}

可以增加设置浅层代理shallowReactive

js 复制代码
import {
    mutableHandlers,
    shallowHandlers
} from './baseHandlers'
export const reactiveMap = new WeakMap(); // 全局依赖地图
export const shallowReactiveMap = new WeakMap(); // 浅依赖

// target: 代理的目标对象 
export const reactive = (target) => {
    // 返回代理对象
    return createReactiveObject(target,reactiveMap,mutableHandlers)
}

export const shallowReactive = (target) => {
    return createReactiveObject(target,shallowReactiveMap,shallowHandlers)
}

// proxyMap 代理地图 proxyHandlers 代理处理函数
function createReactiveObject(target,proxyMap,proxyHandlers) {
    if (typeof target !== 'object') {
        console.warn('target must be an object');
        return target
    }
    const existingProxy = proxyMap.get(target);
    if (existingProxy) {
    // 该对象是否已经被代理过(已经是响应式对象)
        return existingProxy 
    }
    
    // 执行代理操作(将target处理成响应式)
    const proxy = new Proxy(target,proxyHandlers)
    
    // 往 proxyMap 增加 proxy, 把已经代理过的对象缓存起来
    proxyMap.set(target,proxy)
    
    return proxy
}

2.2 代理拦截器实现(逐步解析)

js 复制代码
// baseHandlers.js
import { track, trigger } from "./effect";

// 创建可配置的getter生成器
function createGetter(shallow = false) {
  return function get(target, key, receiver) {
    console.log(`拦截【读取】操作:${String(key)}`);
    
    // 反射API保证this正确性
    const res = Reflect.get(target, key, receiver);
    
    // 依赖收集(记录当前属性被哪些effect使用)
    track(target, "get", key);
    
    // 递归代理:当值为对象时继续代理
    if (isObject(res) && !shallow) {
      return reactive(res);
    }
    
    return res;
  };
}

// 创建setter生成器
function createSetter() {
  return function set(target, key, value, receiver) {
    console.log(`拦截【写入】操作:${String(key)}=${value}`);
    
    const oldValue = target[key];
    const success = Reflect.set(target, key, value, receiver);
    
    // 只有值变化时才触发更新(性能优化)
    if (success && oldValue !== value) {
      trigger(target, "set", key);
    }
    
    return success;
  };
}

// 导出标准处理器
export const mutableHandlers = {
  get: createGetter(),
  set: createSetter()
};

2.3 为什么需要Reflect?

使用Reflect API的三个关键原因:

  1. 保证receiver正确性:正确处理继承场景中的this指向
  2. 操作标准化 :统一对象操作方式(如in操作符对应Reflect.has
  3. 返回值规范化:布尔值表示操作是否成功

三、依赖追踪系统:Vue的"记忆大师"

3.1 核心数据结构图解

js 复制代码
graph TD // 看不明白的宝子,可以先往下看哦
  A[WeakMap] --> B(原始对象)
  B --> C[Map]
  C --> D(属性key)
  D --> E[Set]
  E --> F(effect函数1)
  E --> G(effect函数2)

3.2 effect实现(带详细流程注释)

js 复制代码
// effect.js
let activeEffect = null; // 当前正在收集的副作用函数
const targetMap = new WeakMap(); // 全局依赖存储中心

export function effect(fn, options = {}) {
  console.log('注册副作用函数:', fn.name || '匿名函数');
  
  const effectFn = () => {
    try {
      // 设置为当前活跃effect
      activeEffect = effectFn;
      // 执行函数触发依赖收集
      return fn();
    } finally {
      // 执行完成后重置
      activeEffect = null;
    }
  };
  
  // 立即执行一次(除非配置lazy)
  if (!options.lazy) {
    effectFn();
  }
  
  return effectFn;
}

3.3 依赖收集与触发(逐步解析)

js 复制代码
// track函数:记录依赖关系
export function track(target, type, key) {
  if (!activeEffect) return;
  
  console.log(`收集依赖:${target}的${String(key)}属性`);

  // 三级依赖结构查询
  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()));
  }

  // 避免重复添加
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    console.log(`新增依赖:当前共有${dep.size}个依赖`);
  }
}

// trigger函数:触发更新
export function trigger(target, type, key) {
  console.log(`触发更新:${target}的${String(key)}属性变更`);

  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const dep = depsMap.get(key);
  if (dep) {
    // 创建副本避免无限循环
    const effects = new Set(dep);
    effects.forEach(effect => {
      console.log('执行副作用函数:', effect.name || '匿名函数');
      effect();
    });
  }
}

四、实战测试:让我们的系统跑起来

html 复制代码
<!DOCTYPE html>
<body>
  <div id="app"></div>
  <script type="module">
    import { reactive, effect } from './reactivity.js';

    const state = reactive({
      counter: 0,
      user: {
        name: '小明'
      }
    });

    // 注册副作用
    effect(() => {
      document.getElementById('app').innerHTML = `
        <h1>${state.user.name}的计数器:${state.counter}</h1>
      `;
    });

    // 测试响应式
    setInterval(() => {
      state.counter++;
      if(state.counter % 5 === 0) {
        state.user.name = state.user.name === '小明' ? '小红' : '小明';
      }
    }, 1000);
  </script>
</body>
</html>

运行结果

  1. 每秒计数器自动递增
  2. 每5秒切换用户名
  3. 界面自动更新,无需手动操作DOM

五、常见面试问题深度解析

5.1 为什么Proxy比defineProperty更好?

对比维度 defineProperty Proxy
数组处理 需要重写数组方法 直接检测索引变化
新增属性 需要Vue.set 自动检测
删除属性 需要Vue.delete 自动检测
性能 初始化时递归转换 按需代理
嵌套对象 一次性深度转换 惰性代理

5.2 再从源码回调:reactive与ref的区别是什么?

js 复制代码
// ref实现原理(简化版)
class RefImpl {
  constructor(value) {
    this._value = isObject(value) ? reactive(value) : value;
  }
  
  get value() {
    track(this, 'value');
    return this._value;
  }
  
  set value(newVal) {
    this._value = isObject(newVal) ? reactive(newVal) : newVal;
    trigger(this, 'value');
  }
}

function ref(value) {
  return new RefImpl(value);
}

核心区别

  • 包装方式:ref通过对象包装,reactive直接代理
  • 访问方式 :ref需要.value,reactive直接访问
  • 类型支持:ref支持所有类型,reactive仅对象

六、从源码看框架设计

6.1 Vue响应式系统的精妙设计

  1. 分层架构:代理层/依赖层/调度层分离(多个文件分块)
  2. 惰性代理:嵌套对象按需代理,提升性能(shallowReactive 浅层代理)
  3. 弱引用存储:避免内存泄漏(weakMap)
  4. 批处理更新:通过微任务队列合并更新(Promise处理)

6.2 性能优化技巧

js 复制代码
// 调度器示例(源码简化版)
function trigger(target, key) {
  const effects = getDeps(target, key);
  
  // 批处理更新
  Promise.resolve().then(() => {
    effects.forEach(effect => {
      if (effect.scheduler) {
        effect.scheduler();
      } else {
        effect();
      }
    });
  });
}

优化手段

  • 异步更新队列
  • 副作用去重
  • 调度优先级控制

七、总结

通过实现这个mini响应式系统,我们掌握了:

  1. Proxy的拦截原理
  2. 依赖收集的发布-订阅模式
  3. 响应式系统的分层设计

其实用js写一个reactive就三个设计理念。

  • 1.对象代理。(基于Proxy)。
  • 2.收集副作用函数。在get的时候触发。
  • 3.触发副作用函数。在set的时候触发。

希望这篇文章能帮助你真正理解Vue响应式的精髓,下次面试被问到"Vue如何实现响应式"时,可以自信地从Proxy讲到依赖收集,从性能优化谈到设计模式!

完整代码如下:

js 复制代码
// reactive.js
// map es6 新增数据结构 弱引用 hashMap 
// key取value json 的key 只能是字符串,map 可以是对象
import {
    mutableHandlers,
    shallowHandlers
} from './baseHandlers'
export const reactiveMap = new WeakMap(); // 全局依赖地图
export const shallowReactiveMap = new WeakMap(); // 浅依赖

// target: 代理的目标对象 
export const reactive = (target) => {
    // 返回代理对象
    return createReactiveObject(target,reactiveMap,mutableHandlers)
}
// proxyMap 代理地图 proxyHandlers 代理处理函数
function createReactiveObject(target,proxyMap,proxyHandlers) {
    if (typeof target !== 'object') {
        console.warn('target must be an object');
        return target
    }
    const existingProxy = proxyMap.get(target);
    if (existingProxy) {
        return existingProxy // 存在,直接返回
    }
    const proxy = new Proxy(target,proxyHandlers)
    proxyMap.set(target,proxy)
    return proxy
}
export const shallowReactive = (target) => {
    return createReactiveObject(target,shallowReactiveMap,shallowHandlers)
}

注意增加shallow浅代理哦

js 复制代码
import { track,trigger} from "./effect.js"

const get = createGetter(); // 创建get方法
const set = createSetter(); 
// 收集 shallow 浅显 
function createGetter(shallow = false) {

    // {a:1,b:2,c:{d:{e:1,f:2}} 递归
    return function get(target,key,receiver) {
        console.log('target被读取值',target,key);
        track(target,"get",key);
        // 设置源对象键值 target[key] = value
        let res = target[key];

        return res;
    }
}
function createSetter() {
    return function set(target,key,value,receiver) {
        const res = Reflect.set(target,key,value,receiver)
        trigger(target,"set",key);
        return res;
    }
}


export const mutableHandlers = {
    get,
    set
}

const shallowReactiveGet = createGetter(true)
export const shallowReactiveHandlers = {
    get:shallowReactiveGet,
    set
}

订阅,进行收集依赖和触发依赖

js 复制代码
let activeEffect = null; // 当前effect 
let targetMap = new WeakMap(); // 存储所有响应式对象

// 注册一个响应式副作用函数fn,立即执行并且返回自身
export function effect(fn) {
    // 立即执行一次 返回函数 
    console.log(fn,'......///');
    // 赋值全局activeEffect
    const effectFn = () => {
        try{
            activeEffect = effectFn
            return fn() // 搜集依赖
        } catch (e) {
            console.log('effectFn error',e);
        }
    }
    effectFn()
    return effectFn 
}
// 依赖收集
export function track(target,type,key) {
    console.log('触发track -> target:type(get |{{}} | onMounted)',target,type,key);

    let depsMap = targetMap.get(target); // 收集依赖
    if (!depsMap) {
        // 没有depsMap 创建一个Map
        targetMap.set(target,depsMap = new Map())
    }
    // console.log(depsMap,'depsMap????');

    let dep = depsMap.get(key);
    if (!dep) {
      depsMap.set(key, (dep = new Set()));
    }
  
    dep.add(activeEffect); // 将当前副作用函数添加到依赖集合中
}
// 依赖触发
export function trigger(target,type,key) {
    console.log('触发trigger -> target:type(get |{{}} | onMounted)',target,type,key);
    const depsMap = targetMap.get(target);
    if (!depsMap) return;

    const dep = depsMap.get(key);
    if(dep) {
        dep.forEach(effectFn => {
            effectFn(); // 触发所有副作用函数
        });
    }
    
}
相关推荐
大聪明了1 分钟前
Nuxt3 使用 ElementUI Plus报错问题
前端
Ama_tor7 分钟前
网页制作16-Javascipt时间特效の设置D-DAY倒计时
前端·javascript·html
几何心凉18 分钟前
两款好用的工具,大模型训练事半功倍.....
前端
Pandaconda28 分钟前
【后端开发面试题】每日 3 题(十二)
数据库·后端·面试·负载均衡·高并发·后端开发·acid
uhakadotcom40 分钟前
阿里云MaxFrame分布式计算框架:十倍性能提升的Python大数据利器
算法·面试·github
uhakadotcom42 分钟前
实时计算Flink版:解锁数据处理新世界
后端·面试·github
Dontla43 分钟前
黑马node.js教程(nodejs教程)——AJAX-Day01-04.案例_地区查询——查询某个省某个城市所有地区(代码示例)
前端·ajax·node.js
uhakadotcom44 分钟前
Hologres实时数仓引擎:简化数据处理与分析
后端·面试·github
威哥爱编程44 分钟前
vue2和vue3的响应式原理有何不同?
前端·vue.js
呆呆的猫1 小时前
【前端】Vue3 + AntdVue + Ts + Vite4 + pnpm + Pinia 实战
前端