vue知识点复习

一、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. 跟着提示选择

  1. 输入项目名
  2. 框架选择:Vue
  3. 语法选择:JavaScriptTypeScript

三、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 有什么问题?

标准答案

  1. 性能极差

    v-for 优先级高于 v-if,导致会先把整个列表全部循环渲染一遍,再一个个判断是否隐藏,浪费性能。

  2. 逻辑不清晰

    容易出现预期外的显示隐藏问题。

  3. 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?(延伸必问题)

标准答案

  1. 数组新增、删除、颠倒顺序时,index 会跟着变

  2. Vue 会错误复用 DOM,导致:

    • 复选框选中错乱
    • 输入框内容错乱
    • 动画异常
  3. key 必须用唯一、稳定的值:id、uuid 等


5. v-for 中 key 的作用是什么?

标准答案

  • key 是虚拟 DOM 节点唯一标识,diff 算法用来识别节点、复用节点

六、vue响应式原理

1. Vue2 响应式原理

Vue2 的响应式是采用数据劫持结合发布 - 订阅模式实现的。

核心是 Object.defineProperty,在初始化时递归遍历 data 里的所有属性,为每个属性添加 gettersetter

  • 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?

标准回答:

  1. Proxy 代理整个对象,而 defineProperty 只能劫持单个属性
  2. 支持新增 / 删除属性,defineProperty 做不到
  3. 数组原生支持,不用重写数组方法
  4. 惰性劫持,用到才代理,性能更高
  5. 支持更多集合类型: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-ifv-show 都是 Vue 中用来控制元素显示隐藏的指令,但它们的实现原理、开销和适用场景完全不同:

    1. 实现原理不同
    • v-if真正的条件渲染 :它会根据条件的真假,完全销毁或重建元素 / 组件。如果初始条件为假,元素甚至不会被渲染到 DOM 中。

    • v-show 只是通过设置元素的 display CSS 属性来控制显示隐藏,元素始终会被渲染并保留在 DOM 中。

    1. 性能开销不同
    • v-if 切换开销大:每次切换都会触发组件的销毁和重建,还会触发组件的生命周期钩子。

    • v-show 初始化开销大,但切换非常快:它只是简单地修改 CSS 属性,适合频繁切换的场景。

    1. 语法支持不同
    • v-if 支持 <template> 标签分组,也能和 v-elsev-else-if 搭配使用。

    • v-show 不支持 <template>,也不能和 v-else 搭配。

    1. 使用场景不同
    • v-if 适合条件很少改变的场景,比如权限控制、页面模块的按需加载。
    • v-show 适合需要频繁切换的场景,比如选项卡切换、折叠面板等。

    另外补充一点:v-ifv-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. 异步组件的实现原理是什么?

标准答案:

  1. defineAsyncComponent 接收一个返回 Promise 的加载函数(通常配合 ES 动态 import() 使用)。
  2. 当组件需要渲染时,Vue 才会执行这个加载函数,拉取组件代码。
  3. 构建工具(Webpack/Vite)会把异步组件打包成单独的文件,实现按需加载。

3. 异步组件和普通组件有什么区别?

标准答案:

  • 普通组件:打包时会被打包进主 bundle,页面首次加载就会加载所有组件。
  • 异步组件:打包时会被抽成单独的 chunk,仅在使用时才加载,优化首屏加载性能。

三、延伸高频对比题

1. 动态组件和异步组件的区别?

标准答案:

  • 动态组件 :解决的是「组件切换」的问题,核心是 :is
  • 异步组件 :解决的是「按需加载」的问题,核心是 defineAsyncComponent
  • 两者可以结合使用:比如 Tab 页里的组件,既可以用动态组件切换,也可以用异步组件懒加载。

2. 异步组件和路由懒加载的关系?

标准答案:

路由懒加载本质上就是异步组件的一种应用,通过 () => import('xxx') 实现路由组件的按需加载,和 defineAsyncComponent 的底层原理是一样的。

十、keep-alive的作用和如何实现缓存

一、基本用法(图片里的示例)

它常和动态组件 <component :is="xxx"> 一起使用,通过 includeexclude 来控制哪些组件需要缓存:

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>

二、如何实现缓存?(原理)

  1. 缓存容器keep-alive 内部维护了一个 cache 对象,用来存储已经创建好的组件实例。

  2. 切换逻辑

    • 当组件第一次被渲染时,会创建实例并存入 cache
    • 再次切换回来时,直接从 cache 中取出实例,挂载到 DOM 上,而不是重新创建。
  3. 注意:被切换掉的组件会被从 DOM 中移除,但组件实例本身仍保留在缓存中,状态不会丢失。


三、生命周期钩子(图片里的 activated / deactivated

keep-alive 缓存的组件,不会重复执行 created / mounted / destroyed,而是触发这两个专属钩子:

  • activated:组件被激活(重新显示)时触发
  • deactivated:组件被停用(隐藏)时触发

这两个钩子是 keep-alive 组件独有的,常用于处理组件激活 / 失活时的逻辑,比如数据刷新、定时器启停等。

十一、vue生命周期

1. 什么是 Vue 的生命周期?

标准答案:

Vue 实例从创建、初始化、挂载、更新到销毁的完整过程,就是它的生命周期。

通俗来说,就是一个 Vue 实例从 "出生" 到 "死亡" 的全过程。


2. 什么是生命周期钩子?

标准答案:

生命周期钩子(钩子函数),就是 Vue 实例在生命周期的特定阶段自动触发执行的函数

我们可以在这些钩子中编写自定义逻辑,比如初始化数据、操作 DOM、清理资源等。


3. Vue 实例的完整生命周期流程是怎样的?

标准答案:

完整流程按顺序分为 4 个核心阶段:

  1. 创建阶段 :实例初始化 → 数据观测(beforeCreatecreated
  2. 挂载阶段 :模板编译 → DOM 挂载(beforeMountmounted
  3. 更新阶段 :数据变化 → 视图更新(beforeUpdateupdated
  4. 销毁阶段 :实例卸载 → 资源清理(beforeDestroy/beforeUnmount destroyed/unmounted

一句话串流程:

创建实例 → 初始化数据 → 编译模板 → 挂载 DOM → 数据更新 → 视图重渲染 → 实例销毁。


4. 请按阶段简述每个钩子的执行时机和用途

标准答案:

  • 创建阶段

    • beforeCreate:实例刚创建,datamethods 都未初始化,几乎不用。

    • created:实例创建完成,data 已初始化,但 DOM 还未生成

      👉 常用:

      发起异步请求、初始化数据

  • 挂载阶段

    • beforeMount:模板编译完成,即将挂载到 DOM,此时还拿不到真实 DOM。

    • mounted:DOM 挂载完成,可以访问 $el 操作真实 DOM。($el是 Vue 组件挂载后,组件实例自动创建的一个引用,指向组件模板渲染出来的最外层 DOM 元素。)

      👉 常用:

      DOM 操作、第三方库初始化(如图表、地图)

  • 更新阶段

    • beforeUpdate:数据更新时触发,DOM 还未更新,可获取更新前的 DOM 状态。
    • updated:数据更新完成,DOM 已更新,避免在此修改数据,防止死循环。
  • 销毁阶段

    • beforeDestroy:实例销毁前触发,组件还可用。

      👉 常用:

      清除定时器、解绑事件、销毁第三方实例,清理Websocket,防止内存泄漏

    • destroyed:实例已完全销毁,组件解绑,基本不再使用。


5. createdmounted 的区别是什么?(高频)

标准答案:

对比项 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.mixinextends 的区别是什么?

mixinextends 都是 Vue 中用于复用组件逻辑的方式,但有几个核心区别:

  1. 继承数量不同mixin 支持传入多个混入对象,实现多份逻辑复用;而 extends 只能扩展一个对象,更偏向单继承。
  2. 执行优先级不同 :如果同时使用,extends 中的钩子会比 mixins 中的钩子先执行,两者都优先于组件自身的钩子。
  3. 设计目的不同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>
  • 父组件传的 placeholderclassdata-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(目标对象, 属性名/索引, 新值)

应用场景

  • 对象:向响应式对象添加新属性。

    js 复制代码
    this.$set(this.obj, 'age', 20)
  • 数组:通过索引修改数组,确保响应式。

    js 复制代码
    this.$set(this.arr, 0, 'newValue')

3. 直接赋值 obj.xxx = xxx$set 有什么区别?

满分回答:

  • 直接赋值

    • 只能改变数据层(data 里的值变了)。
    • 不能触发视图更新(页面不刷新)。
    • 新增的属性不是响应式的。
  • $set

    • 既能改变数据层
    • 能触发视图更新(页面刷新)。
    • 确保新增的属性是响应式的。

4. 如何解决 Vue 检测不到对象属性的添加或删除的问题?

满分回答:

有两种方案:

  1. 使用 $set(推荐):

    js 复制代码
    this.$set(obj, '新属性', 值)
  2. 使用 Vue.set / this.$set 配合重新赋值(对象解构方式):

    由于 Vue 无法监听新增属性,我们可以用

    js 复制代码
    Object.assign

    展开运算符

    创建一个新对象来替换旧对象。

    js 复制代码
    this.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 中的修饰符主要用于增强事件、表单、组件的行为,常见的分为几类:

  1. 事件修饰符:.stop、.prevent、.capture、.self、.once、.passive,用于控制事件的传播与默认行为。
  2. 键盘修饰符:.enter、.esc、.tab、.delete、.up/down/left/right 等,用于限定键盘触发。
  3. 表单修饰符:.lazy、.trim、.number,用于优化 v-model 的同步方式和数据格式。
  4. 组件修饰符:.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. 递归组件的使用场景有哪些?

标准答案:

递归组件的核心场景是处理具有嵌套层级结构的数据,最常见的包括:

  1. 树形控件:如文件目录树、菜单树、权限树。
  2. 多级菜单:如电商网站的分类导航。
  3. 评论回复:多层级的评论 / 回复列表。
  4. 组织架构图:企业的部门层级展示。

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 同样无法监测到变化,不会触发视图更新。

解决方案

有两种方式:

  1. 使用 Vue 封装的数组方法

    Vue 对数组的 7 个原生方法进行了重写(包装),调用这些方法会触发响应式更新:

    • push / pop / shift / unshift

    • splice / sort / reverse

      // 用 splice 替代索引修改
      this.list.splice(0, 1, { name: '李四', age: 20 })

  2. 使用 this.$set

    复制代码
    this.$set(this.list, 0, { name: '李四', age: 20 })

二、面试高频追问:为什么 Vue 要重写数组方法?

满分回答

因为 Object.defineProperty 无法监听数组的索引和 length 变化,Vue 只能通过拦截数组的操作方法来实现响应式。

它对数组的 pushpopshiftunshiftsplicesortreverse 这 7 个方法进行了封装,在调用这些方法时,会手动触发视图更新,从而实现响应式。


三、面试终极串讲版(直接背)

Vue2 的响应式基于 Object.defineProperty,存在两个核心限制:

  1. 对象新增 / 删除属性无法响应 :直接赋值不会触发更新,需要用 this.$set 来添加响应式属性。

  2. 数组通过索引 / 修改 length 无法响应 :需要使用 Vue 封装的 splice 等方法,或 this.$set 来修改数组。

    这也是 Vue3 改用 Proxy 实现响应式的主要原因之一。

二十、路由守卫

1. 路由守卫的作用是什么?

标准答案:

路由守卫是 vue-router 提供的导航钩子,主要作用是通过拦截或取消路由跳转,实现对路由导航的权限控制和流程管理

它常用于登录校验、权限验证、数据预加载、页面埋点统计等场景。


2. Vue 路由守卫有哪些?

标准答案:

路由守卫按作用范围分为三大类:

① 全局守卫(所有路由都触发)

  1. router.beforeEach(全局前置守卫)

    • 触发时机:导航触发时,最先执行。
    • 核心作用:登录校验、权限拦截(最常用)。
    • 参数:(to, from) => {}
    • 控制导航:return true 或不返回任何值 → 放行;return false → 取消导航;也可返回一个路由地址实现重定向。
    js 复制代码
    router.beforeEach((to, from) => {
      // 校验是否登录
      if (!isLogin && to.meta.requiresAuth) {
        return '/login'
      }
    })

  2. router.beforeResolve(全局解析守卫)

    • 触发时机:导航被确认之前,所有组件内守卫和异步路由组件被解析之后触发。
    • 核心作用:在进入页面前,执行需要组件解析完成才能进行的异步操作(如获取摄像头权限、加载数据)。
    • 示例(图片中场景):
    js 复制代码
    router.beforeResolve(async (to) => {
      if (to.meta.requiresCamera) {
        try {
          await askForCameraPermission()
        } catch (error) {
          // 权限被拒,取消导航
          return false
        }
      }
    })
  3. router.afterEach(全局后置钩子)

    • 触发时机:导航完成后触发。
    • 核心作用:页面跳转后的埋点统计、修改页面标题等。
    • 注意:不接受 next 函数,也无法改变导航本身
    js 复制代码
    router.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. beforeEachbeforeResolve 的区别是什么?

标准答案:

  • 执行时机不同

    • beforeEach:导航一开始就执行,此时组件和异步路由还未解析。
    • beforeResolve:在导航被确认之前、所有组件和异步路由解析完成后执行。
  • 使用场景不同

    • beforeEach 适合做全局通用的权限校验。
    • beforeResolve 适合做依赖组件解析的异步操作(如获取设备权限、加载数据)。

4. 路由守卫的执行顺序是怎样的?

标准答案:

一次完整的路由导航,守卫的执行顺序为:

beforeEach → 路由独享 beforeEnter → 组件内 beforeRouteEnterbeforeResolve → 导航确认 → afterEach → 组件 mounted


面试终极串讲版(直接背)

路由守卫是 vue-router 提供的导航钩子,作用是控制路由跳转流程,实现权限校验、数据预加载等逻辑。

按作用范围分为三类:

  1. 全局守卫:beforeEach(前置校验)、beforeResolve(解析后异步操作)、afterEach(后置埋点);

  2. 路由独享守卫:beforeEnter

  3. 组件内守卫: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 操作,从而提升性能:

  1. 批量处理,减少重排重绘

    直接操作真实 DOM 会触发浏览器的重排 / 重绘,性能开销极大。虚拟 DOM 会把多次数据变更合并成一次 Diff,只把差异更新到真实 DOM 上,避免了频繁操作。

  2. Diff 算法的最小化更新

    当数据变化时,框架会生成新的虚拟 DOM 树,和旧树进行对比(Diff),找出只需要更新的节点,而不是重新渲染整个 DOM 树。

    • Vue/React 的 Diff 算法都是基于同层比较、key 优化的策略,保证了对比的效率。
  3. 跨平台能力

    虚拟 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
核心结构 包含 stategettersmutationsactionsmodules 只有 stategettersactions移除了 mutations
TypeScript 支持 类型推断差,需要额外配置 原生支持 TS,类型推断完善,无需额外配置
API 风格 基于选项式 API,需写 commitdispatch 支持组合式 API,更符合 Vue3 的开发习惯
模块管理 有嵌套模块、命名空间,结构复杂 扁平化设计,无嵌套模块,Store 之间可直接引用
使用方式 依赖 $store,需通过 commit 提交 mutations 直接导入 Store 实例调用,无魔法字符串,自动补全友好
DevTools 支持 支持,但依赖 mutations 原生支持,调试体验更流畅

3. 关键差异深度解析(图片重点)

  1. Mutations 被废弃

    Pinia 移除了 Vuex 中强制使用 mutations 修改状态的规则,直接在 actions 中修改 state,代码更简洁,也解决了 mutations 冗余的问题。

  2. 更好的 TypeScript 支持

    Pinia 无需额外配置即可获得完整的类型推断,API 设计充分利用 TS 特性,自动补全和类型检查体验极佳。

  3. 无嵌套模块、无命名空间

    Pinia 采用扁平化架构,每个 Store 都是独立的模块,可直接引用其他 Store,无需复杂的命名空间配置,同时也支持循环依赖。

  4. 更轻量、更灵活

    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:

  • 响应式 APIrefreactivecomputedwatch 等,用于创建和管理响应式状态。
  • 生命周期钩子onMountedonUpdatedonUnmounted 等,在 setup 中注册生命周期逻辑。
  • 依赖注入provideinject,支持跨层级组件传值。

2. 为什么要有组合式 API?(面试高频考点)

结合图片内容,核心优势有以下 4 点:

① 更好的逻辑复用

选项式 API 中,复用逻辑依赖 mixins,存在命名冲突、来源不清晰等问题。

组合式 API 可以通过自定义 composables 函数,实现无冲突、可追踪的逻辑复用,代码组织更清晰。

② 更灵活的代码组织

选项式 API 中,相关逻辑会分散在 datamethodscomputed 等不同选项中。

组合式 API 可以把同一业务逻辑的所有代码(状态、方法、监听、生命周期)放在一起,代码可读性和可维护性更强。

③ 更好的 TypeScript 支持

选项式 API 对 TS 的类型推断支持较差,需要额外配置。

组合式 API 原生支持 TS,ref/reactive 等 API 都能提供完善的类型推断,编写类型安全的组件更方便。

④ 更小的生产包体积

组合式 API 基于函数编写,Tree-shaking 更友好,未使用的 API 可以被打包工具自动移除,最终打包体积更小。


3. 组合式 API 核心组成(图片重点梳理)

分类 核心 API 作用
setup() 基本使用、访问 Props、与渲染函数配合 组合式 API 的入口,组件创建前执行
响应式核心 refreactivecomputedwatch 创建和管理响应式状态
响应式工具 isRefunreftoReftoRefs 辅助处理响应式数据
生命周期钩子 onMountedonUpdatedonUnmounted 注册组件生命周期逻辑
依赖注入 provideinject 跨层级组件传值

4. 面试终极串讲版(直接背)

组合式 API 是 Vue3 引入的、以函数为核心的组件编写方式,它以 setup() 为入口,通过 ref/reactive 等响应式 API、生命周期钩子和依赖注入 API 来组织组件逻辑。

相比选项式 API,它的核心优势是:更好的逻辑复用、更灵活的代码组织、更完善的 TypeScript 支持,以及更友好的 Tree-shaking,最终实现更小的打包体积

二十八、ToRef和ToRefs

1. toReftoRefs 的作用?

两者的核心作用都是基于响应式对象创建 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. toReftoRefs 的区别?(面试高频)

对比项 toRef toRefs
作用对象 响应式对象的单个属性 整个响应式对象的所有属性
返回值 单个 ref 对象 包含多个 ref 的普通对象
使用场景 只需要响应式对象中某一个属性 需要解构 / 展开整个响应式对象,且不丢失响应式
是否处理不存在的属性 即使属性不存在,也会创建一个可用的 ref 只会为对象当前存在的属性创建 ref

3. 关键面试点:为什么直接解构 reactive 会丢失响应式?

  • reactive 返回的是一个响应式代理对象,直接解构会取出其原始值(普通数据),不再具有响应式。
  • toRefs 为每个属性创建了 refref.value 会保持对源对象属性的引用,因此解构后仍能保持响应式。

4. 面试终极串讲版(直接背)

toReftoRefs 都是 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:**}
相关推荐
范同学~2 小时前
多个表单如何用element ui 校验
javascript·vue.js·ui
晚烛2 小时前
CANN 日志系统:调试与性能分析的日志艺术
前端·chrome·数据挖掘
FlyWIHTSKY2 小时前
Next中引入 Ant Design (antd)的配置
开发语言·前端·javascript
JAVA学习通3 小时前
《大营销平台系统设计实现》 - 营销服务 第9节:模板模式串联抽奖规则
服务器·前端·javascript
阿正的梦工坊3 小时前
【Typescript】10-条件类型与-infer
前端·javascript·typescript
GuWenyue3 小时前
我被 React 性能问题逼疯了,直到学会这 4 个优化技巧
前端
窗边的anini3 小时前
那个因为 vibecoding 差点搞砸约会的女孩,被 TRAE SOLO 救了
前端·人工智能·程序员
用户713874229003 小时前
OAuth 2.0 client_id深度解析:从规范到安全实践
前端
ZC跨境爬虫3 小时前
跟着 MDN 学CSS day_8:(盒模型完全解)
前端·javascript·css·ui·交互