js里面的proxy理解。以及vue3响应式数据设计底层

请系统讲讲 Vue2 与 Vue3 的核心差异(响应式、API 设计、性能与编译器)。

一、 响应式系统:从"被动打补丁"到"原生降维打击"

这是两代框架最本质的鸿沟。Vue 2 是属性劫持 ,Vue 3 是对象代理

1. 技术方案对比

  • Vue 2 (Object.defineProperty)

    • 原理 :在组件初始化时,必须深度递归遍历 data 中的所有属性,将它们强行转为 getter/setter。
    • 致命缺陷 :无法监听对象动态新增、删除 的属性;无法完美监听数组索引和长度 的变化。因此被迫发明了 this.$set()this.$delete() 这种打补丁的 API。
  • Vue 3 (Proxy)

    • 原理 :直接代理整个外部对象,拦截包含 getsetdeleteProperty 在内的 13 种底层操作。
    • 降维打击 :天生支持动态增删属性、支持数组任意操作。并且是懒代理(Lazy) ------只有当你真正读取深层嵌套对象时,才会动态将其包装成 Proxy,初始化性能暴增。

2. 依赖收集模型差异

  • Vue 2 内部每个属性都有对应的 Dep 类,通过 Watcher 建立连接,闭包强引用较多。
  • Vue 3 彻底解耦,使用全局统一的 WeakMap (targetMap) →\to Map →\to Set 拓扑图谱管理依赖,弱引用对垃圾回收(GC)极度友好,杜绝了大部分内存泄漏。

二、 API 设计:从"巨型黑盒"到"积木解耦"

1. Options API(Vue 2)vs Composition API(Vue 3)

  • Vue 2 (Options API)

    • 代码被迫分散在 datamethodscomputedwatch 等固定的"选项型黑盒"中。
    • 痛点 :当一个组件超过 500 行时,修改一个业务逻辑需要鼠标上下疯狂翻滚;且组件间逻辑复用靠 mixins,极易发生命名冲突数据来源不明(黑盒隐患)
  • Vue 3 (Composition API / <script setup>)

    • 基于函数组合,将同一个业务功能的数据、方法、副作用完美聚集在一起。
    • 爽点 :逻辑复用直接提取为自定义 Hook(如 useUser),数据来源清晰,完美解决 Options API 的割裂感。

2. 类型系统(TypeScript)

  • Vue 2 的源码是基于 Flow 写的,对 TS 的支持属于"硬贴上去的壳子",大量的 this 上下文让 TS 根本无法正确推导类型。
  • Vue 3 整个源码完全用 TypeScript 重写,天生具备完美的类型推导与极致的 IDE 代码提示。

三、 性能与编译器:从"全量比对"到"精准靶向"

Vue 3 能够比 Vue 2 快数倍,核心功劳在于编译器(Compiler)变聪明了,它在编译时给运行时(Runtime)留下了大量的"作弊条"。

1. 静态提升 (Static Hoisting)

  • Vue 2 :无论模板里的 HTML 是静态的(比如 <h1>固定标题</h1>)还是动态的,每次组件刷新,都会全部重新创建虚拟 DOM 节点(VNode),浪费大量内存。
  • Vue 3 :编译器极其聪明,它会自动识别出那些永远不会变的静态节点,将其提升到渲染函数之外只创建一次。每次刷新时,直接复用同一个内存地址的 VNode。

2. 靶向更新:纯贴条机制 (PatchFlags)

  • Vue 2 :数据变了之后,虚拟 DOM 会进行全量 Diff(哪怕页面有一万个节点,只要有一个变了,两棵树就要挨个节点对比一遍)。
  • Vue 3 :在编译阶段,如果发现某个节点是动态的(比如 <span :class="cls">{{ text }}</span>),编译器就会在这个 VNode 后面打上一个数字标签(PatchFlag,比如代表动态文本和动态 class 的位掩码)。
  • Diff 时的降维打击 :Vue 3 在 Diff 时,直接跳过所有静态节点,顺着动态节点链表(Block Tree)进行靶向更新,并且结合位运算,精准知道这个节点是只有文本变了还是只有 class 变了。

3. 缓存事件处理函数 (Event Handler Cache)

  • Vue 2 中绑定的点击事件 @click="doSomething",每次渲染都会重新生成一个新的匿名函数。
  • Vue 3 默认开启事件缓存,把事件函数缓存起来。这样只要函数没变,对应的组件节点就被视为静态的,直接跳过 Diff。

🧱 第一部分:认识 Proxy 的本质(基础权限拦截)

核心原理: Proxy 本质上是对一个目标对象建立的"内存拦截层"。我们实例化 Proxy 时需要传入两个参数:一个是"目标对象"(target),另一个是"处理器对象"(handler)。

JavaScript

javascript 复制代码
// 1. 目标对象(Target / 明星):纯粹的数据载体,不自带任何防御逻辑
const rawUser = {
    name: "张三",
    role: "user",
    _passwordHash: "xyz123456", // 约定的私有属性
};

// 2. 处理器对象(Handler / 经纪人):在这里定义拦截规则(陷阱方法)
const securityHandler = {
    // 拦截 [[Get]] 操作:当外部尝试"读取"属性时触发
    get(target, property, receiver) {
        // 规则 1:如果是以下划线开头的属性,视为私有,拒绝访问
        if (property.startsWith('_')) {
            console.warn(`[警告]:无权访问私有属性 ${property}!`);
            return undefined; 
        }
        
        // 正常属性:记录日志并放行
        console.log(`[日志]:有人读取了属性【${property}】`);
        return Reflect.get(target, property, receiver); // 推荐使用 Reflect 底层转发
    },

    // 拦截 [[Set]] 操作:当外部尝试"写入/修改"属性时触发
    set(target, property, value, receiver) {
        // 规则 2:权限校验,防止越权修改角色
        if (property === 'role' && value === 'admin') {
            console.error(`[拒绝]:普通用户不能自己升级为 admin!`);
            return false; // 严格模式下会抛出 TypeError,明确告知修改失败
        }

        // 正常修改:记录日志并放行
        console.log(`[日志]:属性【${property}】被修改为了【${value}】`);
        return Reflect.set(target, property, value, receiver); // 必须返回 true 代表成功
    }
};

// 3. 实例化代理对象(完全接管对原对象的访问)
const protectedUser = new Proxy(rawUser, securityHandler);

🚨 基础拦截拦截测试

JavaScript

ini 复制代码
// --- 场景 A:正常读取 ---
console.log(protectedUser.name); 
// 输出: [日志]:有人读取了属性【name】 -> 张三

// --- 场景 B:试图窥探隐私(被拦截) ---
console.log(protectedUser._passwordHash); 
// 输出: [警告]:无权访问私有属性 _passwordHash! -> undefined

// --- 场景 C:正常修改数据 ---
protectedUser.name = "李四"; 
// 输出: [日志]:属性【name】被修改为了【李四】

// --- 场景 D:试图进行非法越权操作(被拦截) ---
protectedUser.role = "admin"; 
// 输出: [拒绝]:普通用户不能自己升级为 admin!

🚀 第二部分:进阶应用(Vue 3 响应式系统模拟)

基于 Proxy 的拦截能力,我们可以无缝升级为 Vue 3 的依赖收集与更新机制 。你的理解非常准确:Proxy 是负责抓现场的"哨兵",而 effect 是告诉浏览器哪些 DOM 绑了数据的"登记处"。

1. 全局状态与依赖大仓库

JavaScript

javascript 复制代码
// 临时存储当前正在执行的副作用函数(如当前正在渲染的组件)
let activeEffect = null;

// 全局依赖大仓库(结构:target -> key -> deps)
const targetMap = new WeakMap();

2. 双剑合璧:依赖收集 (Track) 与 派发更新 (Trigger)

JavaScript

ini 复制代码
/**
 * 依赖收集:在读取属性时,把当前的 activeEffect 存入对应的属性桶中
 */
function track(target, key) {
    if (!activeEffect) return; // 如果不是在 effect 环境下读取的数据,直接放行

    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);
    }

    dep.add(activeEffect); // 把当前副作用函数收集进桶里(Set 自动去重)
    console.log(`[依赖收集] 成功收集依赖 -> 属性【${key}】依赖了当前 effect`);
}

/**
 * 派发更新:在修改属性时,把属性桶里的所有 effect 拿出来重新执行
 */
function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;

    const dep = depsMap.get(key);
    if (dep) {
        console.log(`\n[派发更新] 属性【${key}】发生改变,开始通知依赖项更新...`);
        dep.forEach(effect => effect()); // 挨个执行登记过的副作用函数
    }
}

3. 响应式入口与副作用包装(reactive / effect)

JavaScript

javascript 复制代码
/**
 * 将普通对象包装为基于 Proxy 的响应式对象
 */
function reactive(target) {
    if (typeof target !== 'object' || target === null) return target;

    return new Proxy(target, {
        // 拦截读取 -> 触发依赖收集
        get(target, key, receiver) {
            track(target, key);
            const res = Reflect.get(target, key, receiver);
            
            // 【深层响应式优化】惰性代理:只有在读取到深层对象时,才实时将其转为 Proxy
            if (typeof res === 'object' && res !== null) {
                return reactive(res); 
            }
            return res;
        },

        // 拦截修改 -> 触发派发更新
        set(target, key, value, receiver) {
            const oldValue = target[key];
            const result = Reflect.set(target, key, value, receiver);

            // 只有当新旧值发生真实改变时,才触发更新,避免无意义的重复渲染
            if (oldValue !== value) {
                trigger(target, key);
            }
            return result;
        }
    });
}

/**
 * 副作用包装器:让传入的函数具备响应式追踪能力
 */
function effect(fn) {
    const effectFn = () => {
        try {
            activeEffect = effectFn; // 1. 将自己挂载到全局变量上
            return fn();             // 2. 执行函数,触发内部 reactive 数据的 get -> track
        } finally {
            activeEffect = null;     // 3. 执行完毕后洗白,释放全局指针
        }
    };
    effectFn(); // 默认立即执行一次,进行初次渲染和依赖收集
    return effectFn;
}

🧪 第三部分:响应式联动测试

JavaScript

javascript 复制代码
// ---- 测试数据 ----
const user = reactive({
    name: "张三",
    age: 18,
    info: { city: "北京" } // 测试深层代理
});

// ---- 模拟 Vue 的组件渲染 (Effect 1) ----
effect(() => {
    console.log(`【视图更新 🎬】界面渲染 -> 姓名: ${user.name}, 年龄: ${user.age}`);
});

// ---- 模拟另一个 Watch 监听器 (Effect 2) ----
effect(() => {
    console.log(`【日志监控 📝】城市变成了: ${user.info.city}`);
});

console.log("\n--- 初始化收集完毕,开始修改数据 ---\n");

// 修改测试 1:修改普通属性(触发 Effect 1)
user.age = 19; 

// 修改测试 2:修改深层嵌套属性(触发 Effect 2)
user.info.city = "上海";
相关推荐
sunrains2 小时前
uniapp x 动态Tabbar(切换无闪烁)+动角标+主题切换+自定义tabbar页面导航栏样式设置 支持服务端动态配置根据角色动态设置Tabbar
前端
把马铃薯变成土豆2 小时前
前端Stripe跨境支付对接感想
前端·源码
阿黎梨梨2 小时前
AI Loop:告别“人肉写提示词”,让代码替你“鞭策”AI
javascript·人工智能
牧艺2 小时前
用 Three.js 实现一个浏览器端 3D 看车的项目
前端·three.js
hunterandroid2 小时前
WorkManager:可靠的后台任务调度
前端
hunterandroid2 小时前
[Android 从零到一] Navigation Component:让页面跳转更清晰
前端
搬砖的码农2 小时前
(05)进程一关对话就没了:聊天记录怎么存、重启怎么恢复
前端·agent·ai编程
Csvn3 小时前
Vue 3 defineModel 翻车实录:多个 v-model 绑定到底怎么写?
前端·vue.js
甲维斯3 小时前
坦克大战测试全翻车了!豆包,DeepSeek,Qwen,GPT,Claude
前端·人工智能·游戏开发