news 2026/6/15 11:59:07

JavaScript进阶:从面向对象到Vue3响应式原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript进阶:从面向对象到Vue3响应式原理

前言

本质上,Vue3的响应式系统就是对JS对象特性的深度应用

理解对象 → 理解属性描述符 → 理解如何拦截属性 ↓ Object.defineProperty(Vue2方案) ↓ Proxy + Reflect(Vue3方案) ↓ 依赖收集 + 自动更新 = 响应式系统

一、深入理解JS对象

1.1 属性描述符

var obj = { name: "why", age: 18 } ​ // 数据属性描述符 Object.defineProperty(obj, "address", { value: "北京市", // 属性值 configurable: false, // 是否可删除/可重新配置 enumerable: true, // 是否可枚举(for...in 能否遍历到) writable: false // 是否可写入 }) ​ delete obj.address // configurable:false,无法删除 obj.address = "上海市" // writable:false,无法修改

关键:描述符定义的属性,configurable/enumerable/writable默认全是false。字面量定义的属性默认全是true

1.2 存取属性描述符——拦截属性的读写

这是连接"面向对象"和"响应式"的第一个桥梁

var obj = { name: "why", _address: "北京市" } ​ Object.defineProperty(obj, "address", { enumerable: true, configurable: true, get: function() { console.log("获取了一次address的值") return this._address }, set: function(value) { console.log("设置了address的值,新值:" + value) this._address = value } }) ​ console.log(obj.address) // 触发 get obj.address = "上海市" // 触发 set

1.3 限制对象

var obj = { name: 'why', age: 18 } ​ // Level 1:禁止扩展(不能添加新属性) Object.preventExtensions(obj) ​ // Level 2:密封对象(不能添加 + 不能删除) Object.seal(obj) ​ // Level 3:冻结对象(不能添加 + 不能删除 + 不能修改) Object.freeze(obj)

理解这些方法对后续理解 Vue 的readonly/shallowReadonly很有帮助。


二、监听对象变化

2.1 Vue2方案:Object.defineProperty

遍历所有 key,给每个属性加get/set

const obj = { name: "why", age: 18 } ​ Object.keys(obj).forEach(key => { let value = obj[key] ​ Object.defineProperty(obj, key, { get: function() { console.log(`监听到obj对象的${key}属性被访问了`) return value }, set: function(newValue) { console.log(`监听到obj对象的${key}属性被设置值`) value = newValue } }) }) ​ obj.name = "kobe" // 触发 set obj.age = 30 // 触发 set obj.height = 1.88 // 新增属性,无法监听

Vue2方案的缺点

  • 无法监听新增属性(所以需要Vue.set/this.$set

  • 无法监听删除属性(所以需要Vue.delete

  • 必须遍历每个 key

2.2 Vue3方案:Proxy

Proxy可以代理整个对象,而不是逐个属性:

const obj = { name: "why", age: 18 } ​ const objProxy = new Proxy(obj, { get: function(target, key) { console.log(`监听到对象的${key}属性被访问了`, target) return target[key] }, set: function(target, key, newValue) { console.log(`监听到对象的${key}属性被设置值`, target) target[key] = newValue } }) ​ objProxy.name = "kobe" // 触发 set objProxy.height = 1.88 // 新增属性也能监听!

Proxy 的捕获器

const objProxy = new Proxy(obj, { get: function(target, key) { // 拦截读取:obj.key console.log(`${key}属性被访问`) return target[key] }, set: function(target, key, newValue) { // 拦截设置:obj.key = val target[key] = newValue }, has: function(target, key) { // 拦截 in 操作符:key in obj console.log(`${key}的in操作被拦截`) return key in target }, deleteProperty: function(target, key) { // 拦截 delete 操作:delete obj.key console.log(`${key}的delete操作被拦截`) delete target[key] } }) ​ "name" in objProxy // 触发 has delete objProxy.name // 触发 deleteProperty

2.3 Reflect

Reflect提供了与Proxy捕获器一一对应的静态方法:

const objProxy = new Proxy(obj, { get: function(target, key, receiver) { console.log("get---------") return Reflect.get(target, key) // 替代 target[key] }, set: function(target, key, newValue, receiver) { console.log("set---------") const result = Reflect.set(target, key, newValue) // 有返回值,表示成功/失败 if (result) { // 设置成功 } else { // 设置失败 } } })

为什么用 Reflect 而不是直接操作 target?

  1. Reflect.set返回值(boolean),可以判断操作是否成功

  2. Reflect的方法与Proxy捕获器一一对应,代码更一致

  3. 配合receiver参数可以正确处理 this 指向


三、手写响应式系统

有了Proxy+Reflect,我们就可以开始构建一个完整的响应式系统了。

3.1 响应式函数的手动触发

把需要重新执行的函数收集起来,数据变化时统一执行。

// 封装一个响应式函数 let reactiveFns = [] function watchFn(fn) { reactiveFns.push(fn) } ​ const obj = { name: "why", age: 18 } ​ watchFn(function() { console.log(obj.name) // 依赖了 obj.name }) ​ watchFn(function() { console.log(obj.name, "demo function") }) ​ // 数据变了,手动触发 obj.name = "kobe" reactiveFns.forEach(fn => fn()) // 手动通知

问题很明显:每次都要手动调用reactiveFns.forEach

3.2 把函数收集封装成 Depend 类

class Depend { constructor() { this.reactiveFns = [] } ​ addDepend(reactiveFn) { this.reactiveFns.push(reactiveFn) } ​ notify() { this.reactiveFns.forEach(fn => fn()) } } ​ const depend = new Depend() function watchFn(fn) { depend.addDepend(fn) }

有了这个类,我们需要解决的问题变成:如何让 Proxy 在 set 的时候自动调用 depend.notify()?

3.3 Proxy 自动通知

在 Proxy 的set中调用depend.notify()

const objProxy = new Proxy(obj, { get: function(target, key, receiver) { return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) depend.notify() // 自动通知 } }) ​ objProxy.name = "kobe" // 自动触发所有依赖函数

但这有一个问题:所有属性共用同一个depend对象。修改name会触发age的依赖函数也执行

3.4 每个属性独立的 Depend

核心数据结构:WeakMap → Map → Depend

// WeakMap<target, Map<key, Depend>> const targetMap = new WeakMap() ​ function getDepend(target, key) { // 根据 target 获取 Map let map = targetMap.get(target) if (!map) { map = new Map() targetMap.set(target, map) } ​ // 根据 key 获取 Depend let depend = map.get(key) if (!depend) { depend = new Depend() map.set(key, depend) } return depend }

数据结构图

targetMap (WeakMap) ├── obj → Map { "name" → Depend1, "age" → Depend2 } └── info → Map { "address" → Depend3 }

为什么用 WeakMap?当目标对象被垃圾回收时,WeakMap 中对应的 key 也会被自动回收,不会造成内存泄漏。

改造 Proxy:

const objProxy = new Proxy(obj, { get: function(target, key, receiver) { return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) const depend = getDepend(target, key) // 精确获取该 key 的 Depend depend.notify() // 只通知关心这个 key 的函数 } }) ​ objProxy.name = "kobe" // 只触发 name 的依赖函数 objProxy.age = 100 // 只触发 age 的依赖函数

3.5 自动收集依赖

目前还有一个问题:依赖(哪些函数依赖了哪些属性)还是我们手动watchFn添加的

真正需要的是:当函数执行时,如果它访问了objProxy.name,就自动把这个函数收集为name的依赖。

核心思路:利用 Proxy 的get捕获器来自动收集

let activeReactiveFn = null ​ function watchFn(fn) { activeReactiveFn = fn // 记录当前正在执行的响应式函数 fn() // 执行函数 → 访问属性 → 触发 get → 收集依赖 activeReactiveFn = null } ​ // Proxy get 中自动收集 const objProxy = new Proxy(obj, { get: function(target, key, receiver) { const depend = getDepend(target, key) depend.addDepend(activeReactiveFn) // 自动收集 return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) const depend = getDepend(target, key) depend.notify() } })

流程

watchFn(fn) → activeReactiveFn = fn → fn() 执行 → 访问 objProxy.name → 触发 get 捕获器 → depend.addDepend(fn) ← 自动收集 → activeReactiveFn = null

3.6 优化 Depend 类

使用Set代替数组(去重)、封装depend()方法:

let activeReactiveFn = null ​ class Depend { constructor() { this.reactiveFns = new Set() // Set 自动去重 } ​ depend() { if (activeReactiveFn) { this.reactiveFns.add(activeReactiveFn) // 自动收集 } } ​ notify() { this.reactiveFns.forEach(fn => fn()) } }

3.7 封装成reactive()函数

将所有逻辑封装,最终得到完整响应式实现

// ============ Vue3 响应式核心 ============ ​ let activeReactiveFn = null ​ class Depend { constructor() { this.reactiveFns = new Set() } ​ depend() { if (activeReactiveFn) { this.reactiveFns.add(activeReactiveFn) } } ​ notify() { this.reactiveFns.forEach(fn => fn()) } } ​ // 响应式函数 function watchFn(fn) { activeReactiveFn = fn fn() activeReactiveFn = null } ​ // 依赖管理:WeakMap<target, Map<key, Depend>> const targetMap = new WeakMap() function getDepend(target, key) { let map = targetMap.get(target) if (!map) { map = new Map() targetMap.set(target, map) } ​ let depend = map.get(key) if (!depend) { depend = new Depend() map.set(key, depend) } return depend } ​ // 核心:将普通对象变成响应式对象 function reactive(obj) { return new Proxy(obj, { get: function(target, key, receiver) { const depend = getDepend(target, key) depend.depend() // 自动收集依赖 return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) const depend = getDepend(target, key) depend.notify() // 自动通知更新 } }) } ​ // ============ 使用示例 ============ ​ const objProxy = reactive({ name: 'why', age: 18 }) const infoProxy = reactive({ address: '广州市', height: 1.88 }) ​ watchFn(() => { console.log(infoProxy.address) }) ​ infoProxy.address = '北京市' // 自动触发上面的回调! ​ const foo = reactive({ name: 'foo' }) ​ watchFn(() => { console.log(foo.name) }) ​ foo.name = 'bar' // 自动触发回调 foo.name = 'hhh' // 自动触发回调

四、Vue2 的写法对比

同样的依赖收集逻辑,Vue2 用Object.defineProperty实现:

function reactive(obj) { Object.keys(obj).forEach(key => { let value = obj[key] Object.defineProperty(obj, key, { get: function() { const depend = getDepend(obj, key) depend.depend() return value }, set: function(newValue) { value = newValue const depend = getDepend(obj, key) depend.notify() } }) }) return obj }

两者的核心区别

Vue2 (defineProperty)Vue3 (Proxy)
拦截对象逐个属性整个对象
新增属性无法监听可以监听
删除属性无法监听可以监听
数组索引/长度有问题支持
性能初始化时遍历所有 key按需拦截
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/15 11:50:51

抖音批量下载工具:一键获取无水印视频的终极解决方案

抖音批量下载工具&#xff1a;一键获取无水印视频的终极解决方案 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback suppor…

作者头像 李华
网站建设 2026/6/15 11:50:51

MTKClient终极指南:三步解锁联发科设备完整控制权

MTKClient终极指南&#xff1a;三步解锁联发科设备完整控制权 【免费下载链接】mtkclient MTK reverse engineering and flash tool 项目地址: https://gitcode.com/gh_mirrors/mt/mtkclient 你是否曾面对变砖的联发科手机束手无策&#xff1f;是否想要备份手机数据却苦…

作者头像 李华
网站建设 2026/6/15 11:45:51

革命性SQLite无界可视化:基于WebAssembly的客户端沙箱数据库查看器

革命性SQLite无界可视化&#xff1a;基于WebAssembly的客户端沙箱数据库查看器 【免费下载链接】sqlite-viewer View SQLite file online 项目地址: https://gitcode.com/gh_mirrors/sq/sqlite-viewer 在数据驱动的现代开发环境中&#xff0c;SQLite数据库已成为移动应用…

作者头像 李华
网站建设 2026/6/15 11:43:52

大语言模型中的概念表示:从线性几何到符号推理

1. 大语言模型中的概念表示&#xff1a;从线性几何到符号推理在自然语言处理领域&#xff0c;大语言模型(LLMs)展现出了惊人的概念理解和逻辑推理能力&#xff0c;这种能力传统上被认为是符号AI的专属领域。然而&#xff0c;这些模型如何在连续的嵌入空间中编码离散的概念知识&…

作者头像 李华
网站建设 2026/6/15 11:43:51

从 Adapter Engine 到 SAP Process Orchestration 7.5,一条 Java 化集成架构的演进线

很多老 SAP 项目里,PI 和 PO 这两个名字经常被混着叫。现场讨论接口问题时,有人说 PI,有人说 PO,有人说 AAE,有人说 AEX,还有人一开口就是 dual stack。真正把这条线理顺之后,会发现 SAP Process Integration 7.5 并不是一个孤立版本,而是 SAP 集成平台从 ABAP 中心化管…

作者头像 李华