Vue3的响应式原理到底比Vue2强在哪?
先别急着直接讲原理的代码细节,得先搞明白:响应式到底是做什么用的?简单说,就是让网页能“懂”你的数据变了,自动更新对应的界面——比如你在购物车点了“+1”,总价不用手动刷新就能跟着涨,表单输错时提示文字能实时弹出来,这是Vue、React这类现代框架的核心竞争力,搞懂原理不仅能避免踩坑,复杂项目优化时也能心里有底。
Vue2的响应式是怎么做的?先捋清老版本的痛点
要夸Vue3,得先回顾Vue2的实现方式,没有对比就没有优势,Vue2用的是ES5的Object.defineProperty这个API,这个API能给对象的每个属性单独加上“拦截器”——也就是get和set函数,当你读取属性时,get会把当前正在渲染的组件存下来(专业点叫“依赖收集”);当你修改属性时,set会把之前存的组件找出来,让它们重新渲染界面(专业点叫“依赖触发”)。
听起来逻辑挺顺的对吧?但它天生有几个硬伤:
- 只能拦截对象已有属性的读写,如果你一开始定义data时没加某个属性,后来直接加的话,Vue2是“看不见”的,得用$set方法手动补拦截器;如果删除已有属性,也得用$delete,不然界面不会更新。
- 无法监听数组的索引变化和长度修改,比如你直接用arr[0] = '新值'或者arr.length = 0,界面都不会动——Vue2只能重写push、pop、splice这些7个数组原生方法来拦截,绕了个大弯,体验感一般。
- 深层响应式的初始化开销大,不管你用不用深层嵌套的属性,Vue2在初始化data时都会递归遍历整个对象,给每个子属性都加上get/set,如果你的data里有个几百上千条数据的数组,每个数组元素又是复杂对象,刚打开页面时的渲染速度可能会慢个几十毫秒,对于追求极致性能的项目来说,这是个小隐患。
Vue3用Proxy+Reflect重构响应式,彻底解决老问题
Vue3直接抛弃了Object.defineProperty,改用ES6的Proxy和Reflect——这两个API是配套用的,Reflect相当于Object的“增强版”,更规范、更兼容未来的JS语法,它的方法和Proxy的拦截器是一一对应的。
那Proxy是怎么工作的呢?简单说,它不是给单个属性加拦截,而是给整个对象(包括数组、Map、Set这些ES6+的集合类型)包一层“代理壳”,你操作的不再是原始数据,而是这个代理对象——所有的读写、新增、删除、遍历、函数调用等操作,都得先经过这个壳,Proxy就能在壳里拦截到所有操作,然后执行对应的依赖收集或触发逻辑。
现在回头看Vue2的三个痛点,Proxy是怎么一个个解决的:
第一个痛点:自动处理新增和删除属性
举个简单的例子,假设你在Vue3的setup里写了:
const state = reactive({ count: 1 })
// 直接新增属性
state.name = '小明'
// 直接删除属性
delete state.count
这些操作都会被Proxy的get/set/deleteProperty拦截器捕捉到,自动完成依赖的处理,根本不需要$set和$delete,新手再也不会因为漏加$set导致界面不更新而挠头了。
第二个痛点:完美支持数组的所有操作
不管是直接改索引、改长度,还是用数组的任何原生方法(包括那些Vue2没重写的、比如flat、fill这些),Proxy都能拦截到。
const arr = reactive([1, 2, 3]) arr[0] = 99 // 没问题 arr.length = 1 // 没问题 arr.flatMap(item => [item, item*2]) // 拦截到遍历和返回值?不,其实只要读取了数组的元素或者修改了数组本身,Proxy都能处理
绕弯子重写数组方法的历史一去不复返了。
第三个痛点:深层响应式按需初始化
Vue3的reactive函数是懒递归的——只有当你第一次读取到某个深层嵌套的属性时,才会给那个子对象也包一层Proxy,比如你的state里有个state.user.info.address,你一开始只用到了state.user.name,那Vue3只会给state、state.user加代理,不会碰info和address,这样一来,初始化的开销就大大降低了,特别是对于有大对象的项目,刚打开页面的白屏时间会更短。
Vue3还新增了什么响应式相关的好东西?
除了核心的Proxy+Reflect,Vue3还围绕响应式做了很多优化和新增,让开发体验更爽:
reactive和ref的分工更明确
reactive只能给对象、数组这些引用类型加响应式,ref可以给基本类型(数字、字符串、布尔值)和引用类型都加——不过ref给引用类型加响应式时,内部其实还是调用了reactive的,ref的好处是,不管是基本类型还是引用类型,你都可以用.value来统一访问和修改,在setup里传递的时候更方便,不用担心丢失响应式。
computed和watch支持多种写法
Vue3的computed不仅可以像Vue2那样写get函数,还可以写set函数,实现双向绑定的计算属性;watch除了可以监听单个响应式数据,还可以监听多个数据、监听响应式对象的某个属性、甚至可以用函数返回值来指定监听的内容,还支持immediate(立即执行一次)、deep(深层监听)、flush(控制执行时机,比如pre在组件更新前,post在更新后,sync同步执行)这些高级选项,比Vue2灵活多了。
effectScope让响应式副作用管理更方便
Vue3的setup函数在组件卸载时,会自动清理里面的响应式副作用(比如watch、computed的内部逻辑),但如果你在setup之外或者异步函数里创建了响应式副作用,就得手动清理,不然会内存泄漏,effectScope可以把这些副作用打包管理,需要的时候一次性清理,复杂项目里这个功能很实用。
shallowReactive/shallowRef只做浅层响应式
如果你有个很大的对象,只需要它的第一层属性是响应式的,深层属性不需要(比如静态配置项、第三方库的对象),就可以用shallowReactive或shallowRef,这样能进一步减少性能开销。
toRaw/markRaw可以获取原始对象或标记为不可响应式
有时候你需要操作原始数据,不想触发响应式更新(比如性能优化时批量修改数据),就可以用toRaw获取reactive/ref的原始对象;如果你有个对象是永远不会变的,不想让Vue3给它加代理(比如大的静态JSON数据),就可以用markRaw标记它,Vue3会直接忽略它。
举个小例子,直观感受Proxy的强大
假设我们要做一个简单的待办事项列表,有三个功能:添加待办、完成待办、删除待办,我们分别用Vue2和Vue3的核心思路(不写完整组件)来模拟一下:
Vue2的模拟思路
// 模拟Vue2的响应式
function defineReactive(obj, key, val) {
// 递归处理子属性
if (typeof val === 'object' && val !== null) {
observe(val)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`读取了属性:${key}`)
// 这里应该做依赖收集,暂时省略
return val
},
set(newVal) {
if (newVal === val) return
console.log(`修改了属性:${key},新值:${newVal}`)
val = newVal
// 这里应该做依赖触发,暂时省略
}
})
}
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return
// 遍历所有属性
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
// 模拟数组的重写
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = ['push', 'pop', 'splice', 'shift', 'unshift', 'sort', 'reverse']
methodsToPatch.forEach(method => {
arrayMethods[method] = function(...args) {
console.log(`调用了数组方法:${method}`)
const result = arrayProto[method].apply(this, args)
// 这里应该做依赖触发,暂时省略
return result
}
})
// 初始化数据
const data = { todos: [] }
observe(data)
// 给数组加重写的方法
data.todos.__proto__ = arrayMethods
// 测试功能
data.todos.push({ id: 1, text: '学习Vue2', done: false }) // 输出:调用了数组方法:push
// 注意:直接修改索引Vue2是拦截不到的,得用$set
// data.todos[0].done = true // 这里能拦截到,因为todo对象已经被observe过了,done是已有属性
data.todos[1] = { id: 2, text: '学习Vue3', done: false } // 没有输出,Vue2看不见
delete data.todos[0] // 没有输出,Vue2看不见
Vue3的模拟思路(简化版,去掉内部复杂的依赖管理逻辑)
// 模拟Vue3的reactive(简化版,不处理Map/Set,不做懒递归,用weakMap存代理)
const reactiveMap = new WeakMap()
function reactive(target) {
// 如果已经代理过了,直接返回代理对象
if (reactiveMap.has(target)) {
return reactiveMap.get(target)
}
// 如果是原始值或者已经被markRaw标记过,直接返回
if (typeof target !== 'object' || target === null || target.__v_skip) {
return target
}
// 创建代理对象
const proxy = new Proxy(target, {
get(target, key, receiver) {
console.log(`读取了属性:${key}`)
// 用Reflect.get保证this指向正确
const res = Reflect.get(target, key, receiver)
// 懒递归(简化版,第一次读取就代理)
return reactive(res)
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
if (oldValue === value) return true
console.log(`修改了属性:${key},新值:${value}`)
const result = Reflect.set(target, key, value, receiver)
// 这里应该做依赖触发,暂时省略
return result
},
deleteProperty(target, key) {
console.log(`删除了属性:${key}`)
const result = Reflect.deleteProperty(target, key)
// 这里应该做依赖触发,暂时省略
return result
}
})
// 把代理对象存起来
reactiveMap.set(target, proxy)
return proxy
}
// 初始化数据
const state = reactive({ todos: [] })
// 测试功能
state.todos.push({ id: 1, text: '学习Vue2', done: false }) // 输出:读取了属性:todos → 读取了属性:push → 修改了属性:length → 修改了属性:0
state.todos[0].done = true // 输出:读取了属性:todos → 读取了属性:0 → 修改了属性:done
state.todos[1] = { id: 2, text: '学习Vue3', done: false } // 输出:读取了属性:todos → 修改了属性:1 → 修改了属性:length
delete state.todos[0] // 输出:读取了属性:todos → 删除了属性:0
看,差距很明显吧?Vue3的模拟代码不仅更简洁,而且所有操作都能被拦截到,根本不需要绕弯子重写数组方法或者用$set/$delete。
总结一下Vue3响应式的核心优势
- 功能更全面:自动处理新增/删除属性,完美支持数组的所有操作,支持ES6+的集合类型(Map、Set、WeakMap、WeakSet)。
- 性能更好:懒递归初始化深层响应式,减少了不必要的代理开销;没有重写数组方法,执行效率更高。
- 开发体验更爽:不需要手动用$set/$delete,computed和watch更灵活,新增了很多实用的响应式API(shallowReactive/shallowRef、toRaw/markRaw、effectScope等)。
- 更符合未来的JS发展趋势:Proxy是ES6的标准API,支持的浏览器越来越多(现在除了IE11及以下,其他主流浏览器都完全支持),Vue3也不需要再为了兼容IE11做额外的处理(Vue3已经放弃了IE11的支持)。
如果你刚学Vue3,或者准备从Vue2迁移到Vue3,一定要花点时间搞懂响应式原理——这是Vue3的核心,搞懂了之后,不管是写代码还是调试bug,都会事半功倍。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


