一、v-bind和v-model的区别:
1.v-bind是单向数据绑定指令,主要用于把数据动态绑定到HTML属性或组件props上,:是它的简写,日常开发基本都用冒号写法
<img :src="imgUrl">
<a :href="linkUrl">跳转</a>
2.v-model是表单双向绑定指令,本质是v-bind:value加上input事件的语法糖,能实现数据层和视图层的双向同步,主要用在input,select
,checkbox这类表单元素上
<input v-model="msg" />
<p>{{msg}}</p>
<input :value="msg" @input="msg = $event.target.value" />
二、创建一个vue项目
bash
npm create vue@latest
2. 跟着提示选择
- 输入项目名
- 框架选择:Vue
- 语法选择:JavaScript 或 TypeScript
三、vue组件的通信方式
1.props / $emit
可以实现父子组件的双向通信,父组件通过 props 的方式向子组件传递数据,而通过 $emit 子组件可以向父组件通信。
父组件代码:
vue
//父组件
<template>
<div style="width: 100px; background-color: blueviolet">
<h3>我是父组件</h3>
<span>这是子组件传递过来的消息:</span>
<span>{{ b }}</span>
<userInfo :msg="fatherMsg" @updateMsg="getMsg"></userInfo>
</div>
</template>
<script setup lang="ts">
import userInfo from './components/useInfo.vue'
import { ref } from 'vue'
const fatherMsg = ref('这是父组件传递的消息')
let b = ref('')
const getMsg = (a: any) => {
console.log(a)
b.value = a
}
</script>
<style scoped lang="scss"></style>
子组件代码:
vue
<template>
<div style="width: 50%; background-color: bisque">
<h5>我是子组件</h5>
<span>{{ msg }}</span>
<button @click="fatherMsg">点击我发送消息给父组件</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { Prop } from 'vue'
const props = defineProps({
msg: String,
})
const emit = defineEmits(['updateMsg'])
const fatherMsg = () => {
const father = 'ncifelnv '
emit('updateMsg', father)
}
</script>
<style scoped lang="scss"></style>
2.ref
父组件通过绑定 ref 的方式获取组件实例。选项式 API 中,引用将被注册在组件的 this.$refs 中。
父组件:
vue
<template>
<div>
<h3>父组件</h3>
<!-- 给子组件绑定 ref -->
<Child ref="childRef" />
<button @click="getSon">获取子组件</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
// 定义 ref 名称 = 模板上的 ref 名称
const childRef = ref(null)
const getSon = () => {
// 获取子组件实例
console.log(childRef.value)
// 获取子组件数据
console.log(childRef.value.msg)
// 调用子组件方法
childRef.value.childFn()
}
</script>
子组件:
vue
<template>
<div>我是子组件</div>
</template>
<script setup>
import { ref } from 'vue'
// 子组件数据
const msg = ref('子组件消息')
// 子组件方法
const childFn = () => {
console.log('我是子组件方法')
}
// ✅ 必须暴露,父组件才能拿到
defineExpose({
msg,
childFn
})
</script>
3.v-slot(插槽)
可以实现父子组件通信,在实现可复用组件、向组件中传入 DOM 节点 / HTML 内容,以及某些组件库的表格值二次处理等情况时,可以优先考虑 v-slot。
1.基础插槽(传 DOM / HTML 结构)
子组件:
vue
<template>
<div class="card">
<h3>子组件固定标题</h3>
<!-- 插槽:父组件传的内容会显示在这里 -->
<slot></slot>
</div>
</template>
<style scoped>
.card {
border: 1px solid #eee;
padding: 16px;
}
</style>
父组件:
vue
<template>
<div>
<h2>父组件</h2>
<Child>
<!-- 这里写的所有内容,都会塞进子组件的 slot 里 -->
<p>我是父组件传进来的段落</p>
<button>按钮</button>
<span style="color:red">红色文字</span>
</Child>
</div>
</template>
<script setup>
import Child from './Child.vue'
</script>
2.具名插槽
作用
一个组件需要多个插槽,给每个插槽起名字,一一对应。
子组件
vue
<div>
<slot name="header"></slot>
<slot name="main"></slot>
<slot name="footer"></slot>
</div>
父组件
用 v-slot:名字 / 简写 #名字 对应
vue
<Child>
<template #header>
<div>头部内容</div>
</template>
<template #main>
<div>主体内容</div>
</template>
<template #footer>
<div>底部内容</div>
</template>
</Child>
3.作用域插槽(实现父子通信)
子组件:
vue
<template>
<div>
<h3>子组件</h3>
<!-- 把子组件的数据绑定到 slot 上 -->
<slot :childMsg="msg" :childList="list"></slot>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 子组件内部数据
const msg = ref('来自子组件的消息')
const list = ref([1, 2, 3])
</script>
父组件:
vue
<template>
<Child v-slot="slotProps">
<!-- 父组件拿到子组件的数据 -->
<p>子组件消息:{{ slotProps.childMsg }}</p>
<p>子组件数组:{{ slotProps.childList }}</p>
</Child>
</template>
<script setup>
import Child from './Child.vue'
</script>
4.useAttrs()
能够接受父组件透传过来的数据,使用该方法能够一次获取到子组件标签上的所有属性。
官方解释 :用于获取父组件传递给子组件的、没有被 props 接收的所有属性(包括 class、style、自定义属性等),实现属性透传。
父组件:
vue
<template>
<div>
<h3>父组件</h3>
<!-- 给子组件标签上写很多属性 -->
<Child
msg="你好"
type="primary"
class="my-class"
style="color:red;font-size:20px"
@click="handleClick"
/>
</div>
</template>
<script setup>
import Child from './Child.vue'
const handleClick = () => alert('点击了')
</script>
子组件:
vue
<template>
<div>我是子组件</div>
<!-- 自动绑定所有属性:class/style/事件都会生效 -->
<div v-bind="attrs">我会继承所有属性</div>
</template>
<script setup>
import { useAttrs } from 'vue'
// 获取所有透传属性
const attrs = useAttrs()
console.log(attrs)
</script>
5.provide / inject
父组件中通过 provide 来提供变量,子组件及子代组件通过 inject 来注入变量。主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态。
1. 顶层父组件(App.vue)
vue
<template>
<div>
<h2>我是祖先组件</h2>
<Child />
</div>
</template>
<script setup>
import { provide, ref } from 'vue'
import Child from './Child.vue'
// 提供数据:所有后代都能 inject 拿到
const msg = ref('来自祖先的数据')
provide('token', msg)
provide('userInfo', {
name: '张三',
age: 20
})
</script>
2. 子组件(Child.vue)
中间层,什么都不用写,直接继续往下传
vue
<template>
<div>
<h4>我是子组件</h4>
<Grandson />
</div>
</template>
<script setup>
import Grandson from './Grandson.vue'
</script>
3. 孙组件(Grandson.vue)
跨级获取,直接 inject 注入
vue
<template>
<div>
<h5>我是孙组件</h5>
<p>拿到祖先数据:{{ token }}</p>
<p>姓名:{{ userInfo.name }}</p>
<p>年龄:{{ userInfo.age }}</p>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 注入
const token = inject('token')
const userInfo = inject('userInfo')
</script>
6.Vuex / Pinia
允许你跨组件或页面共享状态,通过设置 store 来存储和操作数据进行全局通信。
四、ref 和 reactive 的区别
1. 数据类型支持不同
-
ref
- 支持所有类型:基本类型(string/number/boolean)+ 引用类型(对象 / 数组)
-
reactive
- 只支持引用类型:对象、数组、Map、Set 等
- 不能处理基本类型
2. 取值、赋值方式不同
-
ref
- 必须用
.value访问 / 修改 - 模板中可以省略
.value
- 必须用
-
reactive
- 直接读写,不需要
.value
- 直接读写,不需要
3. 响应式原理不同
-
ref
- 用一个对象对值进行包裹 ,通过
class RefImpl实现响应式
- 用一个对象对值进行包裹 ,通过
-
reactive
- 基于 Proxy 对对象进行深层代理
4. 解构与重新赋值是否丢失响应式
-
ref
- 直接赋值 / 替换不会丢失响应式
-
reactive
- 直接重新赋值会丢失响应式
- 解构也会丢失响应式(需要
toRefs)
5. 使用场景不同
-
ref
- 独立的基本类型变量
- 简单状态、表单输入、开关、计数器
-
reactive
- 对象、表单数据、复杂状态、一组相关数据
- 数据结构较复杂时更优雅
追问 1:为什么 ref 要有 .value?
回答:
因为 ref 可以包裹基本数据类型 ,而 JS 基本类型无法通过 Proxy 实现响应式,所以 Vue 用一个对象 把值包起来,通过访问 .value 来触发 getter/setter,从而实现响应式。
在模板里 Vue 会自动解包,所以不用写 .value。
追问 2:reactive 为什么不能直接赋值?会出现什么问题?
回答:
因为 reactive 返回的是一个 Proxy 代理对象。
如果直接重新赋值:
js
let user = reactive({ name: 'zs' })
user = { name: 'ls' } // 直接覆盖
原来的 Proxy 对象会被丢弃,响应式直接丢失。
而 ref 因为是 .value 修改,所以不会丢失。
追问 3:toRef 和 toRefs 有什么用?
回答:
用于解决 reactive 对象解构后失去响应式 的问题。
- toRef :把 reactive 里的单个属性转成 ref
- toRefs :把整个 reactive 对象批量转成 ref 集合,解构后依然响应式
示例:
js
const user = reactive({ name: 'zs', age: 18 })
// 解构后失去响应式 ❌
const { name, age } = user
// 解构后依然响应式 ✅
const { name, age } = toRefs(user)
一句话总结:
想解构 reactive 又不丢响应式,就用 toRefs。
追问 4(附加高频):ref 和 reactive 怎么选择?
标准回答:
- 独立的基本类型 (数字、字符串、布尔)→ 用 ref
- 对象、数组、表单数据 → 用 reactive
- 想要结构更清晰、代码更简洁 → 优先用 reactive
- 不确定、简单状态、hooks 返回 → 统一用 ref
五、 v-for 和 v-if
1. v-for 和 v-if哪个优先级更高?
标准答案:
v-for 优先级高于 v-if。
先循环,再逐个判断是否显示。
2. 同时使用 v-for + v-if 有什么问题?
标准答案:
-
性能极差
v-for 优先级高于 v-if,导致会先把整个列表全部循环渲染一遍,再一个个判断是否隐藏,浪费性能。
-
逻辑不清晰
容易出现预期外的显示隐藏问题。
-
Vue 官方明确不推荐 同时写在同一个标签上。
3. 怎么正确避免 v-for 和 v-if 一起用?(高频考点)
方案 1:在外层套一层 template(推荐)
把 v-if 提到外层,先判断再循环
vue
<template v-if="isShow">
<div v-for="item in list" :key="item.id">
{{ item }}
</div>
</template>
方案 2:提前计算过滤数据(computed)
先过滤出要显示的数据,再循环
vue
computed: {
activeList() {
return this.list.filter(item => item.status === 1)
}
}
vue
<div v-for="item in activeList" :key="item.id">
方案 3:v-if 放在子元素上
循环不变,只控制内部显示(适合必须循环的场景)
vue
<div v-for="item in list" :key="item.id">
<span v-if="item.status">显示</span>
</div>
4. 为什么不能让 v-for 的 key 用 index?(延伸必问题)
标准答案:
-
数组新增、删除、颠倒顺序时,index 会跟着变
-
Vue 会错误复用 DOM,导致:
- 复选框选中错乱
- 输入框内容错乱
- 动画异常
-
key 必须用唯一、稳定的值:id、uuid 等
5. v-for 中 key 的作用是什么?
标准答案:
- key 是虚拟 DOM 节点唯一标识,diff 算法用来识别节点、复用节点。
六、vue响应式原理
1. Vue2 响应式原理
Vue2 的响应式是采用数据劫持结合发布 - 订阅模式实现的。
核心是
Object.defineProperty,在初始化时递归遍历data里的所有属性,为每个属性添加getter和setter。
getter阶段 :组件渲染时读取属性,会触发getter,此时通过Dep收集当前组件的Watcher作为依赖。setter阶段 :当数据被修改时,会触发setter,通知Dep中收集的所有Watcher去更新视图。这种方式的缺陷是:无法监听对象新增 / 删除属性,也无法监听数组通过下标修改和
length的变化,而且必须递归遍历所有属性,深层对象性能开销较大。
2. Vue3 响应式原理
Vue3 改用
Proxy代理整个对象,结合Reflect和依赖收集实现响应式,解决了 Vue2 的很多限制。
- 读取属性时,通过
track收集当前的副作用函数(effect)作为依赖。- 修改、新增或删除属性时,通过
trigger通知所有依赖该属性的副作用函数重新执行,更新视图。相比 Vue2,
Proxy有几个明显优势:可以监听对象新增 / 删除属性,原生支持数组下标和length的修改,而且是惰性代理,只有用到的属性才会被代理,性能更好,同时还支持 Map/Set 等更多数据类型。
一、getter /setter 做了什么?
回答:
-
getter:收集依赖
谁用到了这个数据,就把对应的 Watcher 存起来(依赖收集)。
-
setter:触发更新
数据变化时,通知之前收集的所有 Watcher 去更新视图。
二、为什么 Vue3 用 Proxy 代替 Object.defineProperty?
标准回答:
- Proxy 代理整个对象,而 defineProperty 只能劫持单个属性
- 支持新增 / 删除属性,defineProperty 做不到
- 数组原生支持,不用重写数组方法
- 惰性劫持,用到才代理,性能更高
- 支持更多集合类型:Map、Set、WeakMap 等
三、简单说下依赖收集(track)和派发更新(trigger)
回答:
-
track 收集依赖
组件渲染访问数据 → 把当前组件的渲染 Watcher 存入该数据的依赖列表。
-
trigger 派发更新
数据被修改 → 遍历依赖列表,让所有 Watcher 执行更新,重新渲染组件。
七、computed 与 watch 内部原理(高频延伸)
回答:
- computed 是 描述依赖响应式状态的复杂逻辑
- watch 每次响应式属性发生变化时的监听
区别:
1.计算属性存在有缓存,页面下次更新时。如果依赖没有发生变化,计算属性不会触发
2.计算属性内部存在有return,不适用于异步请求或者更改DOM
3.计算属性会在页面首次加载时就触发,watch需要配置immediate:true
4.计算属性对于数据是深度监听,watch需要配置deep:true
八、v-if 和 v-show 的区别?(必延伸)
-
v-if和v-show都是 Vue 中用来控制元素显示隐藏的指令,但它们的实现原理、开销和适用场景完全不同:- 实现原理不同
-
v-if是真正的条件渲染 :它会根据条件的真假,完全销毁或重建元素 / 组件。如果初始条件为假,元素甚至不会被渲染到 DOM 中。 -
v-show只是通过设置元素的displayCSS 属性来控制显示隐藏,元素始终会被渲染并保留在 DOM 中。
- 性能开销不同
-
v-if切换开销大:每次切换都会触发组件的销毁和重建,还会触发组件的生命周期钩子。 -
v-show初始化开销大,但切换非常快:它只是简单地修改 CSS 属性,适合频繁切换的场景。
- 语法支持不同
-
v-if支持<template>标签分组,也能和v-else、v-else-if搭配使用。 -
v-show不支持<template>,也不能和v-else搭配。
- 使用场景不同
v-if适合条件很少改变的场景,比如权限控制、页面模块的按需加载。v-show适合需要频繁切换的场景,比如选项卡切换、折叠面板等。
另外补充一点:
v-if和v-for同时使用时,v-if的优先级更高,但官方并不推荐在同一元素上同时使用它们。
九、动态组件和异步组件
一、动态组件(<component :is="xxx">)高频题
1. 什么是动态组件?有什么作用?
标准答案:
动态组件是 Vue 提供的一种机制,通过 <component :is="组件名/组件对象"> 来实现在同一个位置动态切换不同组件。
作用:
- 用于多组件切换场景,比如 Tab 标签页、步骤条、选项卡等。
- 不用写多个
v-if来控制显示隐藏,代码更简洁。
示例:
vue
<template>
<component :is="currentTab"></component>
</template>
2. 动态组件切换时,组件会被销毁吗?如何保留状态?
标准答案:
- 默认情况下,动态组件切换时,被切换掉的组件会被销毁,状态也会丢失。
- 如果需要保留组件状态,可以配合
<keep-alive>组件使用,让被切换掉的组件保持 "存活" 状态。
示例:
vue
<keep-alive>
<component :is="currentTab"></component>
</keep-alive>
3. 动态组件和 v-if 有什么区别?
标准答案:
| 对比项 | 动态组件 | v-if |
|---|---|---|
| 用途 | 多个组件之间切换 | 条件性渲染单个元素 / 组件 |
| 性能 | 切换时默认会销毁重建,配合 keep-alive 可缓存 |
条件为假时会销毁组件,切换开销大 |
| 代码简洁度 | 只需一行 <component :is="xxx"> |
多个 v-if/v-else,代码冗余 |
二、异步组件(defineAsyncComponent)高频题
1. 什么是异步组件?有什么作用?
标准答案:
异步组件是指 Vue 提供的、用于延迟加载组件的一种方式,通过 defineAsyncComponent 来定义。
作用:
- 打包时,异步组件会被抽离成单独的 chunk(分包)。
- 页面首次加载时,只有用到该组件时才会加载,减少首屏加载体积,提高首屏加载效率。
- 常用于大型项目中,比如路由懒加载、非首屏组件、权限组件等。
示例:
vue
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
2. 异步组件的实现原理是什么?
标准答案:
defineAsyncComponent接收一个返回 Promise 的加载函数(通常配合 ES 动态import()使用)。- 当组件需要渲染时,Vue 才会执行这个加载函数,拉取组件代码。
- 构建工具(Webpack/Vite)会把异步组件打包成单独的文件,实现按需加载。
3. 异步组件和普通组件有什么区别?
标准答案:
- 普通组件:打包时会被打包进主 bundle,页面首次加载就会加载所有组件。
- 异步组件:打包时会被抽成单独的 chunk,仅在使用时才加载,优化首屏加载性能。
三、延伸高频对比题
1. 动态组件和异步组件的区别?
标准答案:
- 动态组件 :解决的是「组件切换」的问题,核心是
:is。 - 异步组件 :解决的是「按需加载」的问题,核心是
defineAsyncComponent。 - 两者可以结合使用:比如 Tab 页里的组件,既可以用动态组件切换,也可以用异步组件懒加载。
2. 异步组件和路由懒加载的关系?
标准答案:
路由懒加载本质上就是异步组件的一种应用,通过 () => import('xxx') 实现路由组件的按需加载,和 defineAsyncComponent 的底层原理是一样的。
十、keep-alive的作用和如何实现缓存
一、基本用法(图片里的示例)
它常和动态组件 <component :is="xxx"> 一起使用,通过 include、exclude 来控制哪些组件需要缓存:
vue
<!-- 只缓存名为 a 和 b 的组件 -->
<keep-alive :include="/a|b/">
<component :is="view" />
</keep-alive>
<!-- 不缓存名为 a 和 b 的组件 -->
<keep-alive exclude="a,b">
<component :is="view" />
</keep-alive>
二、如何实现缓存?(原理)
-
缓存容器 :
keep-alive内部维护了一个cache对象,用来存储已经创建好的组件实例。 -
切换逻辑:
- 当组件第一次被渲染时,会创建实例并存入
cache。 - 再次切换回来时,直接从
cache中取出实例,挂载到 DOM 上,而不是重新创建。
- 当组件第一次被渲染时,会创建实例并存入
-
注意:被切换掉的组件会被从 DOM 中移除,但组件实例本身仍保留在缓存中,状态不会丢失。
三、生命周期钩子(图片里的 activated / deactivated)
被 keep-alive 缓存的组件,不会重复执行 created / mounted / destroyed,而是触发这两个专属钩子:
activated:组件被激活(重新显示)时触发deactivated:组件被停用(隐藏)时触发
这两个钩子是 keep-alive 组件独有的,常用于处理组件激活 / 失活时的逻辑,比如数据刷新、定时器启停等。
十一、vue生命周期
1. 什么是 Vue 的生命周期?
标准答案:
Vue 实例从创建、初始化、挂载、更新到销毁的完整过程,就是它的生命周期。
通俗来说,就是一个 Vue 实例从 "出生" 到 "死亡" 的全过程。
2. 什么是生命周期钩子?
标准答案:
生命周期钩子(钩子函数),就是 Vue 实例在生命周期的特定阶段自动触发执行的函数。
我们可以在这些钩子中编写自定义逻辑,比如初始化数据、操作 DOM、清理资源等。
3. Vue 实例的完整生命周期流程是怎样的?
标准答案:
完整流程按顺序分为 4 个核心阶段:
- 创建阶段 :实例初始化 → 数据观测(
beforeCreate→created) - 挂载阶段 :模板编译 → DOM 挂载(
beforeMount→mounted) - 更新阶段 :数据变化 → 视图更新(
beforeUpdate→updated) - 销毁阶段 :实例卸载 → 资源清理(
beforeDestroy/beforeUnmount→destroyed/unmounted)
一句话串流程:
创建实例 → 初始化数据 → 编译模板 → 挂载 DOM → 数据更新 → 视图重渲染 → 实例销毁。
4. 请按阶段简述每个钩子的执行时机和用途
标准答案:
-
创建阶段
-
beforeCreate:实例刚创建,data、methods都未初始化,几乎不用。 -
created:实例创建完成,data已初始化,但 DOM 还未生成。👉 常用:
发起异步请求、初始化数据
-
-
挂载阶段
-
beforeMount:模板编译完成,即将挂载到 DOM,此时还拿不到真实 DOM。 -
mounted:DOM 挂载完成,可以访问$el操作真实 DOM。($el是 Vue 组件挂载后,组件实例自动创建的一个引用,指向组件模板渲染出来的最外层 DOM 元素。)👉 常用:
DOM 操作、第三方库初始化(如图表、地图)
-
-
更新阶段
beforeUpdate:数据更新时触发,DOM 还未更新,可获取更新前的 DOM 状态。updated:数据更新完成,DOM 已更新,避免在此修改数据,防止死循环。
-
销毁阶段
-
beforeDestroy:实例销毁前触发,组件还可用。👉 常用:
清除定时器、解绑事件、销毁第三方实例,清理Websocket,防止内存泄漏
-
destroyed:实例已完全销毁,组件解绑,基本不再使用。
-
5. created 和 mounted 的区别是什么?(高频)
标准答案:
| 对比项 | created |
mounted |
|---|---|---|
| 执行时机 | 实例创建完成,DOM 未挂载 | DOM 挂载完成 |
能否访问 data |
✅ 可以 | ✅ 可以 |
| 能否访问真实 DOM | ❌ 不可以 | ✅ 可以 |
| 常见用途 | 发起异步请求、初始化数据 | DOM 操作、第三方库初始化 |
6. keep-alive 组件的专属生命周期钩子是什么?
标准答案:
被 <keep-alive> 缓存的组件,不会执行 created/mounted/destroyed,而是触发两个专属钩子:
-
activated:组件被激活(从缓存中取出显示)时触发 -
deactivated:组件被停用(隐藏并缓存)时触发👉 常用于:激活时刷新数据,失活时暂停定时器 / 轮询。
7.生命周期的作用是什么?
标准答案:
- 让我们可以在 Vue 实例的不同阶段,执行对应的业务逻辑(如请求数据、操作 DOM、清除定时器等)。
- 合理利用生命周期钩子,可以更好地控制组件的行为,优化性能和用户体验。
十二、Vue 中插槽有哪些?具名插槽和作用域插槽的区别是什么?
Vue 中主要有三种插槽:默认插槽、具名插槽和作用域插槽。
- 默认插槽:是最基础的插槽,用于父组件向子组件传递默认内容。
- 具名插槽 :通过
name属性给插槽命名,父组件可以把不同内容放到子组件的不同位置,常用于多区域布局。 - 作用域插槽:子组件可以向插槽传递自身的数据,父组件接收这些数据后自定义渲染逻辑,常用于列表、表格等需要灵活渲染的场景。
两者的核心区别在于:具名插槽解决的是 "内容放哪里" 的问题,而作用域插槽解决的是 "如何渲染数据" 的问题。具名插槽只有父到子的内容传递,而作用域插槽支持子组件向父组件传递数据,实现了数据的双向传递和渲染逻辑的解耦。
十三、extends和mixin
1.mixin 和 extends 的区别是什么?
mixin 和 extends 都是 Vue 中用于复用组件逻辑的方式,但有几个核心区别:
- 继承数量不同 :
mixin支持传入多个混入对象,实现多份逻辑复用;而extends只能扩展一个对象,更偏向单继承。 - 执行优先级不同 :如果同时使用,
extends中的钩子会比mixins中的钩子先执行,两者都优先于组件自身的钩子。 - 设计目的不同 :
mixin主要用于分发通用功能,供多个组件共享;extends主要用于扩展单个基础组件,实现组件的继承和扩展。
它们的选项合并规则类似,同名的方法或数据都会以组件自身的选项优先覆盖。
2.使用场景
用 Mixin 的场景:
- 多个组件需要复用同一套通用方法、数据或生命周期逻辑。
- 比如多个页面都需要相同的表单校验、权限判断、请求封装。
用 Extends 的场景:
- 基于一个基础组件快速扩展新组件,比如在一个通用表单组件上扩展出不同业务的表单。
- 更轻量的组件继承,适合不想创建新的混入对象的场景。
十四、$attrs 和 $listeners
一、$attrs 是什么?怎么用?
一句话核心:
$attrs 是 Vue 中用来接收父组件传入,但子组件未声明为 props 的所有属性的对象。
常用于透传属性,尤其是多级组件嵌套时。
用法示例
vue
<!-- 父组件 -->
<MyInput placeholder="请输入" class="form-input" data-id="123" />
<!-- 子组件 MyInput -->
<template>
<!-- 透传所有未声明的属性到原生 input 上 -->
<input v-bind="$attrs" />
</template>
<script>
export default {
props: ['value'], // 只声明了 value 作为 props
}
</script>
- 父组件传的
placeholder、class、data-id没有在props中声明,会被收集到$attrs中。 v-bind="$attrs"会把这些属性透传给子组件内的原生元素。
二、$listeners 是什么?怎么用?
一句话核心:
$listeners 是 Vue 中用来接收父组件传入,但子组件未通过 $emit 处理的所有事件监听器的对象。
作用和 $attrs 类似,用于透传事件。
用法示例
vue
<!-- 父组件 -->
<MyInput @focus="onFocus" @blur="onBlur" />
<!-- 子组件 MyInput -->
<template>
<!-- 透传所有事件监听器到原生 input 上 -->
<input v-on="$listeners" />
</template>
- 父组件传的
@focus、@blur事件,会被收集到$listeners中。 v-on="$listeners"会把这些事件透传给子组件内的原生元素。
三、面试高频考点(重点)
1. 两者的核心作用是什么?
$attrs:透传属性(class/style/ 自定义属性等)$listeners:透传事件(自定义事件 / 原生事件)- 共同目的:解决多级组件嵌套时,属性和事件需要层层传递的问题,让组件封装更简洁。
2. 什么时候会用到它们?
典型场景:封装高阶组件 / 基础组件
比如你封装了一个通用的 BaseInput 组件,需要把父组件传的所有属性和事件都透传给内部的原生 <input>,这时候就用 $attrs 和 $listeners。
3. Vue3 中这两个 API 有什么变化?(必延伸)
$attrs:依然保留,功能更强大,现在既包含属性,也包含事件监听器(因为事件在 Vue3 中也被视为属性)。$listeners:已被移除 ,所有事件监听器都合并到了$attrs中,统一用v-bind="$attrs"透传即可。
十五、$set
1. 为什么需要 $set?Vue 为什么无法检测新增属性?
满分回答:
这是 Vue2 响应式系统的核心缺陷。
- 原理 :Vue2 通过
Object.defineProperty对数据进行递归劫持。 - 限制 :它只能监听属性已有 的 get/set,无法监听对象新增 的属性,也无法监听数组下标 和length的变化。
- 后果 :直接赋值
obj.newProp = 'xxx'虽然能改变数据,但无法触发视图更新,导致数据和视图脱节。 - 解决 :
$set就是为了解决这个问题,它会确保新增的属性是响应式的,并触发视图更新。
2. $set 的基本语法是什么?怎么使用?
满分回答:
语法 :this.$set(目标对象, 属性名/索引, 新值)
应用场景:
-
对象:向响应式对象添加新属性。
jsthis.$set(this.obj, 'age', 20) -
数组:通过索引修改数组,确保响应式。
jsthis.$set(this.arr, 0, 'newValue')
3. 直接赋值 obj.xxx = xxx 和 $set 有什么区别?
满分回答:
-
直接赋值:
- 只能改变数据层(data 里的值变了)。
- 不能触发视图更新(页面不刷新)。
- 新增的属性不是响应式的。
-
$set:- 既能改变数据层。
- 能触发视图更新(页面刷新)。
- 确保新增的属性是响应式的。
4. 如何解决 Vue 检测不到对象属性的添加或删除的问题?
满分回答:
有两种方案:
-
使用
$set(推荐):jsthis.$set(obj, '新属性', 值) -
使用
Vue.set/this.$set配合重新赋值(对象解构方式):由于 Vue 无法监听新增属性,我们可以用
jsObject.assign或
展开运算符
创建一个新对象来替换旧对象。
jsthis.obj = { ...this.obj, newProp: 'value' }
5. $set 返回值是什么?
满分回答:
$set 会直接返回设置好的属性值。
在设置成功后,该属性会被自动转为响应式。
十六、常见的修饰符有哪些
一、常见修饰符有哪些?(分类版)
Vue 修饰符主要分 4 大类:
1. 事件修饰符(最常考)
用于处理事件行为,写在 @事件 后面。
- .stop ------ 阻止事件冒泡(stopPropagation)
- .prevent ------ 阻止默认行为(preventDefault)
- .capture ------ 捕获模式(反向事件流)
- .self ------ 只当事件在元素自身触发时才处理
- .once ------ 事件只执行一次
- .passive ------ 立即执行,不阻止默认(常用于滚动优化)
示例:
vue
<div @click.stop="handleClick"></div>
<a @click.prevent href="#">链接</a>
2. 键盘修饰符(常问)
用于键盘事件触发条件。
- .enter ------ 回车
- .tab ------ 制表键
- .delete(退格 / 删除)
- .esc ------ 退出
- .space ------ 空格
- .up / .down / .left / .right ------ 方向键
- .ctrl / .alt / .shift / .meta ------ 组合键
示例:
vue
<input @keyup.enter="submit" />
3. 表单修饰符(非常高频)
用于 v-model 数据绑定行为。
- .lazy ------ 改变时同步,而不是输入时同步(如 change 事件)
- .trim ------ 自动去除首尾空格
- .number ------ 转为数字类型
示例:
vue
<input v-model.lazy="msg" />
<input v-model.number="age" />
<input v-model.trim="name" />
4. 组件修饰符(稍微进阶但常考)
- .sync ------ 用于 props 双向更新(Vue2 常用)
- .native ------ 给组件根元素绑定原生事件
- .v-slot ------ 插槽语法,简写 #
示例:
vue
<Comp @update:msg="msg = $event" /> <!-- 等价 :sync -->
<Child @click.native="handleClick" />
<template #header>...</template> <!-- 等价 v-slot:header -->
二、面试怎么回答最加分?(直接背)
面试官问:"Vue 中常见的修饰符有哪些?"
你可以这样说:
Vue 中的修饰符主要用于增强事件、表单、组件的行为,常见的分为几类:
- 事件修饰符:.stop、.prevent、.capture、.self、.once、.passive,用于控制事件的传播与默认行为。
- 键盘修饰符:.enter、.esc、.tab、.delete、.up/down/left/right 等,用于限定键盘触发。
- 表单修饰符:.lazy、.trim、.number,用于优化 v-model 的同步方式和数据格式。
- 组件修饰符:.sync、.native、.v-slot (#) 等,用于组件通信和插槽封装。
它们的核心作用是让代码更简洁、语义更清晰,避免大量重复逻辑。
三、面试官延伸高频问题
1. .prevent 和 .stop 的区别?
- .prevent:阻止默认行为(如 a 标签跳转)
- .stop:阻止事件冒泡(不阻止默认)
2. .lazy 与 .number 的区别?
- .lazy:改变同步时机(input → change)
- .number:将输入值转为 Number 类型
3. .native 的作用是什么?
用于给组件的根元素绑定原生事件,否则组件只会监听自定义事件。
4...async修饰符
vue
<Child :money.sync="parentMoney" />
vue
<Child :money="parentMoney" @update:money="val => parentMoney = val" />
Vue 内部自动帮你生成了那个监听事件。
所以它不是双向绑定,而是语法糖,简化了代码。
四、.sync 和 v-model 的区别?
| 维度 | .sync | v-model |
|---|---|---|
| 核心目的 | 实现多属性的父子双向修改 | 实现表单元素或组件的双向绑定 |
| 绑定值 | 可以绑定任意数量的属性(如 :a.sync :b.sync) | 只能绑定一个值(通常是 value) |
| 事件名 | 必须为 update:xxx |
固定为 input |
| 适用场景 | 组件封装时,需要频繁修改多个 props | 表单控件、自定义表单组件 |
| 语法 | :xxx.sync="val" |
v-model="val" |
十七、vue.nextTick的作用和使用场景
1.Vue.nextTick 的核心作用是等待 DOM 更新完成后再执行回调函数。
2.原理是利用 Vue 的异步更新队列机制,防止数据频繁变化导致重复渲染。
3.最常见的使用场景是:修改数据后,需要立即操作更新后的 DOM 时 ,比如获取更新后的 DOM 节点宽高、或者在 created 钩子中初始化需要 DOM 的第三方库。
4.它支持回调函数和 Promise 两种写法,组件内部通常使用 this.$nextTick。
vue
// 1. 回调函数形式(最常用)
Vue.nextTick(() => {
// DOM 更新了
})
// 2. Promise 形式(更优雅,无需嵌套)
Vue.nextTick()
.then(() => {
// DOM 更新了
})
十八、递归组件
1. 什么是递归组件?
标准答案:
递归组件就是组件在自身模板中调用自己的组件。
- 前提:组件必须定义
name属性,因为递归时 Vue 是通过组件名来识别自身的。 - 核心:必须设置递归终止条件,否则会导致无限循环,造成浏览器报错。
示例:
vue
<template>
<div>
<div>{{ item.name }}</div>
<!-- 终止条件:item.children 存在且不为空 -->
<tree-item
v-if="item.children && item.children.length"
v-for="child in item.children"
:key="child.id"
:item="child"
/>
</div>
</template>
<script>
export default {
name: 'TreeItem', // 必须定义 name
props: ['item']
}
</script>
2. 递归组件的使用场景有哪些?
标准答案:
递归组件的核心场景是处理具有嵌套层级结构的数据,最常见的包括:
- 树形控件:如文件目录树、菜单树、权限树。
- 多级菜单:如电商网站的分类导航。
- 评论回复:多层级的评论 / 回复列表。
- 组织架构图:企业的部门层级展示。
3. 组件循环引用问题怎么处理?
标准答案:
递归组件本质上就是一种特殊的循环引用(组件 A 引用了组件 A 自身),Vue 提供了两种标准解决方式:
方式 1:使用 beforeCreate 动态注册组件(Vue2 常用)
vue
export default {
name: 'TreeItem',
props: ['item'],
beforeCreate() {
// 在组件创建前,动态注册自身
this.$options.components.TreeItem = this
}
}
方式 2:使用异步组件(通用方案)
vue
<template>
<div>
<div>{{ item.name }}</div>
<component
:is="childComponent"
v-if="item.children && item.children.length"
v-for="child in item.children"
:key="child.id"
:item="child"
/>
</div>
</template>
<script>
export default {
name: 'TreeItem',
props: ['item'],
computed: {
childComponent() {
// 使用异步组件形式,避免循环引用
return () => import('./TreeItem.vue')
}
}
}
</script>
面试终极串讲版(直接背)
递归组件是指组件在自身模板中调用自己的组件,核心是必须定义
name属性并设置终止条件,常用于树形控件、多级菜单等嵌套结构场景。处理循环引用问题,常用两种方式:一种是在
beforeCreate钩子中动态注册自身,另一种是使用异步组件形式,延迟加载组件,打破循环依赖。
十九、vue是如何监听对象和数组的
一、Vue2 响应式数据修改的两个核心问题
1. 对象新增 / 删除属性无法响应式
问题描述:
Vue2 基于 Object.defineProperty 实现响应式,它只能对初始化时已存在的属性进行劫持。
直接给对象新增属性(如 this.obj.name = 'xxx'),虽然数据在内存中被修改了,但 Vue 无法监测到,因此不会触发视图更新,新增的属性也不是响应式的。
解决方案:
使用 this.$set 方法,强制让新增的属性变为响应式并触发更新。
// 语法:this.$set(目标对象, 属性名, 新值)
this.$set(this.obj, 'name', '宁波课堂')
⚠️ 注意 :this.$set 不能给 Vue 实例的根数据对象(data 本身)添加属性,只能用于嵌套对象。
2. 数组通过索引 / 直接修改 length 无法响应式
问题描述:
直接通过数组索引修改元素(如 this.list[0] = { name: '李四' }),或修改数组的 length 属性时,Vue 同样无法监测到变化,不会触发视图更新。
解决方案:
有两种方式:
-
使用 Vue 封装的数组方法:
Vue 对数组的 7 个原生方法进行了重写(包装),调用这些方法会触发响应式更新:
-
push/pop/shift/unshift -
splice/sort/reverse// 用 splice 替代索引修改
this.list.splice(0, 1, { name: '李四', age: 20 })
-
-
使用
this.$set:this.$set(this.list, 0, { name: '李四', age: 20 })
二、面试高频追问:为什么 Vue 要重写数组方法?
满分回答:
因为 Object.defineProperty 无法监听数组的索引和 length 变化,Vue 只能通过拦截数组的操作方法来实现响应式。
它对数组的 push、pop、shift、unshift、splice、sort、reverse 这 7 个方法进行了封装,在调用这些方法时,会手动触发视图更新,从而实现响应式。
三、面试终极串讲版(直接背)
Vue2 的响应式基于
Object.defineProperty,存在两个核心限制:
对象新增 / 删除属性无法响应 :直接赋值不会触发更新,需要用
this.$set来添加响应式属性。数组通过索引 / 修改 length 无法响应 :需要使用 Vue 封装的
splice等方法,或this.$set来修改数组。这也是 Vue3 改用 Proxy 实现响应式的主要原因之一。
二十、路由守卫
1. 路由守卫的作用是什么?
标准答案:
路由守卫是 vue-router 提供的导航钩子,主要作用是通过拦截或取消路由跳转,实现对路由导航的权限控制和流程管理。
它常用于登录校验、权限验证、数据预加载、页面埋点统计等场景。
2. Vue 路由守卫有哪些?
标准答案:
路由守卫按作用范围分为三大类:
① 全局守卫(所有路由都触发)
-
router.beforeEach(全局前置守卫)- 触发时机:导航触发时,最先执行。
- 核心作用:登录校验、权限拦截(最常用)。
- 参数:
(to, from) => {} - 控制导航:
return true或不返回任何值 → 放行;return false→ 取消导航;也可返回一个路由地址实现重定向。
jsrouter.beforeEach((to, from) => { // 校验是否登录 if (!isLogin && to.meta.requiresAuth) { return '/login' } })
-
router.beforeResolve(全局解析守卫)- 触发时机:导航被确认之前,所有组件内守卫和异步路由组件被解析之后触发。
- 核心作用:在进入页面前,执行需要组件解析完成才能进行的异步操作(如获取摄像头权限、加载数据)。
- 示例(图片中场景):
jsrouter.beforeResolve(async (to) => { if (to.meta.requiresCamera) { try { await askForCameraPermission() } catch (error) { // 权限被拒,取消导航 return false } } }) -
router.afterEach(全局后置钩子)- 触发时机:导航完成后触发。
- 核心作用:页面跳转后的埋点统计、修改页面标题等。
- 注意:不接受
next函数,也无法改变导航本身。
jsrouter.afterEach((to, from) => { sendToAnalytics(to.fullPath) })
② 路由独享守卫(单个路由配置)
beforeEnter:写在路由配置里,只对当前路由生效。
js
const routes = [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from) => {
// 仅对 /admin 路由生效
if (!isAdmin) return false
}
}
]
③ 组件内守卫(写在组件中)
beforeRouteEnter:进入组件前触发,此时组件实例还未创建,无法访问this。beforeRouteUpdate:路由复用组件时触发(如带参数的路由跳转)。beforeRouteLeave:离开组件前触发,常用于确认用户是否要离开未保存的表单。
3. beforeEach 和 beforeResolve 的区别是什么?
标准答案:
-
执行时机不同:
beforeEach:导航一开始就执行,此时组件和异步路由还未解析。beforeResolve:在导航被确认之前、所有组件和异步路由解析完成后执行。
-
使用场景不同:
beforeEach适合做全局通用的权限校验。beforeResolve适合做依赖组件解析的异步操作(如获取设备权限、加载数据)。
4. 路由守卫的执行顺序是怎样的?
标准答案:
一次完整的路由导航,守卫的执行顺序为:
beforeEach → 路由独享 beforeEnter → 组件内 beforeRouteEnter → beforeResolve → 导航确认 → afterEach → 组件 mounted
面试终极串讲版(直接背)
路由守卫是
vue-router提供的导航钩子,作用是控制路由跳转流程,实现权限校验、数据预加载等逻辑。按作用范围分为三类:
全局守卫:
beforeEach(前置校验)、beforeResolve(解析后异步操作)、afterEach(后置埋点);路由独享守卫:
beforeEnter;组件内守卫:
beforeRouteEnter/beforeRouteUpdate/beforeRouteLeave。其中
beforeEach
是最常用的,用于全局登录和权限控制。
二十一、Vue Router 三种路由模式
① Hash 模式
- URL 带
#,例如:/home#/user - 基于
hashchange事件 - 优点:兼容性好,不需要后端配置
- 缺点:URL 不好看,SEO 不友好
- 环境:浏览器环境
② History 模式
- URL 不带
#,例如:/home/user - 基于 H5
pushState/replaceState - 优点:URL 美观,SEO 友好
- 缺点:刷新 404,必须后端配置把所有请求重定向到index.html
- 环境:浏览器环境
③ Memory 模式
- URL 不会发生任何变化
- 路由信息存在内存里,不跟 URL 关联
- 不会在地址栏显示路径
- 前进后退不会改变路由
- 刷新页面路由会丢失
1. Memory 模式使用场景
标准答案:
Memory 模式不依赖浏览器 URL,因此用于非浏览器环境,例如:
- 移动端混合开发(uniapp / 小程序)
- Electron 桌面端
- 服务端渲染(SSR)
- 单元测试(Jest / Vitest)
一句话:
没有浏览器地址栏的环境,就用 memory 模式。
2. 三种模式核心区别
| 模式 | URL 是否变化 | 依赖地址 | 后端配置 | 适用环境 |
|---|---|---|---|---|
| Hash | 带 # 变化 | 浏览器 URL | 不需要 | 浏览器 |
| History | 不带 # 变化 | 浏览器 URL | 需要 | 浏览器 |
| Memory | 不变化 | 内存 | 不需要 | 小程序、Electron、测试、非浏览器 |
3. 为什么 History 模式刷新会 404?
原理:
当用户在 history 模式下刷新页面时,浏览器会向服务器发起请求,请求的路径是当前完整的 URL(如 /home)。
而在单页应用中,服务器上并不存在 /home 这个文件,因此会返回 404 错误。
解决方案:
需要在服务器配置中,将所有路由请求重定向到 index.html,由前端路由接管。
- Nginx:配置
try_files $uri $uri/ /index.html; - Vue CLI:内置的开发服务器已自动处理,但生产环境必须配置。
二十二、自定义指令
1. 如何创建自定义指令?
Vue 提供了全局注册 和局部注册两种方式,同时指令有完整的生命周期钩子。
① 全局注册
在 main.js 中注册,所有组件都可以使用:
js
// Vue2
Vue.directive('focus', {
// 指令的生命周期钩子
inserted(el) {
// 元素插入 DOM 时自动聚焦
el.focus()
}
})
// Vue3
app.directive('focus', {
mounted(el) {
el.focus()
}
})
② 局部注册
在组件内部注册,仅当前组件可用:
js
export default {
directives: {
focus: {
mounted(el) {
el.focus()
}
}
}
}
③ 指令的生命周期钩子(Vue2)
bind:指令第一次绑定到元素时调用inserted:元素插入 DOM 时调用(最常用)update:组件更新时调用componentUpdated:组件更新完成后调用unbind:指令解绑时调用
Vue3 中调整为:created / beforeMount / mounted / beforeUpdate / updated / beforeUnmount / unmounted
2. 自定义指令的使用场景有哪些?
自定义指令的核心价值是直接操作 DOM,实现组件和模板难以完成的逻辑,常见场景包括:
| 场景 | 示例说明 |
|---|---|
| 自动聚焦 | 输入框 v-focus,页面加载时自动聚焦 |
| 权限控制 | v-permission,无权限时隐藏 / 禁用按钮 |
| 防重复点击 | v-throttle,按钮防抖节流,防止用户多次提交 |
| 拖拽排序 | v-drag,实现元素拖拽功能 |
| 图片懒加载 | v-lazy,元素进入视口才加载图片 |
| 一键复制 | v-copy,点击元素复制文本到剪贴板 |
| 点击外部关闭 | v-click-outside,点击元素外部时触发关闭 |
3. 面试终极串讲版(直接背)
自定义指令是 Vue 提供的直接操作 DOM 的方式,分为全局注册和局部注册两种。
它通过指令的生命周期钩子(如
inserted/mounted)实现 DOM 操作,常见场景包括自动聚焦、权限控制、防抖节流、拖拽、懒加载等,主要用于封装模板中难以实现的通用 DOM 逻辑。
二十三、虚拟DOM
1. 什么是虚拟 DOM?
标准答案:
虚拟 DOM(Virtual DOM,简称 VDOM),是一种用纯 JavaScript 对象来表示真实 DOM 结构的编程模式。
- 它把真实 DOM 的信息(标签类型、属性、子节点等),映射成一个轻量级的 JS 对象(VNode),保存在内存中。
- 当数据变化时,框架会先更新虚拟 DOM,再通过对比算法(Diff 算法)找出差异,只对真实 DOM 进行最小化更新,从而提升性能。
示例(图片中的代码):
js
const vnode = {
type: 'div',
props: { id: 'hello' },
children: [/* 更多 vnode */]
}
这个 vnode 对象就代表了一个真实的 <div id="hello"> 元素。
2. 虚拟 DOM 为什么 "快"?虚拟 DOM的优势是什么?(核心考点)
标准答案:
虚拟 DOM 不是直接操作 DOM,而是通过以下机制减少了 DOM 操作,从而提升性能:
-
批量处理,减少重排重绘
直接操作真实 DOM 会触发浏览器的重排 / 重绘,性能开销极大。虚拟 DOM 会把多次数据变更合并成一次 Diff,只把差异更新到真实 DOM 上,避免了频繁操作。
-
Diff 算法的最小化更新
当数据变化时,框架会生成新的虚拟 DOM 树,和旧树进行对比(Diff),找出只需要更新的节点,而不是重新渲染整个 DOM 树。
- Vue/React 的 Diff 算法都是基于同层比较、key 优化的策略,保证了对比的效率。
-
跨平台能力
虚拟 DOM 是纯 JS 对象,和平台无关,既可以渲染到浏览器 DOM,也可以渲染到小程序、Native 等其他平台。
3. 面试高频追问:虚拟 DOM 一定会比直接操作 DOM 快吗?
标准答案:
不一定。
- 在简单场景下,直接操作 DOM 比虚拟 DOM 更快,因为虚拟 DOM 多了一层 Diff 计算的开销。
- 在复杂场景下,尤其是涉及大量节点更新时,虚拟 DOM 通过批量更新和最小化 DOM 操作,性能会明显优于频繁直接操作 DOM。
4. 面试终极串讲版(直接背)
虚拟 DOM 是用 JavaScript 对象来表示真实 DOM 的一种编程模式。它通过在内存中维护一份虚拟 DOM 树,当数据变化时,先通过 Diff 算法对比新旧虚拟 DOM 树,找出差异,再对真实 DOM 进行最小化更新,从而减少了直接操作 DOM 的性能开销,提升了渲染效率。
同时,它还提供了跨平台渲染的能力。
二十四、Vuex和Pinia
1. Vuex 和 Pinia 的作用是什么?
标准答案:
它们都是 Vue 生态中的全局状态管理工具,核心作用是:
- 管理组件间共享的全局状态
- 统一状态修改逻辑,实现可追踪、可维护的数据管理
- 解决组件间数据传递复杂的问题(如父子嵌套、兄弟组件通信)
其中:
Vuex是 Vue2 时代的官方状态管理库Pinia是 Vue3 时代官方推荐的新一代状态管理库,也支持 Vue2
2. Vuex 和 Pinia 的区别?(面试核心考点)
结合图片内容,我整理了完整对比:
| 对比维度 | Vuex | Pinia |
|---|---|---|
| 核心结构 | 包含 state、getters、mutations、actions、modules |
只有 state、getters、actions,移除了 mutations |
| TypeScript 支持 | 类型推断差,需要额外配置 | 原生支持 TS,类型推断完善,无需额外配置 |
| API 风格 | 基于选项式 API,需写 commit、dispatch |
支持组合式 API,更符合 Vue3 的开发习惯 |
| 模块管理 | 有嵌套模块、命名空间,结构复杂 | 扁平化设计,无嵌套模块,Store 之间可直接引用 |
| 使用方式 | 依赖 $store,需通过 commit 提交 mutations |
直接导入 Store 实例调用,无魔法字符串,自动补全友好 |
| DevTools 支持 | 支持,但依赖 mutations | 原生支持,调试体验更流畅 |
3. 关键差异深度解析(图片重点)
-
Mutations 被废弃
Pinia 移除了 Vuex 中强制使用
mutations修改状态的规则,直接在actions中修改state,代码更简洁,也解决了mutations冗余的问题。 -
更好的 TypeScript 支持
Pinia 无需额外配置即可获得完整的类型推断,API 设计充分利用 TS 特性,自动补全和类型检查体验极佳。
-
无嵌套模块、无命名空间
Pinia 采用扁平化架构,每个 Store 都是独立的模块,可直接引用其他 Store,无需复杂的命名空间配置,同时也支持循环依赖。
-
更轻量、更灵活
Pinia 去掉了 Vuex 中很多不必要的概念,API 更简洁,体积更小,同时也支持 Vue2 和 Vue3,迁移成本低。
4. 面试终极串讲版
Vuex 和 Pinia 都是 Vue 的状态管理工具,Pinia 是 Vue3 官方推荐的替代方案。
相比 Vuex,Pinia 移除了
mutations,API 更简洁;原生支持 TypeScript,类型推断完善;采用扁平化架构,无嵌套模块和命名空间,Store 之间可直接引用;同时体积更小、调试体验更好,也兼容 Vue2。
二十五、css样式隔离
1.CSS 如何实现样式隔离?
在 Vue 单文件组件中,通过给 <style> 标签添加 scoped 属性可以实现样式隔离。其原理是 PostCSS 会为组件元素添加唯一的 data-v-xxx 属性,并将样式转换为属性选择器,从而避免全局样式污染。
2.样式穿透如何实现?
当需要修改子组件(尤其是第三方组件库)的内部样式时,需要使用样式穿透 。Vue3 推荐使用 :deep() 语法,Vue2 常用 /deep/ 或 ::v-deep,通过它可以绕过 scoped 的限制,精准定位子组件内部元素。
二十六、渲染函数
1.什么是渲染函数?
渲染函数是 Vue 中用纯 JavaScript 生成虚拟 DOM 的方式,替代了模板语法,它通过 h() 函数创建 VNode。相比模板,渲染函数提供了完全的编程控制能力,适合复杂动态组件的场景。h() 函数接收标签类型、属性配置和子节点三个参数,用法非常灵活,支持文本、数组、其他 VNode 等多种形式的子节点。
2.使用渲染函数如何创建dom?
Vue 提供了 h() 函数(全称 createVNode,是 hyperscript 的简称),用于创建虚拟节点(VNode)。
h() 函数的基本语法
js
// 语法:h(type, props, children)
import { h } from 'vue'
// 创建一个 <div id="foo" class="bar"> 元素
const vnode = h(
'div', // 标签类型
{ id: 'foo', class: 'bar' }, // 属性、事件等配置
'Hello World' // 子节点(文本、数组或其他 vnode)
)
js
// 1. 最简形式:只写标签
h('div')
// 2. 带属性
h('div', { id: 'foo', class: 'bar' })
// 3. 带事件监听(onXxx 形式)
h('button', { onClick: () => alert('click') }, '点击我')
// 4. 带子节点(文本)
h('div', 'Hello')
// 5. 带子节点(数组)
h('div', [
h('span', 'A'),
h('span', 'B')
])
// 6. 样式与类名配置(和模板用法一致)
h('div', {
class: ['foo', 'bar'],
style: { color: 'red' }
})
3. 渲染函数 vs 模板:适用场景
| 方式 | 适用场景 | 优势 |
|---|---|---|
| 模板 | 绝大多数场景 | 声明式、直观、易读,Vue 提供了指令糖衣 |
| 渲染函数 | 复杂动态组件、需要完全编程控制的场景 | 完全的 JavaScript 能力,逻辑更灵活 |
二十七、组合式API
1. 什么是组合式 API?
标准答案:
组合式 API 是 Vue3 引入的一套全新的 API,它允许开发者使用函数 而非选项对象(data/methods/computed)的方式来编写 Vue 组件,核心入口是 setup() 函数。
它涵盖了以下几类核心 API:
- 响应式 API :
ref、reactive、computed、watch等,用于创建和管理响应式状态。 - 生命周期钩子 :
onMounted、onUpdated、onUnmounted等,在setup中注册生命周期逻辑。 - 依赖注入 :
provide和inject,支持跨层级组件传值。
2. 为什么要有组合式 API?(面试高频考点)
结合图片内容,核心优势有以下 4 点:
① 更好的逻辑复用
选项式 API 中,复用逻辑依赖 mixins,存在命名冲突、来源不清晰等问题。
组合式 API 可以通过自定义 composables 函数,实现无冲突、可追踪的逻辑复用,代码组织更清晰。
② 更灵活的代码组织
选项式 API 中,相关逻辑会分散在 data、methods、computed 等不同选项中。
组合式 API 可以把同一业务逻辑的所有代码(状态、方法、监听、生命周期)放在一起,代码可读性和可维护性更强。
③ 更好的 TypeScript 支持
选项式 API 对 TS 的类型推断支持较差,需要额外配置。
组合式 API 原生支持 TS,ref/reactive 等 API 都能提供完善的类型推断,编写类型安全的组件更方便。
④ 更小的生产包体积
组合式 API 基于函数编写,Tree-shaking 更友好,未使用的 API 可以被打包工具自动移除,最终打包体积更小。
3. 组合式 API 核心组成(图片重点梳理)
| 分类 | 核心 API | 作用 |
|---|---|---|
| setup() | 基本使用、访问 Props、与渲染函数配合 | 组合式 API 的入口,组件创建前执行 |
| 响应式核心 | ref、reactive、computed、watch |
创建和管理响应式状态 |
| 响应式工具 | isRef、unref、toRef、toRefs |
辅助处理响应式数据 |
| 生命周期钩子 | onMounted、onUpdated、onUnmounted 等 |
注册组件生命周期逻辑 |
| 依赖注入 | provide、inject |
跨层级组件传值 |
4. 面试终极串讲版(直接背)
组合式 API 是 Vue3 引入的、以函数为核心的组件编写方式,它以
setup()为入口,通过ref/reactive等响应式 API、生命周期钩子和依赖注入 API 来组织组件逻辑。相比选项式 API,它的核心优势是:更好的逻辑复用、更灵活的代码组织、更完善的 TypeScript 支持,以及更友好的 Tree-shaking,最终实现更小的打包体积
二十八、ToRef和ToRefs
1. toRef 和 toRefs 的作用?
两者的核心作用都是基于响应式对象创建 ref,并保持双向同步,避免解构 / 赋值导致响应式丢失。
toRef 作用
- 基于响应式对象的单个属性 创建一个
ref。 - 创建的
ref与源对象属性保持双向同步 :修改ref.value会更新源属性,修改源属性也会更新ref.value。 - 常用于从响应式对象中单独取出某个属性,且不丢失响应式。
示例(图片中的核心逻辑):
js
const state = reactive({ foo: 1, bar: 2 })
const fooRef = toRef(state, 'foo')
// 双向同步
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
toRefs 作用
- 将整个响应式对象转换为普通对象 ,对象的每个属性都是一个指向源对象属性的
ref。 - 解决响应式对象解构 / 展开时丢失响应式的问题,常用于组合式函数返回响应式对象。
示例(图片中的核心逻辑):
js
const state = reactive({ foo: 1, bar: 2 })
const stateAsRefs = toRefs(state)
// 解构后仍保持响应式
const { foo, bar } = stateAsRefs
state.foo++
console.log(foo.value) // 2
2. toRef 和 toRefs 的区别?(面试高频)
| 对比项 | toRef |
toRefs |
|---|---|---|
| 作用对象 | 响应式对象的单个属性 | 整个响应式对象的所有属性 |
| 返回值 | 单个 ref 对象 |
包含多个 ref 的普通对象 |
| 使用场景 | 只需要响应式对象中某一个属性 | 需要解构 / 展开整个响应式对象,且不丢失响应式 |
| 是否处理不存在的属性 | 即使属性不存在,也会创建一个可用的 ref |
只会为对象当前存在的属性创建 ref |
3. 关键面试点:为什么直接解构 reactive 会丢失响应式?
reactive返回的是一个响应式代理对象,直接解构会取出其原始值(普通数据),不再具有响应式。toRefs为每个属性创建了ref,ref的.value会保持对源对象属性的引用,因此解构后仍能保持响应式。
4. 面试终极串讲版(直接背)
toRef和toRefs都是 Vue 中用于处理响应式对象的工具,核心作用是创建与源属性双向同步的ref,避免响应式丢失。
toRef用于为响应式对象的单个属性创建ref,而toRefs会将整个响应式对象转换为包含多个ref的普通对象,常用于组合式函数返回响应式对象,支持解构且不丢失响应式。
二十九、说说 Vue 的核心特点?
渐进式框架、数据驱动视图、双向绑定、组件化、虚拟 DOM、路由、状态管理。
三十、插值语法、v-bind、v-on 作用与简写?
{``{}}:插值,渲染文本v-bind:动态绑定属性,简写:v-on:绑定事件,简写@
三十一、Vue 所有组件通信方式?
- 父传子:props /defineProps
- 子传父:$emit /defineEmits
- 跨级:provide /inject
- 全局:Pinia/Vuex
- 兄弟:状态管理库、全局事件
三十二、watch 和 watchEffect 区别
- watch:手动指定监听源,惰性执行,可以拿到新/旧值
- watchEffect:自动收集依赖,默认立即执行,只能拿到新值
三十三、路由懒加载的原理
路由懒加载的核心原理是利用 ES6 动态 import () 异步导入语法,
结合打包工具(Webpack/Vite)进行代码分割,将每个路由组件单独打包为独立 chunk 文件;
项目初始化时只加载首页代码,访问对应路由时才异步请求并加载当前页面组件代码,实现按需加载,减小首屏体积、优化页面加载性能。
三十四、route 和 router 区别
$router 是路由 ** 实例 **,用来做编程式跳转:push、replace。
vue
this.$router.push('/home')//跳转到home,可返回
this.$router.replace('/home')//替换当前页面,不可返回
this.$router.go(-1)//返回上一页
$route 是当前路由信息对象,拿参数、路径、query、params。
vue
this.$route.path//当前路径/user/123
this.$route.params//动态路由参数{id:123}
this.$route.name//路由名字
this.$route.query//查询参数?name=**->{name:**}