一. 谈一谈你对vue的理解?
1.1 vue是一套用于构建用户界面的渐进式框架, Vue的核心库只关注视图层
渐进式: 你可以只使用Vue的一部分功能,也可一根据项目需求,逐步引入更多功能,最终搭建完整应用
1.2 声明式框架
- 早在jq的时代编写的代码都是命令式的,命令式框架重要特点就是关注过程
- 声明式框架更加关注结果,命令式的代码封装到了Vuejs中,过程靠Vuejs来实现
1.3 MVVM 模式
目的: 职责划分,分成管理
1. MVC 全称 Model-View-Controller
- Model: 数据层(比如数据库或业务逻辑)
- View: 视图层(用户看到的页面)
- Controller: 控制器,负责接受用户操作,更新模型或视图
举例
用户点击按钮->控制器接受点击->改变数据模型->在通知视图更新
2.MVVM
全称: Model- View -ViewModel
- Model: 数据层
- view : 视图层
- ViewModel: 视图模型,连接View和Model的桥梁,通过双向数据绑定实现同步
Model ↔ ViewModel ↔ View (双向绑定)
vue本身就是MVVM的一个实践框架:
- data( ) 是Model
- template 是View
- Vue实例作为ViewModel,负责响应式绑定和数据变化侦测
- 用户操作页面(View)后,Vue会自动把值更新到数据(Model),反之亦然
1.4 采用虚拟DOM
虚拟DOM是js对真实DOM的一种表示,简单理解,就是把HTML元素结构用js对象表示,后续用它来更新页面
因为操作真实DOM非常慢,所以: Vue,React等框架使用虚拟DOM来先做内存中的计算,再统一最小化地更新真实DOM
工作流程:
【数据变化】→ 【生成新的虚拟 DOM】 → 【和旧的虚拟 DOM 做对比(Diff 算法)】
→ 【找出差异】 → 【最小化更新真实 DOM】
1.5 组件化
组件化就是把有页面拆分成一个个小的,独立的,可服用的功能模块(组件)来开发和维护,每个组件就是一个包含自己结构,样式,逻辑的独立单元
为什么要组件化?
1. 解耦: 每个模块职责单一,逻辑清晰,互不影响
2. 复用: 封装成组件后可以在多个页面或项目中服用
3. 便于协作: 多人开发时可以按组件划分任务,互不冲突
1.6 区分编译打包和运行(浏览器)时
1. 编译时:
- Vue文件中的写的<template>,<script><style>会被拆分解析
- 例如 .vue文件会被编译成渲染函数(render function)
- ES6+ 会被Babel 编译为ES5,以适配老浏览器
- SCSS/LESS 也会被编译为CSS
2.打包时
- 把项目中引用的模块(组件,依赖库等)合并为一份或几份压缩后的js/css文件
- 会进行Tree-Shaking(去除未使用代码),压缩混淆,代码分割等优化
工具: Webpack/Vite
目的: 提高加载速度,适应线上部署
3.运行时
- 项目代码已加载到浏览器,JS开始执行
- Vue会创建响应式系统,挂在组件,绑定DOM
- 用户交互时,触发事件,修改数据,触发虚拟DOM更新
依赖: 浏览器环境,DOM,Vue的runtime包
二. 谈一谈你对SPA的理解
1.1. 理解基本概念
- SPA 单页应用,默认情况下我们编写Vue,React都只有一个html页面,并且提供一个挂载点,最终打包后会再此页面中引入对应的资源(页面的渲染全部是由JS动态进行渲染的),而不是每次跳转都重新加载页面.
- MPA 多页应用,多个html页面,每个页面必须重复加载,js,css等相关资源,服务端返回完整的html,同时数据也可以再后端进行获取一并返回"模版引擎". 多页应用跳转需要整页资源刷新, 服务器端渲染SSR
如何分清在哪渲染: HTML是在前端动态生成的"客户端渲染",在服务端处理好并返回的是"服务端渲染".
1.2 SPA的特点
1. 优点
- 用户体验好: 页面不刷新,加载速度快,交互流畅
- 前后端分离: 前端负责页面逻辑,后端只提供API
- 组件化开发: 配合Vue等框架实现模块化开发,易维护,易复用
2. 缺点:
- 首屏加载慢: 需要一次加载大量JS,影响首次打开速度
- SEO不友好: 因为页面是通过JS动态渲染的,搜索引擎难以抓取
- 前端逻辑复杂: 需要自己处理路由,状态管理,权限控制
3.SPA单页和多页MPA对比
对比项 | SPA(单页应用) | MPA(多页应用) |
---|---|---|
页面数量 | 一个 HTML,内容动态切换 | 多个 HTML,每个页面独立 |
路由处理 | 前端路由(如 Vue Router) | 传统后端路由(服务端返回页面) |
页面刷新 | 不刷新页面,内容局部更新 | 每次跳转都刷新页面 |
SEO 支持 | 差,需要配 SSR | 好,天然支持搜索引擎 |
首屏速度 | 慢(需加载 JS) | 快(每个页面加载需要的内容) |
三. Vue为什么需要虚拟DOM?
1.1 基本概念
基本上所有框架都引入了虚拟DOM来对真实DOM进行抽象,也就是现在大家所熟知的VNode(虚拟)和VDOM(虚拟DOM)
- Virtual DOM 就是用js对象来描述真实DOM,是对真实DOM的抽象,由于直接操作DOM性能低但是js层的操作效率高,可以将DOM操作转化成对象操作,最终通过diff算法比对差异进行更新DOM(减少了对真实DOM的操作).
- 虚拟DOM不依赖真实平台环境从而也可以实现跨平台
1.2 补充:VDOM是如何生成的?
- 在vue中我们常常会为组件编写模版 - template
- 这个模版会被编译器编译为渲染函数- render
- 在接下来的挂在过程会diaoyongrender函数,返回的对象就是虚拟dom
- 会在后续的patch过程中进一步转化为真实dom
1.3 再次补充: VDOM如何做diff的?
- 挂在过程结束后,会记录第一次生成的VDOM - oldVnode
- 当响应式数据发生变化时,将会引起组件重新render,此时就会生成新的VDOM-newVnode
- 使用oldVnode 与 newVnode 做diff操作,将更改的部分应到真实DOM上,从而转换为最小量dom操作,高效更新视图
四. 请说一下你对响应式数据的理解?
1.1 核心本质:
无论Vue2还是Vue3,响应式的底层原理都遵守"依赖收集+派发更新"机制:
- 数据被访问时,记录"谁"在用这个数据(依赖收集)
- 数据被修改时,通知相关逻辑或组件重新执行(派发更新)
Vue2响应式底层原理(基于Object.defineProperty)将属性劫持,数组则是通过重写数组方法来实现,多层对象是通过递归来实现劫持
关键模块
- Dep 每个属性一个Dep,保存他的依赖(Watcher列表)
- Watcher 每个使用这个属性的地方(比如组件) 对应一个Watcher
数据变化触发视图更新的流程:
组件渲染(访问data) -> getter -> Dep收集当前Watcher
修改数据 -> setter -> Dep 通知 -> Watcher.run( ) -> 组件重新渲染
1.2 vue2缺陷和Vue3响应式实现的区别
vue2:
- 在vue2的时候使用defineProperty来进行数据的劫持,需要对属性进行重写添加getter及setter性能差
- 当新增属性和删除属性时无法监控变化,需要通过$set,$delete实现
- 数组不采用defineProperty 来进行劫持(浪费性能,对所有索引进行劫持会造成性能浪费)需要对数组单独进行处理
- 对于es6中新产生的Map,Set这些数据结构不支持
vue3:
vue3使用Proxy,可以代理整个对象,能够监听新增,删除属性,且支持更多数据类型(如Map,Set)
五. v-if 和v-shou
1.渲染方式不同
- v-if 根据条件决定是否创建或销毁DOM元素,条件为假时元素完全不存在与页面
- v-show 元素始终存在,只是通过CSS的display: none来控制显示与隐藏
2.切换性能差异
- v-if 频繁切换时性能开销大,因为需要多次创建和销毁DOM (触发重排和重绘) 重排一定会引起重绘,但重绘不一定会引起重排
- v-show只改变样式,切换过程块,性能开销较小 (只触发重绘,不会导致重排,因为布局结构未发生变化 )
3.初始渲染开销
- v-if 初次条件为假时,不渲染元素,节省初次渲染那性能
- v-show 无论条件真假,都会渲染元素,初始渲染开销相对较大
4.优先级
- v-if 优先级高于v-show
当同一个元素同时写了v-if 和 v-show 时,Vue会先判断v-if的条件,如果为假,元素根本不会渲染,v-show就不会生效 - 只有v-if 条件为真时,v-show才会控制元素的显示和隐藏
- 原因: v-if 控制的是元素是否存在于DOM, 属于"结构性"条件v-show控制的是元素的css样式,是"表现层"的控制.结构性条件优于表现层控制
六. computed 和watch 的区别
1.computed
- 用于声明式定义一个基于响应式数据的计算属性
- 特点 : 会缓存计算结果,只有当依赖的数据变化时才重新计算
- 使用场景: 适合用来做数据的派生计算,避免重复执行昂贵计算.
const state = reactive({
count: 1,
price: 10
})// 定义计算属性
const total = computed(() => {
return state.count * state.price
})// 使用
console.log(total.value) // 10// 当 state.count 或 state.price 改变时,total 会自动更新且有缓存
state.count = 2
console.log(total.value) // 20
2.watch 用法和特点
- 作用: 监听响应式数据的变化,并在变化时执行回调函数
- 特点: 不缓存,数据每次变化都会执行回调
- 使用场景: 适合处理异步操作,手动执行副作用(如请求接口,操作DOM,触发动画等)
- 有点: 灵活,可监听多个数据,便于执行复杂逻辑
const state = reactive({
count: 0
})// 监听 state.count 变化
watch(() => state.count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变成了 ${newVal}`)
})// 支持立即执行
watch(() => state.count, (newVal) => {
console.log('立即执行,当前 count:', newVal)
}, { immediate: true })// 支持深度监听
const obj = reactive({ nested: { num: 0 } })
watch(() => obj.nested, (newVal) => {
console.log('嵌套对象变化:', newVal)
}, { deep: true })// 监听多个数据
watch([() => state.count, () => obj.nested.num], ([newCount, newNum]) => {
console.log(`count: ${newCount}, num: ${newNum}`)
})
七. vue3 ref 和reactive 区别
1. ref
- 作用: 用于创建一个响应式的基本类型数据引用,也可以包装对象
- 返回值: 返回一个对象,内部值存放在.value属性上,需要通过.value来访问和修改
- 使用场景: 适合处理基本类型响应式数据,或者需要将某个值单独包装程序响应式场景
- 特点: 对基本类型进行相应式处理;通过.value 访问和赋值, 适合配合解构使用,避免相应丢失,底层采用的是Object.defineProperty( ) 实现的
2.reactive
- 将一个普通对象或数组转换为深层响应式代理对象
- 返回的是被代理的原对象,访问属性时自动相应,无需.value
- 适用场景: 适合处理复杂对象,嵌套对象和数组的响应式
- 特点:
1. 适用Proxy 深度代理对象所有属性;
2. 解构会导致响应丢失;
3. 适合状态管理的大型对象
八.vue3 watch 和 watchEffect 的区别
1.watch
显示地监听一个或多个特定的响应式数据源,当其发生变化时执行回调函数
特点:
- 明确监听目标(如ref,reactive的某个属性,getter函数等)
- 回调函数有新值和旧值两个参数
- 支持deep深度监听和immediate 立即执行
- 更适合处理复活用逻辑,异步请求等
适用场景:
- 某个具体数据变化时执行逻辑(如发请求,更新状态)
- 需要用到新旧值对比
- 需要手动监听某个计算属性或getter的结果
2.watchEffect
作用: 自动收集副作用用到的所有响应式数据,这些依赖变化时就重新执行回调.
特点:
- 不需要指定监听的目标,自动追踪依赖
- 第一次立即执行(类似immediate:true)
- 依赖变化时自动重新运行副作用函数
- 支持副作用清理
九. vue的生命周期
1.Vue2的生命周期
- beforeCreate 实例初始化完成前,data和methods都还未挂载(创建前)
- created 实例已创建完成,可访问data,methods,computed (创建后)
用途:
1. 请求接口获取数据 适合发起不依赖DOM的请求
2. 初始化定时器
3. 事件总线监听 - beforeMount 模版已编译,尚未挂在到DOM (挂在前)
- mounted 模版已挂在到真实DOM,适合操作DOM,请求数据(挂载后)
1. 操作DOM (如图标库初始化)
2. 获取DOM节点尺寸
3. 请求数据(如果需要操作DOM结构) - beforeUpdate 响应式数据更新前调用,适合更新前的逻辑处理 (更新前)
1. 对更新前的旧数据做处理
2. 手动保存滚动位置等 - updated 响应式数据更新后,DOM已重新渲染完成(更新后)
1. 获取最新DOM状态
2. 使用$nextTick 做动画,滚动条调整等 - beforeDestroy 实例销毁前调用,可以一座清理操作(销毁前)
1. 清除定时器
2. 解绑事件监听
3. 清理全局状态 - destyoyed 实例销毁后调用,所有事件监听器,子组件等已清理
2.Vue3 生命周期(组合式API)
Vue3 中推荐使用组合式API,声明周期名称以on开头(在setup()中调用):
Vue2 Options API | Vue3 Composition API |
---|---|
beforeCreate | ⛔(已废弃) |
created | ⛔(已废弃) |
beforeMount | onBeforeMount() |
mounted | onMounted() |
beforeUpdate | onBeforeUpdate() |
updated | onUpdated() |
beforeDestroy | onBeforeUnmount() |
destroyed | onUnmounted() |
—— | onActivated() / onDeactivated() (keep-alive 用) |
—— | onErrorCaptured() (捕获子组件错误) |
十. Vue中key的作用和原理
1. 唯一表示VNode
-
key的本质是: 为每一个虚拟DOM节点设置唯一的标识
-
帮助Vue准确判断哪些节点需要服用,哪些需要新增或删除
2.提高DIff算法效率
- 没有key时,Vue默认会用"就地复用策略",会造成错误复用,渲染错误,动画异常等问题
- 有key后,Vue会根据key精准对比新旧节点,提高性能和准确性
3.对比规则:
1. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
- 若虚拟DOM中内容没变,直接使用之前的真实DOM!
- 若虚拟DOm中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
2.旧虚拟DOM中未找到与新虚拟DOM相同的key
- 创建新的真实DOM,随后渲染到页面
3. 用index作为key可能会引发的问题:
1. 若对数据进行: 逆序添加,逆序删除等破坏顺序操作:
会产生没有必要的真实DOM更新==> 界面效果没问题,但效率低
2. 如果结构中还包含输入类的DOM:
会产生错误DOM更新===> 界面有问题
4.开发中如何选择key?:
1. 最好使用每天数据的唯一表示为key,比如id,手机号,身份证号,学号等唯一值
2. 如果不存在对数据的逆序添加,逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的
十一. 自定义指令
1.本质:
- Vue自定义指令其实是对原生DOM操作的封装
- 指令钩子函数会接受到当前绑定的DOM元素el,通过操作它实现各种效果
- 本质上,Vue在编译模版时,会解析指令,调用对应的钩子函数完成操作
2.指令钩子参数详解
(el: HTMLElement, binding: DirectiveBinding, vnode: VNode, prevVnode: VNode | null) => void
- el : 指令绑定的真实DOM元素
- binding: 一个对象,包含指令相关信息
binding.value: 绑定的值
binding.oldValue: 绑定的旧值(只有更新和卸载时有)
binding.arg:指令传入的参数,如v-my-directive: arg中的arg
binding.modifiers:修饰符对象,如v-my-directive.foo.bar变成{foo:true,bar:true} - vnode: 当前虚拟节点
- prevVnode :上一个虚拟节点
3.应用场景:
1.权限控制
- 根据用户权限,控制元素的显示或隐藏
- 适合按钮,菜单等权限校验
app.directive('permission', {
mounted(el, binding) {
const userRole = getUserRole();
if (!userRole.includes(binding.value)) {
el.style.display = 'none';
}
}
});
2.懒加载与无限滚动
- 监听滚动事件,实现图片懒加载或列表滚动到底部自动加载更多
- 性能优化必备手段
十二. 说说你对nextTick的理解?
1. 概念
Vue中视图更新是异步的,使用nextTick方法可以保证用户定义的逻辑在更新之后执行
可用于获取更新后的DOM,多次调用nextTick会被合并
因为: Vue在修改响应式数据后,不会立即更新DOM,而是先缓存修改,然后等同步代码跑完后,再一次性批量更新DOM,nextTick就是告诉Vue:
在你DOM渲染完之后通知我,我再执行我的逻辑
2.使用场景
场景 | 为什么用 nextTick |
---|---|
修改数据后需要立即获取 DOM | 数据变了,但 DOM 还没更新,需等 DOM 更新后操作 |
组件生命周期内要访问更新后的 DOM | 如在 mounted 里等待某些异步 DOM 生成 |
手动聚焦某个输入框 | 改变可见性后,DOM 不一定立即出现 |
UI 动画或过渡计算依赖 DOM | 等 DOM 完整渲染后再做动画更安全 |
十三. 讲一下keep-alive
keep-alive用于缓存组件实例,当组件切换时被缓存的组件不会被销毁,下次再激活时可以继续使用之前的状态,提升性能
1.解决了什么问题?
默认情况下,Vue的组件在切换时会销毁卸载,再重新挂载创建,比如常见的tab切换或动态组件切换时
但有些组件,比如:
- 表单页面
- 复杂表格
- 需要保留滚动/编辑状态的页面
频繁销毁和重建,会造成性能浪费和用户体验下降,keep-alive就是用来解决这个问题的。
2. 工作原理
被keep-alive包裹的组件会被Vue缓存下来,切换时不是销毁,而是隐藏,再次激活时是显示
<template>
<keep-alive>
<component :is="currentView" />
</keep-alive>
</template>
<script setup>
import FormPage from './FormPage.vue';
import ListPage from './ListPage.vue';
import { ref } from 'vue';
const currentView = ref('ListPage');
</script>
上面这个例子中,当currentView切换时,之前的组件不会被销毁
3.keep-alive的常用属性
<keep-alive include="FormPage,ChartPage" exclude="LoginPage" max="10">
-
include 只缓存哪些组件(名字)
- exclude 排除哪些组件不缓存
- max 最多缓存多少个组件实例,超出会LRU置换
注意: 这些名字必须是组件的name选项
生命周期钩子:
当组件被缓存时不会触发created和mouned,而是:
- activated():被显示时触发
- deactiveted( ) :被隐藏时触发
export default {
name: 'MyForm',
activated() {
console.log('组件被激活');
},
deactivated() {
console.log('组件被缓存');
}
}
十四. Vue中使用了哪些设计模式?
1.观察者模式
对象之间一对多依赖,一个对象状态变化,所有依赖它的对象都会收到通知
Vue中应用:
- Vue的响应式系统(如data,ref,reeactive)
- watch,computed 底层原理
2.发布-订阅模式(Pub-Sub Pattern)
发布者和订阅者之间不直接通信,通过事件中心解耦
Vue中应用:
- 组件间通信(如EventBus)
- Vue内部事件机制(如$meit/$on)
3.策略模式
指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案
4. 装饰器模式
在不改变原类的情况下,动态扩展其功能
Vue中的应用:
- Vue3中Composition API的reactive,ref
- @Watch @Component 等装饰器
5. 代理模式
通过代理对象控制对目标对象的访问
Vue中应用:
- Vue3响应式系统的核心Proxy
- 拦截属性的get/set,实现依赖收集和派发
6.单例模式
定义:全局只有一个实例
Vue中应用:
- Vuex 状态管理(store 是全局唯一)
- Vue根实例
7.组合模式
将对象组合成树形结构表示"部分-整体"的层次结构
Vue中应用:
- 组件树结构(父组件,子组件递归组合)
- 插槽(slot) / 嵌套组件渲染
总结: Vue中大量使用了设计模式,比如响应式原理中用到观察者模式和代理模式,组件通信中用到发布-订阅模式,Vue3用Proxy 重写响应式就是代理模式的体现,像Vuex是一个典型的单例模式,组件树结构式组合模式,而Vue的表单校验,diff算法中也使用了策略模式,这些设计模式提升了Vue的扩展性和可维护性