Vue2/3面试题
Vue2
1. Vue 的基本原理
当 一 个 Vue 实 例 创 建 时 , Vue 会 遍 历 data 中 的 属 性 , 用 Object.defineProperty ( vue3.0 使 用 proxy) 将 它 们 转 为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时 通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组 件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用 时,会通知 watcher 重新计算,从而使它关联的组件得以更新。
2. 双向数据绑定的原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数 据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几 个步骤:
-
需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会 触发 setter,那么就能监听到了数据变化
-
compile 解析模板指令,将模板中的变量替换成数据,然后初始化 渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数 据的订阅者,一旦数据有变动,收到通知,更新视图
-
Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做 的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ② 自身必须有一个 update()方法 ③待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调,则成功 成身退。
-
MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input)-> 数据 model 变更的双向绑定效果。
3. MVVM、MVC 的区别
-
MVC
MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应 用的响应操作,当用户与页面产生交互的时候,Controller 中的事 触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修 改,然后 Model 层再去通知 View 层更新。 -
MVVM
MVVM 分为 Model、View、ViewModel:
Model 代表数据模型,数据和业务逻辑都在 Model 层中定义;
View 代表 UI 视图,负责数据的展示;
ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理 用户交互操作;
Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 Model 中 的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改 变的数据也会在 Model 中同步。
这种模式实现了 Model 和 View 的数据自动同步,因此开发者只需要 专注于数据的维护操作即可,而不需要自己操作 DOM。
4. 单页应用与多页应用的区别
-
SPA 单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次 js、css 等相关资源。所有内容都 包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换 相关组件,仅仅刷新局部资源。
-
MPA 多页面应用 (MultiPage Application),指有多个独立页面的 应用,每个页面必须重复加载 js、css 等相关资源。多页应用跳转,需要整页资源刷新。
5. 为何组件 data 必须是一个函数
在 Vue 中,组件的 data 必须是一个函数,这是为了确保每个组件实例都有独立的数据作用域。函数形式的 data 会返回一个新的数据对象,当多个组件实例化时,每个实例都可以拥有自己的独立数据副本,避免数据相互污染。如果 data 是一个对象,则所有实例会共享同一个数据对象,导致状态被意外修改。这个设计主要是为了保证组件的复用性和稳定性。
首先组件data必须是一个函数,最根本的原因在于我们定义的vue组件是一个class(类),每个地方去使用这个组件的时候,相当于这个类的一个实例化,确保每个组件实例都有独立的数据作用域,避免相互污染。
6. Vue data 中某一个属性的值发生改变后,视图会立即同步执 行重新渲染吗
不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之 后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在 缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要 的。然后,在下一个的事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作。
7. 异步更新DOM
- 为什么 Vue 要异步更新 DOM
- 批量合并更新:同一事件循环内多次修改响应式数据,Vue 会合并为一次 DOM 更新,避免重复渲染,提升性能。
- 保持一致性:数据变更是同步的,但 DOM 更新被延后到本轮任务末尾(微任务优先),保证在你本次逻辑结束后再进行统一渲染。
例:在同一次点击回调里连改多次状态,DOM 只重渲染一次。
- 更新何时发生(事件循环视角)
- Vue 会在当前宏任务内收集依赖变更,调度一个异步刷新队列的任务。
- 刷新顺序:微任务优先(Promise.then/MutationObserver)> 宏任务(setTimeout 等)。
- 刷新时机:当前同步代码执行完后,进入微任务队列,执行“渲染队列”,触发虚拟 DOM diff,再更新真实 DOM。
8. 计算属性和监听的区别
- 计算属性:
- 基于现有响应式数据“派生出新数据”;用于模板展示或进一步计算。
- 会缓存,依赖不变时多次访问不会重新计算。
- 以属性的形式使用,如在模板或代码中直接访问
this.fullName。 - 计算属性有返回值return。
- 当其依赖的响应式数据发生变化且该属性被访问/渲染时才重新求值。
- 监听器:
- 在某个响应式数据变化时“执行副作用逻辑”;用于异步请求、手动控制复杂逻辑等。
- 不缓存,每次被监听的值变化时都会触发回调。
- 以回调的形式配置 watch,监听某个数据或计算结果的变化。
- 执行副作用(可发请求、更新其他状态、节流/防抖、路由跳转等),无返回值约束。
- 当被监听的源值变化时立即调用回调(可选 immediate 首次触发)。
9. Vue生命周期
- beforeCreate:创建之前,此时还没有data和Method
- Created:创建完成,此时data和Method可以使用了
- beforeMount:在渲染之前
- mounted:页面已经渲染完成,并且vm实例中已经添加完$el了,已经替换掉那些DOM元素了(双括号中的变量),这个时候可以操作DOM了(但是是获取不了元素的高度等属性的,如果想要获取,需要使用nextTick())
- beforeUpdate:data改变后,对应的组件重新渲染之前
- updated:data改变后,对应的组件重新渲染完成
- beforeDestory:在实例销毁之前,此时实例仍然可以使用
- destoryed:实例销毁后
父子组件的生命周期:
- 渲染的过程:父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
- 子组件更新过程:父beforeUpdate->子beforeUpdate->子updated->父updated
- 父组件更新过程:父beforeUpdate->父updated
- 销毁过程:父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
10. $nextTick 原理及作用
- 解释
- nextTick:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
- 应用
- 想要在Vue生命周期函数中的created()操作DOM可以使用Vue.nextTick()回调函数
- 在数据改变后要执行的操作,而这个操作需要等数据改变后而改变DOM结构的时候才进行操作,需要用到nextTick。
11. Vue相关指令问题
- 为什么避免v-for和v-if在一起使用
Vue 处理指令时,v-for 比 v-if 具有更高的优先级, 虽然用起来也没报错好使, 但是性能不高, 如果你有5个元素被v-for循环, v-if也会分别执行5次 - Vue中Key值作用
key值的作用是给元素添加一个唯一的标识符,提高vue渲染性能。当数据变化的时候,vue就会使用diff算法对比新旧虚拟Dom。 如果遇到相同的key值,则复用元素。如果遇到不同的key值则强制更新。 - Vue中有时候数组会更新页面,有时候不更新为什么
因为vue内部只能监测到数组顺序/位置的改变/数量的改变, 但是值被重新赋予监测不到变更, 可以用 Vue.set() / vm.$set() - v-if 和 v-show 的区别
v-show 通过CSS display 控制显示与隐藏
v-if 是组件真正的渲染和销毁,而不是显示与隐藏 - 自定义指令
局部注册和使用
<template><div><!-- <input type="text" v-gfocus> --><input type="text" v-focus></div>
</template><script>
// 目标: 创建 "自定义指令", 让输入框自动聚焦
// 1. 创建自定义指令
// 全局 / 局部
// 2. 在标签上使用自定义指令 v-指令名
// 注意:
// inserted方法 - 指令所在标签, 被插入到网页上触发(一次)
// update方法 - 指令对应数据/标签更新时, 此方法执行
export default {data(){return {colorStr: 'red'}},directives: {focus: {inserted(el){el.focus()}}}
}
</script>
全局注册
// 在main.js用 Vue.directive()方法来进行注册, 以后随便哪个.vue文件里都可以直接用v-gfocus指令
Vue.directive("gfocus", {inserted(el) {el.focus() // 触发标签的事件方法}
})
自定义指令-传值
// el为绑定指令dom对象,binding为所传值
Vue.directive('color', {inserted(el, binding) {el.style.color = binding.value},update(el, binding) {el.style.color = binding.value}
})
12. Vue组件通信
- 父传子:子组件设置props + 父组件设置v-bind:/:
- 子传父:子组件的$emit + 父组件设置v-on/@
- 任意组件通信:新建一个空的全局Vue对象,利用 e m i t 发 送 , emit发送, emit发送,on接收
Vue.prototype.Event=new Vue();
Event.$emit(事件名,数据);
Event.$on(事件名,data => {})
- 祖先组件使用provide提供数据,子孙组件通过inject注入数据
// Provider.vue
export default {data() {return {theme: { color: "#409EFF" } // 对象是响应式的};},provide() {return {theme: this.theme};},template: `<div><slot></slot><button @click="theme.color = '#67C23A'">切换主题</button></div>`
};// Child.vue
export default {inject: ["theme"],template: `<div :style="{ color: theme.color }">当前主题色:{{ theme.color }}</div>`
};
- ref—$refs:通过this.$refs.XXX获取子组件的属性或方法。
- 直接获取父组件:this.$parent。
- 直接获取根组件:this.$root。
- keep-alive:缓存当前组件,生命周期:deactivated、activated。
<component :is="comName"></component>通过控制is属性绑定相应组件名称可实现动态组件展示
13. Vuex相关属性
- state∶ 进行数据存储。
this.$store.state.xxx - getters∶ 针对于state数据进行二次计算。this.$store.getters.xxx
- mutatioins:状态改变操作方法,是 Vuex 修改 state 的唯一推荐方法,该方法只能进行同步操作,且 方法名只能全局唯一。
this.$store.commit(“方法名”,数据) - actions:存放异步方法的,并且是来提交mutations。
this.$store.dispatch(“方法名”,数据) - modules:把vuex再次进行模块之间的划分
Vuex 和 localStorage 的区别:
(1)存储方式
- vuex 存储在内存中
- localstorage 则以文件的方式存储在本地,只能存储字符串类型的 数据,存储对象需要 JSON 的 stringify 和 parse 方法进行处理。 读 取内存比读取硬盘速度要快
(2)应用场景
- Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集 中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一 种可预测的方式发生变化。vuex 用于组件之间的传值。
- ocalstorage 是本地存储,是将数据存储到浏览器的方法,一般是 在跨页面传递数据时使用 。
- Vuex 能做到数据的响应式,localstorage 不能
(3)永久性
- 刷新页面时 vuex 存储的值会丢失,localstorage 不会。
注意:对于不变的数据确实可以用 localstorage 可以代替 vuex,但 是当两个组件共用一个数据源(对象或数组)时,如果其中一个组件 改变了该数据源,希望另一个组件响应该变化时,localstorage 无 法做到,原因就是区别 1。
14. slot/插槽
- 匿名插槽
<template><div class="child"><h2>我是子组件的标题</h2>// 默认插槽<slot></slot></div>
</template><template><div><DefaultSlotChild>// 这里的内容会被渲染到子组件的默认插槽中<p>这是来自父组件的默认插槽内容。111</p><p>这是来自父组件的默认插槽内容。222</p></DefaultSlotChild></div>
</template><script>
import DefaultSlotChild from './DefaultSlotChild.vue';export default {components: {DefaultSlotChild}
}
</script>
- 具名插槽
// 父组件
<child-component>
<template slot="header"><h1>这是头部内容</h1></template>
<template slot="footer"><p>这是底部内容</p></template>
</child-component>// 子组件
<template>
<slot name="header"></slot>
<slot name="footer"></slot>
</template>
- 作用域插槽
// 父组件
<child-component>
<template slot="item" slot-scope="{text}">
// 可简写为 <template #item="{text}">
<p>{{ text}}</p>
</template>
</child-component>// 子组件
<template>
<slot name="item" text="itemText"></slot>
</template>
15. 路由
在单页应用(SPA)中。它描述的是URL与UI之间的映射关系,即当用户访问不同的URL时,前端应用会相应地展示不同的界面或组件,而无需重新从服务器加载整个页面。
路由分为:后端路由和前端路由
后端路由:由服务器端进行实现并实现资源映射分发(nodejs、jsp、php等)
- 概念:根据不同的用户URL请求,返回不同的内容(地址与资源产生对应关系)
- 本质:URL请求地址与服务器资源之间的对应关系(映射)
前端路由:根据不同的事件来显示不同的页面内容,是事件与事件处理函数之间的对应关系
- 概念:根据不同的用户事件,显示不同的页面内容(地址与事件产生对应关系)
- 本质:用户事件与事件处理函数之间的对应关系
前端路由的实现方式
hash模式:
- 用 # 后的片段作为前端路由标识,变化不会触发服务器请求。
- 依赖浏览器的 hashchange 事件,历史记录由浏览器维护。
- 服务器只看到 # 之前的路径,通常是根路径,不需要后端配合。
history模式:
- 使用 HTML5 History API(pushState/replaceState),改变路径但不刷新页面。
- 刷新或直接访问非根路径时,浏览器会向服务器请求该路径,需服务器正确返回前端入口文件。
- 更贴近真实路径,URL 更干净。
$route 与$router的区别
$router是用于做编程式导航的(改变路由的);
$route是用户获取路由信息的。
路由跳转与传参方式
query传参:
- 以?形式拼接在路由中,示例:/user?id=123
- 用path指定目标路由,query传递参数
this.$route.query接收参数
this.$router.push({
path:'/naws',
query:{id:'123'}
})
params传参
- 不在路由中展示,URL地址栏传参隐藏
- 刷新页面会导致数据丢失
- 用name指定目标路由,params传递参数
this.$route.params接收参数
this.$router.push({
name:'News',
params:{name:'xzl'}
})
以上方式都可以实现路由跳转,统称为编程式导航,使用<router- link to="xxx"></ router- link>为声明式导航。
动态路由
所谓动态路由就是路由规则中有部分规则是动态变化的,不是固定的值,需要去匹配取出数据(即路由参数)。
- 如何传递
在声明路由的时候,将可变部分通过“:变量名”的形式进行替代 - 如何获取
获取this.$route来获取
// router.js
{path:'/about/:id?', // ?表示非必传name: 'About',component: () => import('@/views/About.vue')
}<router-link to='/about/123'>about</router-link>console.log(this.$route.params.id) // 123
嵌套路由
嵌套路由用于表达页面的层级结构:父路由渲染公共布局(如头部、侧栏、面包屑),子路由在父布局的占位处切换显示相应页面。
// 懒加载页面(推荐)
const Layout = () => import('@/layouts/AppLayout.vue')
const Home = () => import('@/views/Home.vue')
const Users = () => import('@/views/users/Users.vue')
const UserDetail = () => import('@/views/users/UserDetail.vue')const routes = [{path: '/',component: Layout, // 父路由:提供公共布局children: [{ path: '', name: 'home', component: Home }, // 默认子路由(相对路径为空){ path: 'users', name: 'users', component: Users }, // /users{ path: 'users/:id', name: 'user-detail', component: UserDetail } // /users/123]}
]<template><div class="layout"><header>Header</header><aside>Sidebar</aside><main><!-- 子路由会渲染到这里 --><router-view /></main></div>
</template>
路由守卫
路由守卫有三种:
- 全局钩子: beforeEach(跳转路由后进入页面前触发)、 afterEach(跳转路由后进入页面后触发)
- 独享守卫(单个路由里面的钩子): beforeEnter、 beforeLeave
- 组件内守卫:beforeRouteEnter、 beforeRouteUpdate、 beforeRouteLeave
每个守卫方法接收三个参数:
- to: Route: 即将要进入的目标路由对象(to是一个对象,是将要进入的路由对象,可以用to.path调用路由对象中的属性)
- from: Route: 当前导航正要离开的路由
- next: Function: 这是一个必须需要调用的方法,执行效果依赖 next 方法的调用参数。
前置路由守卫(每次切换前被调用)
{path: '/',name: 'Home',component: () => import('../views/Home.vue'),meta: { isAuth: true, title:'主页' },
},//全局前置路由守卫————初始化的时候被调用、每次路由切换之前被调用
router.beforeEach((to, from, next) => {//如果路由需要跳转if (to.meta.isAuth) {//判断 如果school本地存储是qinghuadaxue的时候,可以进去if (localStorage.getItem('school') === 'qinghuadaxue') {next() //放行} else {alert('抱歉,您无权限查看!')}} else {// 否则,放行next()}
后置路由守卫(每次切换后被调用)
//全局后置路由守卫————初始化的时候被调用、每次路由切换之后被调用
router.afterEach((to, from) => {document.title = to.meta.title || '默认名' //修改网页的title
})
独享路由守卫(某一个路由所单独享用的路由守卫)
{path: '/',name: 'Home',component: () => import('../views/Home.vue'),meta: { isAuth: true },beforeEnter: (to, from, next) => {if (to.meta.isAuth) { //判断是否需要授权if (localStorage.getItem('school') === 'qinghuadaxue') {next() //放行} else {alert('抱歉,您无权限查看!')}} else {next() //放行}}
},
组件内守卫(写在路由组件内部的路由守卫)
//通过路由规则,进入该组件时被调用
beforeRouteEnter(to,from,next) {if(toString.meta.isAuth){if(localStorage.getTime('school')==='qinghuadaxue'){next()}else{alert('学校名不对,无权限查看!')}} else{next()}
},//通过路由规则,离开该组件时被调用
beforeRouteLeave(to,from,next) {next()
}
16.diff算法
Diff 算法是一种对比算法。对比两者是 旧虚拟 DOM 和新虚拟 DOM,对比出是哪个 虚拟节点更改了,找出这个 虚拟节点并只更新这个虚拟节点所对应的 真实节点而不用更新其他数据没发生改变的节点,实现 精准地更新真实 DOM,进而 提高效率
在新老虚拟 DOM 对比时:
- 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
- 如果为相同节点,进行 patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的 children 没有子节点,将旧的子节点移除)
- 比较如果都有子节点,则进行 updateChildren,判断如何对这些新老节点的子节点进行操作(diff 核心)。
- 匹配时,找到相同的子节点,递归比较子节点
Vue3
1. Vue3 相比 Vue2 的区别
- 响应式系统重写
- vue2通过Object.defineProperty遍历每个属性,监听劫持数据进行更改
- vue3使用Proxy监听整个对象变化
// Vue 2 响应式片段
data() { return { list: [1,2,3] }
},
mounted() {this.list[5] = 10; // 无法被劫持 -> 不会触发视图更新
}// Vue 3 响应式片段
import { reactive } from 'vue';const state = reactive({ list: [1,2,3] });
state.list[5] = 10; // 自动触发更新
- 组合式API vs 选项式API
- Vue 2 选项式组织方式–逻辑分散
- Vue 3 组合式API(Composition API)–逻辑复用能力好
// vue2选项式
export default {data() { return { count: 0 } },methods: { increment() { this.count++ } },mounted() { console.log('已加载') }
}// vue3组合式
import { ref, onMounted } from 'vue';export default {setup() {const count = ref(0);const increment = () => count.value++;onMounted(() => {console.log('组件已挂载');});return { count, increment };}
}
- 性能优化提升
- 编译器优化:在编译阶段标记哪些是动态内容(如 {{ count }}),更新时跳过静态内容(如纯文字)。
- Tree-shaking支持:按需引入,只打包你用到的功能,减少代码体积
- Fragment支持:多根节点无需额外包裹div
- 新组件
<Teleport>:把组件渲染到任意位置(比如弹窗放到 body 下,避免被父组件样式影响)。<Suspense>:优雅处理异步加载(比如数据加载时显示 Loading 动画)。它可以定义一个fallback内容,在异步组件加载完成前显示。
// teleport
<teleport to="#modal-container"><div class="modal">模态框内容</div>
</teleport>// Suspense
<Suspense><template #default><AsyncComponent/></template><template #fallback>加载中...</template>
</Suspense>
2. Vue3 的响应式原理
- Proxy 代理对象:
-通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等- 通过Reflect(反射): 对源对象的属性进行操作。
- 对比 Vue2:Vue2 使用 Object.defineProperty,无法监听新增属性和数组下标变化(必须用 this.$set)
new Proxy(data, {// 拦截读取属性值get (target, prop) {return Reflect.get(target, prop)},// 拦截设置属性值或添加新属性set (target, prop, value) {return Reflect.set(target, prop, value)},// 拦截删除属性deleteProperty (target, prop) {return Reflect.deleteProperty(target, prop)}
})
proxy.name = 'tom'
3. ref 和 reactive 的区别
ref:- 用于包装 基本类型(数字、字符串等),因为 Proxy 无法直接监听基本类型。
- 使用方式:必须通过 .value 访问。
- 通过Object.defineProperty()的get与set来实现响应式(数据劫持)
const count = ref(0);
console.log(count.value); // 0
count.value++;
reactive:- 用于包装 对象/数组,可以直接访问属性。
- 通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
const user = reactive({ name: '张三' });
console.log(user.name); // 张三
user.name = '李四';
4. toRef和toRefs
- toRef用于将响应式对象的某个属性转换为ref
- toRefs用于将响应式对象的所有属性转换为ref对象,并返回一个包含这些ref的普通对象
- 这两个函数常用于在解构响应式对象时保持响应性
const user = reactive({ name: 'John', age: 30 })
const nameRef = toRef(user, 'name')
const { name, age } = toRefs(user)
5. shallowReactive和shallowRef
- shallowReactive创建一个浅响应式对象,只监听对象的顶层属性变化
- shallowRef创建一个浅响应式ref,只监听.value的变化,不关心.value内部的变化
- 这两个函数常用于优化性能,避免不必要的深度响应式
const shallowObj = shallowReactive({ a: 1, b: {
c: 2 } })
// 修改shallowObj.a会触发更新
// 修改shallowObj.b.c不会触发更新
const shallowValue = shallowRef({ a: 1 })
// 修改shallowValue.value会触发更新
// 修改shallowValue.value.a不会触发更新
6. readonly和shallowReadonly
- readonly创建一个只读的响应式对象,任何修改都会被阻止
- shallowReadonly创建一个浅只读的响应式对象,只阻止顶层属性的修改
- 这两个函数常用于保护不可变数据
const user = readonly({ name: 'John', age: 30 })
// 尝试修改user.name会被阻止
const shallowUser = shallowReadonly({ name:
'John', address: { city: 'Beijing' } })
// 尝试修改shallowUser.name会被阻止
// 但可以修改shallowUser.address.city
7. toRaw 与 markRaw
- toRaw:将一个由reactive生成的响应式对象转为普通对象。
- toRaw:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
- markRaw:标记一个对象,使其永远不会再成为响应式对象。
- markRaw:有些值不应被设置为响应式的,例如复杂的第三方类库等。
8. vue3响应式数据的判断
- isRef: 检查一个值是否为一个 ref 对象
- isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
- isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
- isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理
9. 生命周期
vue2 vue3

10. setup函数
- 理解:Vue3.0中一个新的配置项,值为一个函数。
- setup是所有Composition API(组合API)“ 表演的舞台 ”。
- 组件中所用到的:数据、方法等等,均要配置在setup中。
- setup函数的两种返回值:
- 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)
- 若返回一个渲染函数:则可以自定义渲染内容。(了解)
- setup的几个注意点
- setup执行的时机
- 在beforeCreate之前执行一次,this是undefined。
- setup的参数
- props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。
- context:上下文对象
- attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于 this.$attrs。
- slots: 收到的插槽内容, 相当于 this.$slots。
- emit: 分发自定义事件的函数, 相当于 this.$emit。
- setup不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)
- setup执行的时机
11. watch 和 watchEffect
watch:明确监听某个数据,适合精确控制(比如监听搜索关键词变化,触发请求)。
watch( keyword, (newVal) => { fetchData(newVal) }, { immediate: true } // 立即执行一次
);
watchEffect:自动收集依赖,并在依赖变化时重新执行。与watch不同,watchEffect不需要明确指定要监听的数据,而是会自动追踪函数内部使用的响应式数据。watchEffect通常用于处理副作用,如数据fetching、DOM操作等。
watchEffect(() => { console.log('关键词和页码变化了:', keyword.value, page.value); fetchData();
});
12. 使用expose和ref实现组件通信
子组件可以通过expose暴露方法或属性,父组件通过ref获取子组件实例并调用这些方法或访问属性。
// 子组件
import { ref, defineExpose } from 'vue'
const count = ref(0)
const increment = () => { count.value++ }
defineExpose({ count, increment })
// 父组件
import { ref, onMounted } from 'vue'
export default {setup() {const childRef = ref(null)
onMounted(() => {// 访问子组件的count属性console.log(childRef.value.count)// 调用子组件的increment方法childRef.value.increment()})
return { childRef }}
}
13. 什么是静态提升
静态提升是指将模板中的静态节点(不包含动态数据的节点)提升到渲染函数之外,避免在每次渲染时都重新创建这些节点。这可以减少内存占用和提高渲染性能。Vue3的编译器会自动进行静态提升。
14. 什么是PatchFlag
PatchFlag是Vue3中新增的一个标记,用于标记动态节点的变化类型(如文本变化、属性变化等)。在diff过程中,Vue可以根据PatchFlag只检查节点的特定部分,而不是整个节点,从而提高diff效率。常见的PatchFlag包括TEXT、CLASS、STYLE、PROPS等。
15. 如何优化Vue3的渲染性能
- 使用v-once指令缓存静态内容
- 使用v-memo指令缓存动态内容
- v-memo指令用于缓存模板中的一部分内容,只有当依赖的数据变化时才会重新渲染。它接收一个依赖数组,当数组中的任何一个元素变化时,才会重新渲染缓存的内容。v-memo可以用于优化频繁渲染的列表或复杂组件。
<div v-memo="[user.id, user.name]">{{ user.name }} - {{ user.id }}
</div>
- 避免不必要的响应式数据
- 使用shallowReactive和shallowRef减少响应式开销
- 合理使用组件拆分,提高组件复用性
- 使用keep-alive缓存组件状态
- 优化大型列表渲染
- 使用虚拟滚动库(如vue-virtual-scroller),只渲染可视区域内的列表项
- 使用v-memo指令缓存列表项
- 避免在列表项中使用复杂的计算属性
- 合理设置key,避免不必要的DOM操作
- 懒加载列表数据,分页加载
16. Pinia和Vuex的区别
- Pinia是Vue3推荐的状态管理库,Vuex是Vue2的官方状态管理库
- Pinia没有mutations,直接通过actions修改状态
- Pinia支持TypeScript,类型推断更好
- Pinia的体积更小,性能更好
- 定义使用
import { defineStore } from 'pinia'
export const useCounterStore = defineStore
('counter', {state: () => ({ count: 0 }),getters: {doubleCount: (state) => state.count * 2},actions: {increment() {this.count++},async fetchCount() {// 异步操作const res = await fetch('/api/count')this.count = await res.json()}}
})
- 组件中使用
import { useCounterStore } from './stores/counter'
import { computed } from 'vue'
export default {setup() {const counterStore = useCounterStore()
// 访问stateconst count = computed(() => counterStore.count)
// 访问getterconst doubleCount = computed(() => counterStore.doubleCount)
// 调用actionconst increment = () => counterStore.increment()
return { count, doubleCount, increment }}
}
- 状态持久化:将状态保存到本地存储(如localStorage、sessionStorage)中,以便在页面刷新或重新打开时恢复状态。可以使用pinia-plugin-persistedstate等插件实现,也可以手动在actions中保存状态。
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// store.js
export const useCounterStore = defineStore
('counter', {state: () => ({ count: 0 }),// 启用持久化persist: true
})
