news 2026/10/17 7:27:14

Vue响应式原理深度剖析:从Vue2的defineProperty到Vue3的Proxy

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue响应式原理深度剖析:从Vue2的defineProperty到Vue3的Proxy

1. Vue响应式原理:数据是怎么学会“自动更新”的

做前端的人,几乎没有人能绕开Vue响应式这个坎。不管是面试被问到“说说Vue2和Vue3的响应式有什么区别”,还是自己在项目里遇到“数据改了页面死活不更新”的玄学Bug,归根结底都在考同一个东西——你到底是真懂响应式,还是只背了几个名词。

我一直认为,搞懂响应式原理最好的方式不是看十篇源码解析,而是自己动手把整个链路走通一遍:数据改了,视图怎么知道要更新;视图更新了,又是怎么精确找到那个依赖它的渲染函数的。这两个问题想明白了,Vue2和Vue3的区别,其实不需要背,自然就长在脑子里了。

先说说我的结论,方便你带着主线往下看:Vue2的响应式核心是Object.defineProperty,Vue3的响应式核心是Proxy。这个区别衍生出了一连串的能力差异:能不能监听新增属性、能不能监听数组索引、初始化的时候会不会一次性递归到底、性能瓶颈出在哪里……每一个特性,都跟这个底层选型直接相关。

1.1 Vue2的响应式基石:Object.defineProperty

Vue2在初始化data的时候,会遍历data中的所有属性,用Object.defineProperty把它们全部转换成getter/setter。你平时在组件里写:

data() { return { name: '张三', info: { age: 18 } } }

Vue会把这个对象变成类似这样的结构:

function defineReactive(obj, key, val) { // 递归处理嵌套对象 observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 收集依赖 return val }, set(newVal) { if (newVal === val) return val = newVal // 触发依赖 dep.notify() } }) }

这个过程中的关键角色有三个:

  • Dep(依赖收集器):每个响应式属性都对应一个Dep实例,它维护着一个Watcher列表。
  • Watcher(订阅者):组件渲染函数、computed、watch,都会被包装成Watcher。它读取数据时把自己添加到依赖中。
  • 依赖收集的过程:渲染函数执行时,访问到某个属性,触发getter,此时当前正在运行的Watcher就会被收集到这个属性的Dep里。以后这个属性setter被触发,Dep就通知所有Watcher更新。

用一个生活化的类比:Dep就是一个班级点名册,Watcher就是班上的学生。渲染函数点名念到某个属性,这个属性的点名册上就记下这个学生。以后属性值变了,点名册就会喊所有相关学生“过来重新算一下”。

这里有一个绕不开的缺陷,也是Vue2被吐槽最多的点:Object.defineProperty是针对“某个对象的某个属性”做拦截的,所以:

  • 新增一个属性(this.obj.newProp = 1),这个新属性根本没有被defineProperty处理过,不响应。
  • 删除一个属性(delete this.obj.prop),没有对应的拦截机制,不响应。
  • 通过索引修改数组(this.arr[0] = 1),同理不响应。
  • 数组的push、pop、shift等方法,需要Vue自己改写数组原型来“补课”,才能触发更新。

Vue2官方给出的解决方案是:新增属性用Vue.set(或this.$set),删除属性用Vue.delete,数组索引操作就老老实实换成splice。这些方案都能解决问题,但本质上是“打补丁”,说明底层方案本身是存在边界的。

1.2 Vue3的响应式升级:Proxy代理

Vue3直接把整个对象用Proxy包了一层,拦截的是对象层面的操作,而不是某个具体属性。这意味着:不管你是新增属性、删除属性、读取属性,还是用任意下标访问数组,都会被Proxy的处理器捕获。

Vue3内部实现响应式的大致逻辑是这样的:

function reactive(target) { // 如果已经代理过,直接返回 if (target[ReactiveFlags.IS_REACTIVE]) return target return new Proxy(target, { get(target, key, receiver) { // 读取属性时收集依赖 track(target, key) // 注意这里用 Reflect.get,保证 this 指向正确 const res = Reflect.get(target, key, receiver) // 懒递归:用到了才去深层代理 if (isObject(res)) { return reactive(res) } return res }, set(target, key, value, receiver) { const oldValue = target[key] const result = Reflect.set(target, key, value, receiver) if (hasChanged(oldValue, value)) { // 修改属性时触发依赖 trigger(target, key) } return result }, deleteProperty(target, key) { const hasKey = hasOwn(target, key) const result = Reflect.deleteProperty(target, key) if (hasKey && result) { trigger(target, key) } return result } }) }

这里有几个反直觉但非常关键的设计:

第一是惰性递归。Vue2在初始化时就会递归遍历整个data对象,无论层级多深,一次性全部转成响应式。这也是Vue2初始化大对象时卡顿的主要原因。Vue3不一样,它是在get的时候才判断当前值是不是一个对象,如果是才会去reactive。也就是说,一个深层对象,在你不访问它的某个子级属性之前,它的那一层可能根本没有被代理过。这大大降低了初始化成本。

第二是Reflect.get/Reflect.set。很多人不理解为什么用Proxy拦截了操作,还要再调用Reflect。核心原因在于,当你用receiver参数时,可以保证对象内部方法或getter中this指向的是代理对象本身,而不是原始对象。

const obj = { get count() { return this.a + this.b } }

如果直接用target[key],可能会导致this指向原始对象,而原始对象不具备响应式能力,从而丢失依赖收集。这是Proxy和defineProperty处理getter时一个本质区别,Vue3用Reflect来规避了这个问题。

第三是数组的天然友好。Vue2需要改写七个数组方法来实现响应式,而且arr[0] = xx的操作无法拦截。Vue3用Proxy直接拦截数组索引操作,arr[0] = 99、arr.length = 0、新增元素、删除元素,全部都能触发更新。因为数组和对象的读取、操作,在Proxy层面都被统一成get/set/deleteProperty等几类处理。

1.3 设计差异带来的能力边界

用一张表把两个版本的底层能力对标,会非常直观:

场景Vue2(Object.defineProperty)Vue3(Proxy)
新增对象属性不支持,需要$set补丁原生支持
删除对象属性不支持,需要$delete补丁原生支持
通过索引修改数组不支持原生支持
数组length修改不支持原生支持
监听嵌套对象初始化时一次性递归get时惰性递归
Map/Set/WeakMap等集合类型不支持原生支持
浏览器兼容性IE8+(实际上IE8有坑)不支持IE,现代浏览器OK

注意一个细节:Vue3的reactive并不是对任何类型都能代理的。如果传入的是一个基本类型(string、number、boolean),Proxy本身就无法直接处理,需要靠ref包一层。这也是为什么Vue3引入了ref,本质上是为了补齐Proxy不能代理基础类型的短板。很多人以为ref是Composition API的“语法糖”,其实它是reactive方案的一个必要补充。

2. Vue2和Vue3的区别不止响应式

很多教程讲Vue2和Vue3的区别,都在说响应式。但我在实际开发中的体感是:响应式只是冰山一角。真正让Vue3跟Vue2拉开代差的是模板编译优化、组合式API、以及工程化方式的彻底升级。如果你只盯响应式,面试可能能过,但项目里用起来还是会一脸懵。

2.1 模板编译与渲染性能的进化

Vue2的虚拟DOM在render阶段,会创建一棵完整的虚拟DOM树,然后diff时也不知道哪些节点是静态的、哪些是动态的,只能从根节点一路比较到叶子节点。组件越大,这个diff的浪费就越明显。

Vue3在编译器层面做了很大的优化,核心思路是“编译时标记动态节点,运行时只比较动态部分”。

来看这段模板:

<div> <span>用户名:</span> <span>{{ userName }}</span> </div>

Vue3编译后会生成类似这样的render函数:

return (_openBlock(), _createElementBlock("div", null, [ _createElementVNode("span", null, "用户名:", -1 /* HOISTED */), _createElementVNode("span", null, _toDisplayString(_ctx.userName), 1 /* TEXT */) ]))

注意两个细节:

  • 第一个span的useState是-1,表示这是静态提升的节点,整个节点对象被创建一次就不再变化,diff时完全跳过。
  • 第二个span的useState是1,也就是PatchFlags.TEXT,表示这个节点只有文本可能变化。diff时如果节点类型没变、flag是TEXT,就直接比较并更新文本,不再深入子节点。

这就是Vue3所谓的Block Tree + PatchFlags优化。以静态节点为锚点,把动态节点单独收集起来。diff的时候直接在这条动态节点数组上遍历,性能跟组件内动态节点的数量成正比,而不是跟整个模板的大小成正比。典型场景下,一个很大的列表页,只有少量数据在变,Vue3的diff开销会小很多。

除了patch flag,Vue3还做了事件缓存。Vue2里每次render都会重新创建内联事件函数(比如@click="() => fn()"),Vue3会检测到内联函数并把它缓存起来,下次render直接复用同一个函数引用。避免了子组件因为父组件每次render都传入一个新函数引用而被迫更新。这一点在实际项目中很管用,特别是配合子组件使用memo/纯函数组件时,父组件只要没改逻辑,子组件就不会因为事件引用变化而重新渲染。

2.2 Composition API重构了代码组织方式

Vue2的Options API把一块业务逻辑拆散到data、methods、computed、watch、生命周期钩子这几个固定位置。组件小的时候问题不大,组件一旦复杂,你会发现同一个功能的数据、方法、计算属性、监听器被拆到了四五个地方。想看懂一个功能的完整实现,要在组件文件里上下翻好几轮。

我在一个中后台项目里经历过这种痛:一个表格页面,筛选逻辑、分页逻辑、导出逻辑、权限逻辑混在一起,每个逻辑都分散在各处。后来用Composition API重写,把筛选逻辑放在一个useFilter函数里,分页逻辑放usePagination,导出放useExport,每个函数内部自己管理响应式状态和生命周期,组件主体只剩几行调用代码。这种“按功能组织”的方式,比“按选项类型组织”更适合复杂业务。

一个最直观的对比:

// Vue2 Options API:同一份筛选逻辑被拆散 export default { data() { return { keyword: '', page: 1, list: [] } }, computed: { filteredList() { /* 依赖 keyword 和 list */ } }, watch: { keyword() { this.page = 1 } }, methods: { fetchList() { /* 请求列表 */ } }, mounted() { this.fetchList() } }
// Vue3 Composition API:筛选逻辑完整封装 function useFilter() { const keyword = ref('') const page = ref(1) const list = ref([]) const filteredList = computed(() => { /* ... */ }) watch(keyword, () => { page.value = 1 }) async function fetchList() { /* ... */ } onMounted(fetchList) return { keyword, page, list, filteredList, fetchList } }

Composition API最大的价值不是“少写几行代码”,而是让逻辑可以被真正地提取、复用、测试。Vue2也有mixin,但mixin的命名冲突、数据来源不明、隐式依赖等坑,深入用过的人都有体会。Composition API本质上是把“逻辑”作为一等公民,可以像函数一样自由组合。

2.3 生命周期与常用API的细节变化

生命周期是API层面最容易踩坑的地方。Vue3把beforeDestroy改成了beforeUnmount,destroyed改成了unmounted,语义更明确。组合式API里则对应的是onBeforeUnmount和onUnmounted。

还有一个容易被忽略的变化:Vue3中created生命周期钩子不再需要显式使用了,因为setup阶段就相当于created,变量声明、函数定义、请求初始化都可以直接写在setup里。

几个常用API的变化清单:

功能Vue2Vue3
全局配置Vue.use / Vue.componentcreateApp().use / component
全局混入Vue.mixinapp.mixin
v-model默认使用value + input事件默认使用modelValue + update:modelValue
异步组件() => import() 配合 componentdefineAsyncComponent 包裹
过滤器filter全局注册,模板里可以用移除,推荐使用computed或方法
插槽$slots.x / scopedSlots$slots都合并了,v-slot统一
key无特殊要求需要放在template上时用v-bind的方式

v-model那个变化特别重要。Vue2里一个组件只能有一个v-model,因为绑定的是value和input。Vue3可以给一个组件写多个v-model,比如v-model:title、v-model:content,每个都对应一个modelValue的变体。这在封装表单组件时非常香,不用再绞尽脑汁想sync修饰符那套了。

2.4 工程化与生态层面的变化

Vue3的标配构建工具是Vite,这个选择背后其实是“原生ESM + 按需编译”的思路。开发阶段,Vite启动时不需要像Webpack那样先打包整个项目,而是让浏览器直接请求ES模块,所以冷启动速度和热更新速度都有明显提升。大项目中,Vite的热更新基本是“秒级”,但Webpack可能在十秒以上甚至几十秒。

有个容易被忽略的点:Vue3对TypeScript的支持是底层重写的,类型推断能力比Vue2+TS的组合强了很多。你写ref、reactive、computed时,TS可以自动推导出类型,不需要像Vue2那样到处手动声明接口。这对于中大型团队的长期维护很关键。

另外,Vue2的全局API挂载在一个静态的Vue对象上,同一个页面同时存在多个Vue应用实例时,全局配置是互相污染的。Vue3用createApp创建应用实例,每个app拥有自己独立的全局配置、组件注册、指令注册。如果你做过微前端或页面嵌入,应该能体会这个变化有多舒服。

3. 手写一个迷你响应式系统:从零彻底理解原理

前面讲了那么多概念,如果不动手写一遍,始终是“纸上谈兵”。我建议你把源码部分拆掉,只需要保留三个核心:Dep(依赖收集器)、effect(Watcher)、reactive(Proxy代理)。然后照着下面的思路自己实现一遍,整个响应式链路就通了。

3.1 依赖收集器Dep怎么设计

Dep的作用很简单:记住“哪些函数在读取这个属性”,并在属性变化时通知它们重新执行。我用一个类来承载:

class Dep { constructor() { this.subscribers = new Set() } depend() { if (activeEffect) { this.subscribers.add(activeEffect) } } notify() { this.subscribers.forEach(effect => effect()) } }

这里最难理解的是activeEffect。它表示“当前正在运行的副作用函数”。在全局建立一个栈来管理:

let activeEffect = null const effectStack = [] function effect(fn) { const wrappedEffect = () => { try { // 入栈,保证嵌套 effect 时外层 effect 不丢失 effectStack.push(wrappedEffect) activeEffect = wrappedEffect fn() } finally { effectStack.pop() activeEffect = effectStack[effectStack.length - 1] || null } } wrappedEffect() return wrappedEffect }

这里用栈而不是单个变量,是为了处理嵌套场景,比如一个effect里读取了computed,computed内部又读取了别的属性。如果只有一个activeEffect变量,内层执行完,外层的effect就丢了。

3.2 订阅者(effect)怎么和Dep联动

上面有了Dep和effect,还差一步:读取属性的时候,怎么让Dep知道“当前在跑哪个effect”。这一步就要靠reactive中的get拦截了。

接下来实现reactive:

function reactive(target) { return new Proxy(target, { get(target, key, receiver) { const dep = getDep(target, key) // 收集依赖 dep.depend() return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver) const dep = getDep(target, key) // 通知更新 dep.notify() return result } }) }

需要一个全局的WeakMap来存储每个对象的每个属性对应的Dep。为什么用WeakMap?因为WeakMap的key是弱引用,当原始对象不再被使用时,内存可以被回收。Vue3源码里也是这个思路(甚至在模块设计上还更进一步,用了targetMap -> key -> dep的三层结构):

const targetMap = new WeakMap() function getDep(target, key) { let depsMap = targetMap.get(target) if (!depsMap) { depsMap = new Map() targetMap.set(target, depsMap) } let dep = depsMap.get(key) if (!dep) { dep = new Dep() depsMap.set(key, dep) } return dep }

3.3 用Proxy实现reactive并打通闭环

到这里,整个迷你响应式系统就能跑起来了:

const state = reactive({ count: 0 }) effect(() => { console.log(`count is: ${state.count}`) }) state.count++ // 控制台输出:count is: 1

你看到的现象是:effect里的函数读取了state.count,然后state.count++,函数自动重新执行,打印了新值。这就是Vue组件更新的最小模型。

再进一步,把computed也实现一下。computed本质上是一个“带缓存的effect”:

function computed(getter) { let value let dirty = true const runner = effect(() => { value = getter() dirty = false }) return { get value() { if (dirty) { runner() } return value } } }

当computed依赖的数据变化时,脏标记dirty被重置为true,下一次访问value时重新计算。这不只是一个玩具,Vue3的computed原理跟这是同一个套路:惰性求值 + 依赖追踪 + 脏标记缓存。

3.4 这个迷你系统有哪些局限性

写到这里,肯定有人要去对照Vue3源码,说“你怎么没处理Set、Map、Reflect的receiver边界、还有数组的特殊方法”。确实,这个迷你版本做了很多简化:

  • 没有处理集合类型(Set/Map)的响应式。
  • 没有处理数组的length、for...in等特殊操作。
  • 没有处理receiver和Reflect.get的边界情况。
  • 没有实现ref、shallowRef、watchEffect等工具函数。

但我觉得这恰恰是最好的学习方法:你先把主干跑通,再去看源码帧测。主干逻辑无误,源码中的那些边界处理就是在主干上打补丁而已。而且,自己写过一遍之后,再去读Vue3源码的core部分,你会发现那些看似神秘的模块,其实没有那么高不可攀。

4. 实战排坑:Vue2和Vue3响应式常见问题实录

原理讲得再多,最终还是要落到“能不能解决问题”。我把这些年实际开发中碰到的响应式相关坑,按Vue2和Vue3分别整理出来,附上排查思路和解决方案,你可以直接收藏备查。

4.1 Vue2三大经典响应式陷阱

第一个坑是新增属性不响应。某次我给一个对象动态加了个字段,页面死活不更新,检查了所有变量、计算属性,最后发现根源就是this.obj.newProp = 1在Vue2的响应式体系下根本无效。解决方案是改用this.$set(this.obj, 'newProp', 1),或者在data初始化时就把这个字段提前占位。这里有个很实用的建议:Vue2的data里要把会用到的动态字段全部提前声明,哪怕初始值是undefined,也要先占个位置。

第二个坑是数组索引修改不响应。我在Vue2项目里写过这样的代码:this.list[0] = newItem,结果视图毫无反应。当时第一反应是“Vue是不是出bug了”,后来才意识到这属于defineProperty无法拦截的范畴。正确的做法是this.list.splice(0, 1, newItem),或者用this.$set(this.list, 0, newItem)。这个坑在表格、列表类的业务场景里非常常见,因为很多时候你是直接按索引去改某一行数据。

第三个坑是初始化大对象时卡顿。Vue2在初始化data时,会对所有属性递归执行defineReactive。如果一个对象层级非常深、数据量非常大(比如一个几千行的大列表,每行还有很多嵌套字段),初始化时的性能消耗可能是秒级的。Vue2官方对这个问题也没有特别完美的解,只能建议:尽量扁平化数据结构、减少深嵌套;大列表考虑虚拟滚动;不要塞太多无关数据到data里。这也是Vue3用惰性递归代理的重要原因之一。

4.2 Vue3的响应式陷阱

Vue3的坑和Vue2反而不太一样。最大的一个坑是结构丢失响应性:

const state = reactive({ count: 0 }) // 隐患:解构后 count 是一个普通值,不再是响应式 const { count } = state // 或者函数里返回 function useCount() { return { ...state } }

解决方案有几种:如果只是想要一个响应式的独立变量,用ref替代;如果想解构后还保持响应性,Vue3官方提供了toRefs:

const state = reactive({ count: 0 }) const { count } = toRefs(state) // 现在 count 是 ref,修改 count.value 会同步更新 state

第二个坑是reactive和ref混用时的自动解包问题。在模板中,ref会自动解包,你写{{ count }}就行,不需要{{ count.value }}。但如果ref被放到了reactive对象里,它的解包行为又会变得很微妙:

const count = ref(0) const state = reactive({ count }) // state.count 会自动解包成 0,直接赋值和读取都可以 state.count++

这个特性大部分时候很爽,但如果你把ref嵌到一个数组里,或者嵌套得很深,解包行为偶尔会让人迷糊。我的建议是:在一个功能的内部,尽量统一使用ref或统一使用reactive,不要交叉混用。除非是为了解包便利,否则混用是后续排查问题的重灾区。

第三个坑是浅层响应式和触发控制的边界。比如shallowRef、shallowReactive、shallowReadonly,这些API只做最浅层的代理,深层对象不处理,适合那些不希望在深层递归上浪费性能的场景。比如一个对象嵌套了极大的列表但每一层都不可变,你就可以用shallowRef配合整个对象替换来触发更新。但新手很容易误用:以为shallowRef也能深层响应,结果改了深层的值页面没反应。用这类API时,心里要明确一条准则:只有整体替换这个浅层引用时,才会触发更新。

第四个坑是人容易忽略的triggerRef。配合shallowRef使用时,如果想手动强制触发更新,可以用triggerRef(ref)。这个API存在的意义就是为浅层响应式场景提供一个手动控制入口。但在实际项目中,triggerRef用多了会破坏数据流的单向性,我的做法是能不用就尽量不用,优先改用整体替换的方式触发更新。

4.3 排查思路速查表

响应式问题排查起来,很多同学喜欢直接改代码“试错”。我更推荐按照下面这张表先定位问题,再动手改,效率高很多:

症状可能原因优先排查动作
新增属性后视图不更新(Vue2)属性未经过defineProperty检查是否用了this.$set
数组索引修改后视图不更新(Vue2)索引操作无法被拦截换成splice或$set
解构reactive对象后视图不更新(Vue3)解构得到的值是原始值使用toRefs / ref
重新赋值shallowRef内部属性时视图不更新(Vue3)浅层响应式不递归内部整体替换ref.value,或使用triggerRef
视图更新但数据已变(渲染层问题)组件层级缓存 / key复用错误检查列表key是否稳定、检查是否使用了memo
数据变了但computed不更新computed依赖的数据未收集到用DevTools看computed的依赖列表

排查要诀:先在数据层确认“数据本身是否已经变化”,再确认“响应式系统是否收到了通知”,最后才去看“渲染函数是否正确重新执行”。大多数时候,问题出在前两步。用Vue DevTools打开组件面板,直接看组件的data或setup state,如果数据已经变了但视图没变,再沿着响应式链路往上查。

5. 迁移建议:从Vue2平滑过渡到Vue3的技术策略

我知道很多团队的项目还是Vue2,正在观望要不要升Vue3。说实话,Vue2虽然官方已经进入维护尾声,但存量项目实在太多,短期内替代不了。我这里只说我实际接触过的几个迁移案例和对应策略。

5.1 分阶段迁移,不要一锅端

最忌讳的做法是把项目所有页面一次性从Options API改写成Composition API。我见过一个团队这么干,结果两个月的排期拖成了半年,中途还差点回滚。正确的策略是:

  • 第一阶段:把Vue2项目升级到2.7版本,因为2.7是Vue2最后一个minor版本,它通过plugin的方式引入了Composition API的部分能力。
  • 第二阶段:逐个页面把新代码用<script setup>写,老代码先不动,保证增量代码是Vue3风格。
  • 第三阶段:如果确实要切换构建工具(Vite),可以先用Vue 3.4 + Vite 5做一个独立页面跑通,再考虑整体迁移。

我比较推荐“渐进式重构”,而不是“整体切换”。毕竟业务开发者的核心任务是交付功能,不是陪框架升级。

5.2 迁移时最容易翻车的API对照清单

很多人升级之后卡在API差异上。我把最容易踩的API差异整理成一张对照表,方便随时查阅:

场景Vue2Vue3
全局组件注册Vue.componentapp.component
全局间件Vue.useapp.use
事件总线new Vue() 作为eventBusmitt / 自己实现
过滤器filter用computed或方法替代
异步组件() => import()defineAsyncComponent
自定义指令bind/inserted/updatebeforeMount/mounted/updated
路由守卫beforeEnter内写组合式API里也可以onBeforeRouteLeave
全局watch$watchwatch() 函数

特别注意事件总线这一条。Vue2时代用new Vue()当eventBus很常见,但Vue3里Vue.prototype机制变了,不再有实例级的事件方法。替代方案是dessert类库,或者自己写几十行的发布订阅。如果你项目里大量使用eventBus,迁移时建议分层处理:先改成mitt,再逐步替换成真正可测试的状态管理。

5.3 迁移后的性能验证

我习惯在迁移后做三个维度的验证:

  • 初始化耗时:通过Performance面板记录从开始加载到首屏可交互的时间,对比迁移前后。
  • 列表渲染耗时:用同样的列表数据,对比渲染完成时间。
  • 内存占用:Vue2递归defineProperty的大对象,在Vue3惰性代理下,内存占用一般会更低。

如果迁移后性能反而变差,通常不是Vue3的问题,而是迁移过程中把原本应该静态化的模板变成了动态渲染,破坏了静态提升的优化效果。可以用Vue3的DevTools查看组件的编译产物,确认是否有静态节点被标记为动态。

6. 我对Vue响应式学习的最后几点体会

写这篇文章的时候,我又重新翻了翻手写迷你响应式的代码。每次看,都会有一些新的理解。比如这次我意识到:响应式系统的精华不在Proxy那一层,而在依赖收集与触发的闭环设计。Proxy只是换了一个更好用的拦截工具,真正的灵魂是Dep和effect之间的双向绑定。

回到最开始的那个问题:Vue响应式原理是什么,Vue2和Vue3有什么区别?我现在会这样回答:

  • 响应式本质是“读取时收集依赖、修改时触发更新”,这是一个经典的观察者模式在数据层上的应用。
  • Vue2用Object.defineProperty实现,优点是对ES5兼容性好,但对增删属性、数组索引天然不敏感,需要打补丁。
  • Vue3用Proxy实现,能拦截的操作为更多、支持惰性递归、性能更好,但需要ref来补齐基础类型无法代理的短板。

如果你正在准备面试,不要去背“Vue3比Vue2快”之类的结论,而是要把整个依赖收集链路自己在纸上画一遍。如果你正在用到Vue3项目,我建议你动手把文中那个迷你响应式系统写一遍,然后去看源码里reactive、effect、computed的实现,很多困惑会自然解开。如果你还在Vue2的老项目里,那至少要知道这条路已经快走到头了,新项目尽量选Vue3,老项目也尽早规划升级路径。

最后分享一个可以马上去做的小技巧:打开一个Vue3项目,在render函数里写一段带静态节点和动态节点的模板,然后在控制台打印编译后的render函数字符串,亲眼看一看PatchFlags和静态提升到底长什么样。看完一次,你对Vue3编译优化的理解就超过大多数只知道“Vue3更快”的人了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 7:27:13

基于Spring Boot与Vue的科研管理系统:状态机驱动流程与计分策略设计

简介&#xff1a;教师科研管理系统是一套面向教育信息化领域的完整项目源码&#xff0c;适合高校教师、科研管理人员以及需要完成课程设计或毕业设计的学生学习和参考。该系统围绕科研项目申报、立项执行、成果登记、资源共享与量化评估等环节设计&#xff0c;能够有效减少科研…

作者头像 李华
网站建设 2026/10/17 7:26:32

机器人控制系统分层架构与ego数据流设计实战解析

做机器人控制系统这些年&#xff0c;有个感受特别深&#xff1a;很多项目的代码量不小&#xff0c;但真正让它跑不顺的往往不是算法多难&#xff0c;而是数据流的组织方式出了问题。传感器数据满天飞、状态估计和底层控制耦合在一起、改一处就牵一发动全身。这套代号"Ψ₀…

作者头像 李华
网站建设 2026/10/17 7:25:29

GitHub日榜趋势观测系统:轻量级技术雷达构建指南

1. 项目概述&#xff1a;这不是一份榜单&#xff0c;而是一套可复用的趋势观测系统“GitHub 日榜趋势速报 | 2026-09-18”——看到这个标题&#xff0c;很多人第一反应是点开一张静态截图&#xff0c;扫一眼Top 10项目就划走。但作为连续三年每天凌晨三点爬取、清洗、标注、归因…

作者头像 李华
网站建设 2026/10/17 7:24:08

PowerCenter ETL开发入门:从映射到工作流的完整实践指南

1. 老牌ETL工具的定位&#xff1a;PowerCenter解决的是数据搬运的"脏活"1.1 先想清楚&#xff1a;你为什么要用ETL工具第一次接触Informatica ETL开发的人&#xff0c;多半是被项目上那句"所有数据都从这边走"推到这个工具面前的。PowerCenter在数据仓库里…

作者头像 李华
网站建设 2026/10/17 7:22:46

大模型压缩与推理加速实战:量化、蒸馏、剪枝及低秩分解全攻略

大模型部署是个体力活。模型越来越大&#xff0c;显存越来越不够用&#xff0c;推理速度越来越拉胯&#xff0c;线上并发一上来就告警。我这两年做过不少大模型的"瘦身"项目&#xff0c;从知识蒸馏到量化&#xff0c;再到剪枝和低秩分解&#xff0c;基本把这几条主路…

作者头像 李华