面试总结之vue篇

208 阅读18分钟

1.vue的生命周期有哪些及每个生命周期做了什么?

vue生命周期共分为四个阶段

实例创建、DOM渲染、数据更新、销毁实例

共有八个基本钩子函数

  1. beforeCreate --创建前
  • 触发的行为:vue实例的挂载元素$el(模板)和数据对象data都为undefined,还未初始化。
  • 在此阶段可以做的事情:加loading事件

创建一个空白的Vue实例,data method尚未初始化

  1. created --创建后
  • 触发的行为:vue实例的数据对象data有了,$el(模板)还没有
  • 在此阶段可以做的事情:解决loading,请求ajax数据为mounted渲染做准备

Vue实例初始化完成,完成响应式绑定,data method 都已经初始化,尚未开始模版渲染

  1. beforeMount --渲染前
  • 触发的行为:vue实例的$el和data都初始化了,但还是虚拟的dom节点,具体的data.filter还未替换

编译模版,调用render生成vdom。还没有开始渲染dom

  1. mounted --渲染后
  • 触发的行为:vue实例挂载完成,data.filter成功渲染
  • 在此阶段可以做的事情:配合路由钩子使用

完成dom渲染,组件创建完成。开始由“创建阶段”进入“运行阶段”

  1. beforeUpdate --更新前
  • 触发的行为:data更新时触发
  1. updated —更新后
  • 触发的行为:data更新时触发
  • 在此阶段可以做的事情:数据更新时,做一些处理(此处也可以用watch进行观测)
  1. beforeDestroy —销毁前
  • 触发的行为:组件销毁时触发
  • 在此阶段可以做的事情:可向用户询问是否销毁
  1. destroyed —销毁后
  • 触发的行为:组件销毁时触发,vue实例解除了事件监听以及和dom的绑定(无响应了),但DOM节点依旧存在
  • 在此阶段可以做的事情:组件销毁时进行提示

2.Vue 的父组件和子组件生命周期钩子执行顺序是什么

1.首次加载过程

父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount ->子mounted -> (子activated) -> 父mounted

2.父组件更新过程

父beforeUpdate -> (子deactivated) -> 父updated

3.子组件更新过程

父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated

4.销毁过程

父beforeDestroy-> 子beforeDestroy -> 子destroyed -> 父destroyed

3.vue响应式原理是什么?vue3的响应式有何不同

Vue在初始化数据时,会使用Object.defineProperty重新定义data中的所有属性,当页面使用对应属性时,首先会进行依赖收集(收集当前组件的watcher)如果属性发生变化会通知相关依赖进行更新操作(发布订阅)。

Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。

Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?

判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。

监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。

vue2和vue3对响应式数据,数组怎么处理的?

Vue2通过重写数组7个方法实现响应式,无法检测索引赋值和length变化,需要$set特殊处理。Vue3用Proxy直接代理整个数组,任何修改都能监听到,包括索引赋值、length变化,不再需要特殊API。

Vue2和Vue3处理数组响应式的核心区别:

Vue2(Object.defineProperty限制)

  1. 方法拦截:重写push、pop、shift、unshift、splice、sort、reverse这7个方法
  2. 无法监听:直接通过索引修改(arr[0]=newValue)和修改length
  3. 解决方案:必须使用Vue.set()this.$set()
  4. 性能问题:需要遍历数组每个属性进行劫持

Vue3(Proxy全面代理)

  1. 完整代理:Proxy直接代理整个数组对象
  2. 全面监听:可以监听到索引修改、length变化、所有数组方法
  3. 无需特殊API:直接赋值即可响应(arr[0]=newValue)
  4. 性能优化:懒处理嵌套对象,按需响应

4.vue3和vue2的区别

  • 源码组织方式变化:使用 TS 重写
  • 支持 Composition API:基于函数的API,更加灵活组织组件逻辑(vue2用的是options api)
  • 响应式系统提升:Vue3中响应式数据原理改成proxy,可监听动态新增删除属性,以及数组变化
  • 编译优化:vue2通过标记静态根节点优化diff,Vue3 标记和提升所有静态根节点,diff的时候只需要对比动态节点内容
  • 打包体积优化:移除了一些不常用的api(inline-template、filter)
  • 生命周期的变化:使用setup代替了之前的beforeCreate和created
  • Vue3 的 template 模板支持多个根标签
  • Vuex状态管理:创建实例的方式改变,Vue2为new Store , Vue3为createStore
  • Route 获取页面实例与路由信息:vue2通过this获取router实例,vue3通过使用 getCurrentInstance/ userRoute和userRouter方法获取当前组件实例
  • Props 的使用变化:vue2 通过 this 获取 props 里面的内容,vue3 直接通过 props
  • 父子组件传值:vue3 在向父组件传回数据时,如使用的自定义名称,如 backData,则需要在 emits 中定义一下

5.谈一谈对 MVVM 的理解?

MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

6.在 Vue2.x 中如何检测数组的变化?

使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。

7.v-model 双向绑定的原理是什么?

v-model本质就是一个语法糖,可以看成是value + input方法的语法糖。 可以通过model属性的prop和event属性来进行自定义。原生的v-model,会根据标签的不同生成不同的事件和属性。

8.vue2.x 和 vuex3.x 渲染器的 diff 算法分别说一下?

简单来说,diff整体策略为:深度优先,同层比较

  • 比较只会在同层级进行,不会跨层级比较
  • 比较的过程中,循环从两边向中间收拢
  • 如果tag不相同,直接删除重建,不再深度比较
  • 如果tag和key都相同,默认是一样的节点,也不再深度比较

正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。 Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

Vue3.x借鉴了 ivi算法和 inferno算法

在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。(实际的实现可以结合Vue3.x源码看。) 该算法中还运用了动态规划的思想求解最长递归子序列。

9.vue组件通信方式有哪些及原理

  • 父子组件通信:父->子props,子->父 $on、$emit
  • 获取父子组件实例 $parent、$children
  • Ref 获取实例的方式: 调用组件的属性或者方法
  • Provide、inject: 官方不推荐使用,但是写组件库时很常用
  • 兄弟组件通信 Event Bus:实现跨组件通信 。Vue.prototype.$bus = new Vue/Vuex
  • 跨级组件通信Vuex/$attrs、$listeners

10.子组件为什么不可以修改父组件传递的Prop?

Vue提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。

11.说一下 v-if 与 v-show 的区别

v-if 会在切换过程中对条件块的事件监听器和子组件进行销毁和重建,如果初始条件是false,则什么都不做,直到条件第一次为true时才开始渲染模块。
v-show 只是基于css进行切换,不管初始条件是什么,都会渲染。
所以,v-if 切换的开销更大,而 v-show 初始化渲染开销更大,在需要频繁切换,或者切换的部分dom很复杂时,使用 v-show 更合适。渲染后很少切换的则使用 v-if 更合适。

12.keep-alive的常用属性有哪些及实现原理

常用属性

  • keep-alive可以实现组件缓存,当组件切换时不会对当前组件进行卸载。
  • 常用的两个属性include/exclude,允许组件有条件的进行缓存。
  • 两个生命周期activated/deactivated,用来得知当前组件是否处于活跃状态。
  • keep-alive的中还运用了LRU(Least Recently Used)算法。

实现原理

13. nextTick 的作用是什么?他的实现原理是什么?

在下次 DOM 更新循环结束之后执行延迟回调。nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用

  • Promise
  • MutationObserver
  • setImmediate
  • 如果以上都不行则采用setTimeout

定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。

14.nextTick为什么要优先使用微任务实现?

  • vue nextTick的源码实现,异步优先级判断,总结就是Promise > MutationObserver > setImmediate > setTimeout
  • 优先使用Promise,因为根据 event loop 与浏览器更新渲染时机,宏任务 → 微任务 → 渲染更新,使用微任务,本次event loop轮询就可以获取到更新的dom
  • 如果使用宏任务,要到下一次event loop中,才能获取到更新的dom

15.vue 异步更新原理

Vue的数据频繁变化,但为什么dom只会更新一次?

  • Vue数据发生变化之后,不会立即更新dom,而是异步更新的
  • 侦听到数据变化,Vue 将开启一个队列,并缓存在同一事件循环中发生的所有数据变更
  • 如果同一个 watcher 被多次触发,只会被推入到队列中一次,可以避免重复修改相同的dom,这种去除重复数据,对于避免不必要的计算和 DOM 操作是非常重要的
  • 同步任务执行完毕,开始执行异步 watcher 队列的任务,一次性更新 DOM

16.Vue 组件的 data 为什么必须是函数

一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。

17.说一下 watch 与 computed 的区别是什么?以及他们的使用场景分别是什么?

computed 计算属性,是依赖其他属性的计算值,并且有缓存,只有当依赖的值变化时才会更新。
watch 是在监听的属性发生变化时,在回调中执行一些逻辑。
所以,computed 适合在模板渲染中,某个值是依赖了其他的响应式对象甚至是计算属性计算而来,而 watch 适合监听某个值的变化去完成一段复杂的业务逻辑。

  • 计算属性本质上是 computed watcher,而watch本质上是 user watcher(用户自己定义的watcher)
  • computed有缓存的功能,通过dirty控制
  • wacher设置deep:true,实现深度监听的功能
  • computed可以监听多个值的变化

18.说一下 Vue 的 computed 的实现原理

  • 初始化计算属性时,遍历computed对象,给其中每一个计算属性分别生成唯一computed watcher,并将该watcher中的dirty设置为true
  • 初始化时,计算属性并不会立即计算(vue做的优化之一),只有当获取的计算属性值才会进行对应计算
  • 初始化计算属性时,将Dep.target设置成当前的computed watcher,将computed watcher添加到所依赖data值对应的dep中(依赖收集的过程),然后计算computed对应的值,后将dirty改成false
  • 当所依赖data中的值发生变化时,调用set方法触发dep的notify方法,将computed watcher中的dirty设置为true
  • 下次获取计算属性值时,若dirty为true, 重新计算属性的值
  • dirty是控制缓存的关键,当所依赖的data发生变化,dirty设置为true,再次被获取时,就会重新计算

总结:Computed 通过响应式系统自动追踪依赖,实现「缓存 + 按需重新计算」的优化机制,保证在依赖未变化时直接返回缓存值,避免不必要的计算开销。

19.说一下 Vue 的 watch 实现原理

  • 遍历watch对象, 给其中每一个watch属性,生成对应的user watcher
  • 调用watcher中的get方法,将Dep.target设置成当前的user watcher,并将user watcher添加到监听data值对应的dep中(依赖收集的过程)
  • 当所监听data中的值发生变化时,会调用set方法触发dep的notify方法,执行watcher中定义的方法
  • 设置成deep:true的情况,递归遍历所监听的对象,将user watcher添加到对象中每一层key值的dep对象中,这样无论当对象的中哪一层发生变化,wacher都能监听到。通过对象的递归遍历,实现了深度监听功能

20. computed vs methods

计算属性是基于他们的响应式依赖进行缓存的,只有在依赖发生变化时,才会计算求值,而使用 methods,每次都会执行相应的方法。

21.vue css scoped原理

1)编译时,会给每个vue文件生成一个唯一的id,会将此id添加到当前文件中所有html的标签上 如<div class="demo"></div>会被编译成<div class="demo" data-v-27e4e96e></div>

2)编译style标签时,会将css选择器改造成属性选择器,如.demo{color: red;}会被编译成.demo[data-v-27e4e96e]{color: red;}

22.组件中写 name 选项有哪些好处

  • 可以通过名字找到对应的组件( 递归组件:组件自身调用自身 )
  • 可以通过 name 属性实现缓存功能(keep-alive)
  • 可以通过 name 来识别组件(跨级组件通信时非常重要)
  • 使用 vue-devtools 调试工具里显示的组见名称是由 vue 中组件 name 决定的

23. Vue 中 v-html 会导致什么问题

在网站上动态渲染任意 HTML,很容易导致 XSS 攻击。所以只能在可信内容上使用 v-html,且永远不能用于用户提交的内容上。

24.你的接口请求一般放在哪个生命周期中?为什么要这样做?

接口请求可以放在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。 但是推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间
  • SSR 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于代码的一致性
  • created 是在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。如果在 mounted 钩子函数中请求数据可能导致页面闪屏问题

25.Vuex的理解及使用场景

Vuex 是一个专为 Vue 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。

  1. Vuex 的状态存储是响应式的;当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新

  2. 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation, 这样使得我们可以方便地跟踪每一个状态的变化 Vuex主要包括以下几个核心模块:

  • State:定义了应用的状态数据
  • Getter:在 store 中定义“getter”(可以认为是 store 的计算属性),就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来, 且只有当它的依赖值发生了改变才会被重新计算
  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数
  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作
  • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中

26.mixin

mixin 项目变得复杂的时候,多个组件间有重复的逻辑就会用到mixin
多个组件有相同的逻辑,抽离出来
mixin并不是完美的解决方案,会有一些问题
vue3提出的Composition API旨在解决这些问题【追求完美是要消耗一定的成本的,如开发成本】

场景:PC端新闻列表和详情页一样的右侧栏目,可以使用mixin进行混合
劣势:

  • 变量来源不明确,不利于阅读
  • 多mixin可能会造成命名冲突
  • mixin和组件可能出现多对多的关系,使得项目复杂度变高

27.为什么v-for和v-if不建议用在一起

1.当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费(Vue2.x)
2.这种场景建议使用 computed,先对数据进行过滤

注意:3.x 版本中 v-if 总是优先于 v-for 生效。由于语法上存在歧义,建议避免在同一元素上同时使用两者。比起在模板层面管理相关逻辑,更好的办法是通过创建计算属性筛选出列表,并以此创建可见元素。

28.插槽slot

Vue 插槽(slot)用于在组件中定义 灵活的占位内容,使组件更具复用性和灵活性。Vue 提供了 默认插槽、具名插槽、作用域插槽 三种类型。

  • 具名插槽:适用于多个插槽的情况,每个插槽可以有不同的内容。
  • 作用域插槽:用于父组件访问子组件内部的数据,提升插槽的灵活性。

29.vue路由守卫

Vue 路由守卫(Router Guards)用于拦截路由跳转,可以在路由进入、离开或解析时执行逻辑,比如权限控制、登录校验、页面缓存等。

Vue Router 提供了三类守卫

  1. 全局守卫(所有路由都会触发):适用于登录验证、权限控制、全局数据处理等。
  2. 路由独享守卫(单个路由独有):在路由配置中定义,仅作用于单个路由。
  3. 组件内守卫(在组件内部):在单个 Vue 组件内部定义,只影响该组件的路由行为。

30.vue 中的 spa 应用如何优化首屏加载速度?

优化首屏加载速度的核心是减少首屏资源体积、提高资源加载效率,常见优化方案包括:代码拆分、懒加载、SSR、资源压缩、CDN 加速、骨架屏等。

1. 代码优化

  • 路由懒加载:Vue 默认会把所有组件一次性打包,导致首屏加载体积过大。可以使用动态 import 按需加载组件。减少首屏 JS 体积,提高首屏加载速度。
  • Vue 组件按需加载: 默认情况下,Vue 会打包所有组件。可以使用 异步组件,避免一次性加载所有组件。避免主包体积过大
  • 按需加载第三方库: 某些大型第三方库(如 lodashmoment)可能影响首屏加载。使用 import() 按需加载,减少打包体积
  • 提高代码使用率:利用代码分割,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程
  • 封装:构建良好的项目架构,按照项目需求就行全局组件,插件,过滤器,指令,utils 等做一 些公共封装,可以有效减少我们的代码量,而且更容易维护资源优化

2. 资源优化

  • 开启 Gzip / Brotli 压缩:Gzip 或 Brotli 能显著减少 JS/CSS 体积,加快资源下载速度。
  • 使用 CDN 加速:将第三方的类库放到 CDN 上,能够大幅度减少生产环境中的项目体积
  • 懒加载图片:使用图片懒加载可以优化同一时间减少 http 请求开销,避免显示图片导致的画面抖动,提高用户体验

3. 服务器优化

  • 开启 HTTP/2:HTTP/2 多路复用 可以加快资源加载。
  • 使用 SSR 或静态预渲染: Vue 默认是 CSR(客户端渲染),但 SSR(服务器渲染)或**静态预渲染(Prerender)**可以提升首屏速度。
  • 骨架屏:在首屏加载时,显示骨架屏代替空白页面,提高用户体验。

其余

  • 缓存:将长时间不会改变的第三方类库或者静态资源设置为强缓存,将 max-age 设置为一个非常长的时间,再将访问路径加上哈希达到哈希值变了以后保证获取到最新资源,好的缓存策略有助于减轻服务器的压力,并且显著的提升用户的体验

31.vue3中的diff算法相对于vue2改进了哪些?

1. 编译时优化(最大提升)

  • 静态提升:静态节点只创建一次,不参与Diff
  • PatchFlag标记:动态节点打标,更新时只对比有变化的部分
  • Block Tree:动态内容包裹成块,跳过静态子树

2. Diff算法升级

  • Vue2:双端比较(头头、尾尾、头尾、尾头)
  • Vue3:最长递增子序列算法 → 找到最少DOM移动方案

3. 性能提升明显

  • 静态内容:90%+性能提升
  • 列表重排:5-10倍更快
  • 内存占用:减少40%-60%

32.vue中的diff算法原理?

为什么用diff算法

因为直接操作DOM代价太高,每次更新都全量重渲染性能差。Diff算法通过比较新旧虚拟DOM树的差异,只更新变化的部分。

核心原理三层递进

1. 树对比策略(Tree Diff)

  • 同级比较:只比较同一层级的节点,不跨层级
  • 深度优先:从根节点开始,递归比较子节点
  • 时间复杂度O(n) :通过key优化可以达到线性复杂度

2. 组件对比策略(Component Diff)

  • 相同组件:继续递归Diff子节点
  • 不同组件:直接替换整个组件(不继续比较子节点)

3. 元素对比策略(Element Diff)

Vue的Diff优化策略(重点)

  • Vue2采用双端比较策略,通过四个指针(新旧头尾)进行四种比较
  • Vue3在双端比较基础上,加入最长递增子序列(LIS)  优化

Key的重要性

key的作用是帮助Diff算法识别哪些节点可以复用

更概括的回答是

Diff算法的核心是最小化DOM操作。具体来说:

Vue2采用双端比较:通过新旧节点的头尾四个指针,进行四种比较匹配,找到可复用节点。

Vue3进一步优化:使用最长递增子序列算法,在列表重排时找到最少移动方案,加上编译时的静态提升和PatchFlag标记,性能大幅提升。

关键点

  1. 通过虚拟DOM的差异计算代替直接DOM操作
  2. key帮助算法正确识别可复用节点
  3. 多层级的优化策略(树Diff → 组件Diff → 元素Diff)

实际效果:虽然Diff需要计算成本,但避免了昂贵DOM操作,整体性能更好。

33.Composition API是什么?

Composition API是Vue3推出的新代码组织方式,核心解决Options API在复杂组件中的代码分散问题

主要优势

  1. 逻辑聚合:相关功能的代码可以写在一起,而不是分散到data、methods等选项中
  2. 更好的复用:通过自定义Hook清晰复用逻辑,没有mixins的命名冲突问题
  3. TypeScript友好:天然支持类型推断,开发体验更好
  4. 更灵活的代码组织:可以按功能而不是按选项组织代码

核心API

  • ref/reactive 创建响应式数据
  • computed 创建计算属性
  • watch/watchEffect 监听变化
  • 生命周期钩子变成函数形式(onMounted等)
  • provide/inject 依赖注入

34.watch 和 watchEffect区别?

核心区别

  1. 监听方式watch需要显式指定监听源,watchEffect自动收集函数内的依赖
  2. 执行时机watch默认惰性(值变化才执行),watchEffect立即执行一次
  3. 参数获取watch能获取新旧值对比,watchEffect只有当前值
  4. 停止和清理:两者都支持停止监听和清理副作用

选择建议

  • 需要新旧值对比精确控制监听逻辑 → 用 watch
  • 需要自动收集依赖立即执行副作用 → 用 watchEffect
  • 简单监听可以用 watchEffect 简化代码
  • 复杂条件监听用 watch 更清晰可控

实际经验:项目中我通常用 watch 处理业务逻辑,用 watchEffect 处理UI副作用和调试。

简短回答模板:watch 是精确监听指定数据变化,能获取新旧值;watchEffect 是自动收集依赖并立即执行,适合处理副作用。简单监听用watchEffect方便,复杂逻辑用watch更可控。

35.描述组件渲染更新过程?

  1. 初次渲染,响应式实现原理中的getter进行依赖收集响应式数据,进入观察者模式
  2. 当data数据更新时,触发setter,通知watcher,然后触发render函数,生成虚拟DOM,重新渲染

36.vue3中 ref和 reactive的区别 ?

三个核心区别

  1. 数据类型:ref 支持所有类型,reactive 只支持和数组
  2. 访问方式:ref 需要 .value,reactive 直接访问属性,无需 .value
  3. 替换方式:ref 可以整个替换,reactive 不能整个替换,会失去响应性。 只能修改属性
  4. reactive解构会失去响应性,需要用 toRefs

选择建议:原始值用 ref,复杂对象用 reactive,需要灵活替换时用 ref。

底层上,ref 内部使用一个对象包装值,通过 getter/setter 实现响应性;reactive 使用 ES6 Proxy 代理整个对象。ref(对象) 实际上内部调用了 reactive。

团队规范是

  1. 原始值、可能为 null 的数据、模板引用 → 用 ref
  2. 表单对象、配置对象、复杂状态 → 用 reactive
  3. 组合式函数返回值优先返回 ref,方便用户解构
  4. 避免混合使用,保持代码一致性

常见陷阱提醒

注意三个坑:

  1. reactive 解构失去响应性 → 用 toRefs
  2. reactive 赋值整个新对象会失去响应性 → 用 Object.assign 或 ref
  3. watch reactive 的深层对象时,默认不深度监听 → 加 { deep: true }