vue3问题总结
react 和 vue 有哪些区别?
1. 设计哲学与定位
- React: 严格来说是一个库(Library),专注于视图层(View Layer)。
- Vue: 是一个渐进式框架(Progressive Framework)。
2. 模板 vs JSX
- Vue: 默认使用基于 HTML 的模板语法(Template)。
- React: 使用 JSX(JavaScript XML),
3. 数据绑定与响应式系统
- Vue: 核心是响应式系统。Vue 2 使用
Object.defineProperty,Vue 3 升级为基于Proxy。开发者只需修改数据,Vue 会自动追踪依赖并更新 DOM,实现了双向绑定(通过v-model指令)。这使得数据操作非常直观。 - React: 采用单向数据流。状态(State)是不可变的,需要通过
setState或useStateHook 显式地更新状态来触发组件重新渲染。
4. 状态管理
- React: 内置了
useState、useReducer等 Hook 来管理组件状态。 - Vue: 提供了
ref和reactive等 API 来定义响应式数据。
7. 性能
- Vue 3 引入了编译时优化(如静态提升、Patch 标记),在某些场景下可能比 React 更高效。
- React 的 Fiber 架构 和 Concurrent Mode(并发模式)旨在提供更流畅的用户体验,特别是在处理大型应用和复杂交互时。
Object.defineProperty 有哪些局限性
Object.defineProperty 是 ES5 中引入的强大特性,Vue 2 正是利用它来实现响应式系统的核心。然而,它存在一些显著的局限性,这些局限性也是 Vue 3 升级到 Proxy 的主要原因。
以下是 Object.defineProperty 的主要局限性:
1. 无法监听数组索引的变化(直接通过索引设置元素)
这是最广为人知的局限性。
- 问题:当你直接通过索引修改数组元素(如
arr[0] = newValue)或修改数组长度(如arr.length = 0)时,Object.defineProperty无法检测到这些变化。const arr = [1, 2, 3]
Object.defineProperty(arr, '0', {
set(value) {
console.log('索引0的值被修改了')
// 实际上,这个set函数在这里不会被触发
},
})
arr[0] = 4 // 不会触发 set - Vue 2 的解决方案:Vue 2 通过重写数组的原型方法(如
push,pop,shift,unshift,splice,sort,reverse)来“劫持”这些方法,在调用原方法后手动触发视图更新。但这只解决了通过方法修改数组的情况,直接通过索引赋值或修改length仍然无效。 - 后果:使用
Vue.set(vm.arr, indexOfItem, newValue)或vm.$set(vm.arr, indexOfItem, newValue)来确保响应式更新。
2. 无法监听对象属性的动态添加或删除
Object.defineProperty 只能对对象上已经存在的属性进行劫持。
-
问题:如果你向一个已经通过
Object.defineProperty使其响应式的对象添加一个新属性,或者删除一个已有属性,这个操作不会触发视图更新。const obj = { a: 1 }
Object.defineProperty(obj, 'a', {
get() {
console.log('读取a')
},
set(value) {
console.log('设置a')
},
})
obj.a = 2 // 会触发 set
obj.b = 3 // 新增属性b,无法被监听!
delete obj.a // 删除属性a,无法被监听! -
Vue 2 的解决方案:
- 添加属性:使用
Vue.set(vm.obj, 'newProp', value)或vm.$set(obj, 'newProp', value)。 - 删除属性:使用
Vue.delete(vm.obj, 'propToRemove')或vm.$delete(obj, 'propToRemove')。
- 添加属性:使用
3. 需要递归遍历才能实现深度监听(Deep Watching)
Object.defineProperty 只能监听对象的单层属性。
- 问题:如果对象的某个属性值本身也是一个对象,那么这个嵌套对象的属性变化是无法被监听到的。
const obj = { nested: { c: 3 } }
// 如果只对 obj 的 'nested' 属性进行 defineProperty
// 那么 obj.nested.c = 4 这样的操作不会触发任何监听 - 解决方案:必须递归地遍历对象的所有嵌套属性,对每一层的每个属性都调用
Object.defineProperty。这不仅代码复杂,而且在对象层级很深或属性非常多时,会造成性能开销和内存占用。 - Vue 2 的实践:Vue 2 在初始化时会对
data对象进行递归遍历,将所有属性转换为响应式。这也是为什么 Vue 2 推荐在data中预先声明好所有需要响应的属性。
4. 对整个对象的监听需要逐个属性定义
Object.defineProperty 的作用目标是单个属性,而不是整个对象。
- 问题:要使一个对象的所有属性都具有响应性,必须对它的每一个属性都单独调用
Object.defineProperty。 - 后果:这使得实现一个通用的、自动化的响应式系统变得复杂,需要大量的循环和递归代码。
5. 兼容性问题
Object.defineProperty是 ES5 的特性,不支持 IE8 及更早版本。虽然现代开发已基本不考虑这些旧浏览器,但在某些特殊场景下仍需注意。
总结
Object.defineProperty 的核心局限在于:
- 监听不完整:无法监听数组索引赋值、属性增删。
- 监听不彻底:只能监听已有属性,且需要手动递归实现深度监听。
- 使用不友好:开发者需要记住并使用
Vue.set/Vue.delete等特殊 API 来规避限制。
Vue 3 的 ref() 如何实现对简单数据类型的响应式
const name = ref('hello')
这里的 "hello" 是基本类型(字符串),它不会被 reactive() 包装,而是直接放进 ref 的 .value 里。
机制拆解:
-
初始化阶段
ref("hello")调用createRef("hello")。- 判断值是不是对象:字符串不是对象,所以不会走
reactive,直接存到_value。
_value = 'hello' -
getter 拦截
- 当你访问
name.value时,会触发 getter:- 调用
track(),把当前副作用(effect)收集起来; - 返回存储的
_value(也就是"hello")。
- 调用
get value() {
track(this, 'value')
return this._value
} - 当你访问
-
setter 拦截
- 当你修改
name.value = "world"时,会触发 setter:- 先判断新值和旧值是否不同;
- 如果不同,就更新
_value; - 然后调用
trigger(),通知所有依赖更新。
set value(newVal) {
if (hasChanged(newVal, this._value)) {
this._value = newVal
trigger(this, 'value')
}
} - 当你修改
-
响应式触发
const name = ref('hello')
effect(() => {
console.log(name.value) // 依赖收集
})
name.value = 'world' // 触发 trigger → effect 重新执行
所以字符串(还有数字、布尔、null、undefined 等)在 ref() 里都是靠 .value 的 getter/setter 来实现响应式,而不是 Proxy。
换句话说:
- 对象传给
ref→.value里套了个 Proxy(reactive)。 - 字符串/数字传给
ref→.value是普通值,依赖收集和触发全靠.value的 getter/setter。
要不要我再帮你画一张「字符串 ref 的响应式流程图」,让它和对象 ref 的区别更直观?
Vue 3 的 ref() 如何实现对复杂数据类型的响应式
Vue3 里的 ref() 本质是给单值做响应式包装,但当你传入一个对象时,它内部会偷偷把这个对象用 reactive() 包一层。
拆解一下:
-
ref 初始化
function ref(rawValue) {
return createRef(rawValue, false)
} -
createRef 内部逻辑
- 如果
rawValue是对象,会调用reactive(rawValue); - 如果是基本类型,就直接保存。
源码里类似这样:
function createRef(rawValue, shallow) {
return {
__v_isRef: true,
_value: shallow ? rawValue : toReactive(rawValue),
get value() {
track(this, 'value')
return this._value
},
set value(newVal) {
if (hasChanged(newVal, this._value)) {
this._value = shallow ? newVal : toReactive(newVal)
trigger(this, 'value')
}
},
}
}
function toReactive(value) {
return isObject(value) ? reactive(value) : value
} - 如果
-
对象响应式的实现
reactive(obj)内部用Proxy拦截get/set;- 这样
ref({ foo: 1 })的.value本身是响应式对象,访问myRef.value.foo时,会走 Proxy → 触发依赖收集。 - 修改时,比如
myRef.value.foo = 2,会走 Proxy 的set→ 触发更新。
所以:
ref()的响应式靠.value的 getter/setter。- 如果
.value是对象,就进一步用reactive()包装,这样对象的属性也能保持响应式。
一个小例子:
const state = ref({ count: 0 })
effect(() => {
console.log(state.value.count) // 依赖收集
})
state.value.count++ // 触发 Proxy.set → trigger → effect 重新执行
这里 响应式触发的源头 并不是 ref.value 的 setter(因为 .value 本身没变),而是内部的 reactive Proxy 在发挥作用。