1.vue的生命周期有哪些及每个生命周期做了什么?
vue生命周期共分为四个阶段
实例创建、DOM渲染、数据更新、销毁实例
共有八个基本钩子函数
- beforeCreate --创建前
- 触发的行为:vue实例的挂载元素$el(模板)和数据对象data都为undefined,还未初始化。
- 在此阶段可以做的事情:加loading事件
创建一个空白的Vue实例,data method尚未初始化
- created --创建后
- 触发的行为:vue实例的数据对象data有了,$el(模板)还没有
- 在此阶段可以做的事情:解决loading,请求ajax数据为mounted渲染做准备
Vue实例初始化完成,完成响应式绑定,data method 都已经初始化,尚未开始模版渲染
- beforeMount --渲染前
- 触发的行为:vue实例的$el和data都初始化了,但还是虚拟的dom节点,具体的data.filter还未替换
编译模版,调用render生成vdom。还没有开始渲染dom
- mounted --渲染后
- 触发的行为:vue实例挂载完成,data.filter成功渲染
- 在此阶段可以做的事情:配合路由钩子使用
完成dom渲染,组件创建完成。开始由“创建阶段”进入“运行阶段”
- beforeUpdate --更新前
- 触发的行为:data更新时触发
- updated —更新后
- 触发的行为:data更新时触发
- 在此阶段可以做的事情:数据更新时,做一些处理(此处也可以用watch进行观测)
- beforeDestroy —销毁前
- 触发的行为:组件销毁时触发
- 在此阶段可以做的事情:可向用户询问是否销毁
- 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限制) :
- 方法拦截:重写push、pop、shift、unshift、splice、sort、reverse这7个方法
- 无法监听:直接通过索引修改(arr[0]=newValue)和修改length
- 解决方案:必须使用
Vue.set()或this.$set() - 性能问题:需要遍历数组每个属性进行劫持
Vue3(Proxy全面代理) :
- 完整代理:Proxy直接代理整个数组对象
- 全面监听:可以监听到索引修改、length变化、所有数组方法
- 无需特殊API:直接赋值即可响应(arr[0]=newValue)
- 性能优化:懒处理嵌套对象,按需响应
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为createStoreRoute 获取页面实例与路由信息: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(仓库)。
-
Vuex 的状态存储是响应式的;当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新
-
改变 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 提供了三类守卫:
全局守卫(所有路由都会触发):适用于登录验证、权限控制、全局数据处理等。路由独享守卫(单个路由独有):在路由配置中定义,仅作用于单个路由。组件内守卫(在组件内部):在单个 Vue 组件内部定义,只影响该组件的路由行为。
30.vue 中的 spa 应用如何优化首屏加载速度?
优化首屏加载速度的核心是减少首屏资源体积、提高资源加载效率,常见优化方案包括:代码拆分、懒加载、SSR、资源压缩、CDN 加速、骨架屏等。
1. 代码优化
路由懒加载:Vue 默认会把所有组件一次性打包,导致首屏加载体积过大。可以使用动态import按需加载组件。减少首屏 JS 体积,提高首屏加载速度。Vue 组件按需加载: 默认情况下,Vue 会打包所有组件。可以使用 异步组件,避免一次性加载所有组件。避免主包体积过大按需加载第三方库: 某些大型第三方库(如lodash、moment)可能影响首屏加载。使用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标记,性能大幅提升。
关键点:
- 通过虚拟DOM的差异计算代替直接DOM操作
- key帮助算法正确识别可复用节点
- 多层级的优化策略(树Diff → 组件Diff → 元素Diff)
实际效果:虽然Diff需要计算成本,但避免了昂贵DOM操作,整体性能更好。
33.Composition API是什么?
Composition API是Vue3推出的新代码组织方式,核心解决Options API在复杂组件中的代码分散问题。
主要优势:
- 逻辑聚合:相关功能的代码可以写在一起,而不是分散到data、methods等选项中
- 更好的复用:通过自定义Hook清晰复用逻辑,没有mixins的命名冲突问题
- TypeScript友好:天然支持类型推断,开发体验更好
- 更灵活的代码组织:可以按功能而不是按选项组织代码
核心API:
ref/reactive创建响应式数据computed创建计算属性watch/watchEffect监听变化- 生命周期钩子变成函数形式(
onMounted等) provide/inject依赖注入
34.watch 和 watchEffect区别?
核心区别:
- 监听方式:
watch需要显式指定监听源,watchEffect自动收集函数内的依赖 - 执行时机:
watch默认惰性(值变化才执行),watchEffect立即执行一次 - 参数获取:
watch能获取新旧值对比,watchEffect只有当前值 - 停止和清理:两者都支持停止监听和清理副作用
选择建议:
- 需要新旧值对比或精确控制监听逻辑 → 用
watch - 需要自动收集依赖或立即执行副作用 → 用
watchEffect - 简单监听可以用
watchEffect简化代码 - 复杂条件监听用
watch更清晰可控
实际经验:项目中我通常用 watch 处理业务逻辑,用 watchEffect 处理UI副作用和调试。
简短回答模板:watch 是精确监听指定数据变化,能获取新旧值;watchEffect 是自动收集依赖并立即执行,适合处理副作用。简单监听用watchEffect方便,复杂逻辑用watch更可控。
35.描述组件渲染更新过程?
- 初次渲染,响应式实现原理中的getter进行依赖收集响应式数据,进入观察者模式
- 当data数据更新时,触发setter,通知watcher,然后触发render函数,生成虚拟DOM,重新渲染
36.vue3中 ref和 reactive的区别 ?
三个核心区别
- 数据类型:ref 支持所有类型,reactive 只支持和数组
- 访问方式:ref 需要 .value,reactive 直接访问属性,无需 .value
- 替换方式:ref 可以整个替换,reactive 不能整个替换,会失去响应性。 只能修改属性
- reactive解构会失去响应性,需要用 toRefs
选择建议:原始值用 ref,复杂对象用 reactive,需要灵活替换时用 ref。
底层上,ref 内部使用一个对象包装值,通过 getter/setter 实现响应性;reactive 使用 ES6 Proxy 代理整个对象。ref(对象) 实际上内部调用了 reactive。
团队规范是
- 原始值、可能为 null 的数据、模板引用 → 用 ref
- 表单对象、配置对象、复杂状态 → 用 reactive
- 组合式函数返回值优先返回 ref,方便用户解构
- 避免混合使用,保持代码一致性
常见陷阱提醒
注意三个坑:
- reactive 解构失去响应性 → 用 toRefs
- reactive 赋值整个新对象会失去响应性 → 用 Object.assign 或 ref
- watch reactive 的深层对象时,默认不深度监听 → 加 { deep: true }