vue面试题
MVC是什么?MVVM是什么? 区别?
MVC是model-view-controller:
- model层代表数据模型,负责业务逻辑和数据存取;
- view代表ui组件,负责显示数据;
- controller可以响应用户的操作,更新model,从而更新view,实现同步更新。
MVVM是model-view-viewModel的缩写,就是将MVC中的C(controller)换成VM(viewmodel)。
viewmodel就是view和model层的桥梁,最大的特点是做到了数据的双向绑定。数据会绑定到viewmodel层并自动将数据渲染到页面上,而视图变化时也会通知viewmodel层更新数据。
MVVM优点:
1、低耦合:简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View。视图(View)可以独⽴于 Model 变化和修改,⼀个 ViewModel 可以绑定到不同的”View”上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
2、可重⽤性:可以把⼀些视图逻辑放在⼀个ViewModel⾥⾯,让很多 view 重⽤这段视图逻辑。
3、独⽴开发:开发⼈员可以专注于业务逻辑和数据的开发(ViewModel),设计⼈员可以专注于⻚⾯设计。
参考资料
怎样理解 Vue 的单向数据流
数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父级组件的状态,从而导致应用的数据流向难以理解。
注意:在子组件直接用 v-model 绑定父组件传过来的 prop 这样是不规范的写法 开发环境会报警告
如果实在要改变父组件的 prop 值 可以再 data 里面定义一个变量 并用 prop 的值初始化它 之后用$emit 通知父组件去修改
vue的双向绑定是如何实现的?
Vue2使⽤的是Object.defineProperty()进⾏数据劫持,结合发布订阅的⽅式实现。
Vue3使⽤的是Proxy代理,使⽤ref或者reactive将数据转化为响应式数据
通过数据劫持和发布订阅。数据劫持就是设置数据的setter和getter属性来对数据进行一些操作。Vue2数据劫持用到的是Object.defineProperty,3用到的是Proxy。
Object.defineProperty和Proxy有什么区别?
Object.defineProperty监听对象属性。而Proxy监听的是整个对象
Vue2的响应式原理?
用了数据劫持、观察者模式以及发布订阅模式。(数据劫持在上面)
数据劫持与 Dep 对象创建
Vue 获取 data 后,将其传入 Observer 进行处理。Observer 会遍历 data 中的每个属性,为每个属性创建一个 Dep 对象(依赖收集器),这个 Dep 对象是发布 - 订阅模式中的发布者。并使用 Object.defineProperty 对属性进行数据劫持,将这些属性转换为响应式数据。当访问或修改这些属性时,会触发相应的 getter 和 setter 方法。
模板编译与 Watcher 创建(观察者模式体现)
在完成数据劫持后,Compiler 会对模板进行编译。在编译过程中,会为每个需要响应数据变化的地方创建一个 Watcher 对象。Watcher 对象负责监听数据的变化,并在数据变化时更新视图。
依赖收集(发布 - 订阅模式:订阅阶段)
Dep 对象内部维护有一个 subs 数组,用于收集 Watcher 订阅者。Dep 提供了 addSub 方法用于添加 Watcher(收集依赖),notify 方法用于通知所有 Watcher 数据发生了变化。
当组件视图刷新或者初始化时,组件对应的 Watcher 会被设置为 Dep.target。当访问响应式数据触发 getter 时,getter 会调用 Dep 的 addSub 方法,将 Dep.target(即当前 Watcher)添加到 dep.subs 数组中,完成依赖收集。收集完成后,Dep.target 会被清空。
数据更新与视图更新(发布 - 订阅模式:发布阶段)
当组件中的某个数据发生变化时,会触发相应属性的 setter 方法。setter 方法会调用 Dep 的 notify 方法,通知 dep.subs 数组中的所有 Watcher 数据已更新。
每个 Watcher 接收到通知后,会调用自身的 update 方法。update 方法会触发 vm.render 渲染函数,生成新的虚拟 DOM(VNode)。接着,update 方法会带着新的 VNode 调用 patch 方法,将新的 VNode 与旧的 VNode 进行对比,更新实际的 DOM 节点,从而实现视图的更新。
参考资料:
vue2简易手写
https://juejin.cn/post/6989106100582744072#heading-14
Vue3的响应式原理
Vue 3 的响应式原理基于 Proxy 对象和 Reflect 以及一套依赖收集与触发更新机制。
核心 API 基础
- Proxy:Vue 3 用 Proxy 替代 Vue 2 的 Object.defineProperty,解决了后者无法监听数组索引和 length 属性变化等局限,还能默认监听动态添加属性和属性删除操作。
- Reflect:ES6 引入的新特性,用于在代码运行时设置或获取对象成员,与 Object 相关方法功能类似但更具语义,和 Proxy 配合使用。
响应式对象创建
- reactive 函数:用于创建响应式对象,会递归处理对象的嵌套属性,确保引用类型属性也是响应式的。其 handler 中的 get 方法在获取属性值时会调用 track 函数进行依赖收集;set 和 deleteProperty 方法在属性值变化或属性删除时会调用 trigger 函数触发更新。
依赖收集与触发更新机制
- effect 函数:创建副作用函数,执行时会将当前副作用函数赋值给 activeEffect,执行完后置为 null。当依赖的数据变化时,副作用函数会重新执行。
- track 函数:收集依赖,利用 WeakMap 类型的 targetMap 存储对象的依赖信息。每个对象对应一个 Map(depsMap),depsMap 中每个键对应一个 Set(dep),dep 存储依赖该属性的副作用函数。
- trigger 函数:触发更新,当对象属性值变化时,从 targetMap 找到对应依赖集合,遍历执行其中的副作用函数。
不同类型数据响应式处理
- ref 函数:将基础类型数据包装成响应式对象,通过 get/set 存取器进行依赖追踪和触发更新。若数据是对象则调用 reactive 处理。
- toRef 函数:将对象中的某个属性转化为响应式数据,返回具有 value 属性的对象,对 value 的修改会影响原始对象属性值。
- toRefs 函数:将 reactive 创建的对象解构为多个响应式属性,方便在模板中使用,内部调用 toRef 实现。
参考资料:
什么是虚拟DOM?有什么优点?
虚拟DOM是一颗用js对象作为基础的树,用对象属性来描述节点,有tagname、props、children这些属性,是对真实DOM的一种抽象,VirtualDOM映射到真实DOM要经历VNode的create、diff、patch等阶段。
操作原生DOM慢,而js的运行效率高。我们可以把DOM对比操作放在js层,提高效率。运用patch算法来找到真正需要更新的节点,最大限度的减少DOM操作,提高性能。同时因为是js对象不依赖真实平台环境,所以具有了跨平台的优势。虚拟DOM能提升渲染性能,优势不在于单次的操作,而是在大量、繁琐的操作下,对视图进行高效的刷新,从总体上来看,性能消耗有一个让人能接受的范围。
为什么 data 是一个函数
组件中的 data 写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果
Vue 组件通讯有哪几种方式
props 和$emit 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的
$parent,$children 获取当前组件的父组件和当前组件的子组件
$attrs 和$listeners
父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。
$refs 获取组件实例
eventBus 兄弟组件数据传递 事件总线
vuex 状态管理
为什么v-for要加key?
Key的作用是尽可能复用DOM元素。这跟vue的虚拟DOM机制和diff算法有关系。Key是children的唯一标识。数据发生改变的时候不可能直接去生成全新 的虚拟DOM,这样违背了创造虚拟DOM的初衷,使性能开销更大。Diff算法要判断是否是同一个节点关键就是要根据key去判断。
为什么不能用index作为key?
Key的值对于节点来说必须是唯一的,能够标识出身份的。而index是会随着节点数目、位置不同发生改变的,这可能会导致diff算法过程中误将两个不相同的节点当成相同节点然后进行pathvnode,或者两个相同节点当成不相同节点,进行不必要的添加、删除DOM的操作,可能原本只需要进行移动子节点这种少量的操作变成了大量对DOM的操作,性能开销会变大。
v-if 和 v-show 的区别
v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。
v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)
使用场景
v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景
v-show 适用于需要非常频繁切换条件的场景
v-if 与 v-for 为什么不建议一起使用
v-for 和 v-if 不要在同一个标签中使用,因为解析时先解析 v-for 再解析 v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式。
vue 内置指令
什么是diff算法?
Diff算法是一种对比算法。对比旧的虚拟DOM和新的虚拟DOM,判断是哪个虚拟节点变更了,找出并只更新这个虚拟节点所对应的真实节点。Vue里如果数据发生改变就会触发setter,通过dep.notify方法通知所有订阅者watcher,订阅者们就会调用patch方法给真实DOM打补丁,更新视图。Patch方法对比当前同层的虚拟节点是否为同一种类型的标签,是就继续执行patchVnode方法进行深层对比,不是就直接整个节点替换为新的节点。sameVnode方法判断是否为同一类型节点,patchVnode方法判断新旧节点文本是否一样,以及子节点的情况,决定添加、删除DOM节点或者是执行updateChildren函数去对比子节点,updatechildren采用首位指针法,判断新旧首位节点是否是同一节点,用新vnode的key去找出旧节点中可以复用的位置,主要是子节点的位置的移动,如果最后发现新旧节点的子节点数量不同,就执行插入和删除操作。


参考资料
nextTick是什么?应用场景是?
用在DOM节点变化后立即调用以获取变化后的节点。因为vue中DOM更新是异步的,更新后可能不能及时获取到节点。浏览器的事件循环机制再细致一点是宏任务->微任务->UI渲染->宏任务。Vue为了防止重复渲染,会把宏任务、微任务的事件放到队列中,nextTick的回调函数设置成一个promise的形式放到微任务队列最后。如果浏览器兼容性不支持,就依次降级成mutationObserve、setInternal、setTimeout。
vue中数据频繁变化为什么只更新一次(vue的异步更新机制)
利用了浏览器的异步任务队列实现。首选微任务队列,宏任务次之。vue检测到数据变化会开启一个队列,在同一事件循环中会缓存所有的数据变化,如果同一个watcher被多次触发,只会被推到队列中一次。
生命周期钩子有哪些?一般在哪一步发请求?
分为创建、挂载、更新、销毁阶段
beforeCreate、created、 beforeMount、mounted、beforeUpdate、updated、beforeDestory、destoried、activated
- beforeCreate:new Vue后触发第一个钩子
- created:实例创建完成但还没有挂载到DOM,已经完成数据的检测,可以访问到data、methods、watch、computed,但是$el还不能访问。这时候可以进行一些初始化操作,发送简单的ajax请求。
- beforeMount:vue实例已经挂载到el上,模板编译完成,虚拟DOM创建完成。
- mounted:真实DOM挂载完毕,完成渲染,可以使用$ref访问到DOM节点。(:mounted 不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted 内部使用 vm.$nextTick。)
- beforeUpdate:响应式数据更新时调用,发生在虚拟DOM打补丁之前。
- updated:虚拟DOM打补丁完成,组件DOM已经更新。


可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面 loading 时间;
- ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
Vue 的父子组件生命周期钩子函数执行顺序
- 加载渲染过程
父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
- 子组件更新过程
父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
- 父组件更新过程
父 beforeUpdate->父 updated
- 销毁过程
父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
路由钩子
全局守卫三个:
router.beforeEach
、router.afterEach
、router.beforeResolve
(全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用)
to/from/next三个参数 路由对象是平时用this.$route获取到的对象
next() 进入该路由。
next(false): 取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)。
next 跳转新路由,当前的导航被中断,重新开始一个新的导航。
路由独享守卫beforeEnter
(在全局前置守卫调用之后)
写法
1 | const router = new VueRouter({ |
组件路由守卫
beforeRouteEnter
、beforeRouteLeave
、beforeRouteUpdate
(路由复用同一个组件时调用)
beforeRouteEnter在组件实例还没创建时调用,不能获取this,next的回调执行时期在mounted后
beforeRouteLeave 阻止用户离开,比如未保存草稿
1 | beforeRouteLeave (to, from , next) { |
路由解析流程
beforeEach(->beforeRouteUpdate-)> beforeEnter(独享)->解析异步路由组件->beforeRouteEnter->beforeResolve->导航被确认->
afterEach->beforeCreate->created->beforeMount->mouted->activated->beforeRouteEnter中next的回调
完整的导航解析流程:
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫 (2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
keep-alive组件
可以实现组件缓存。缓存组件内部状态避免重新渲染。缓存动态组件用的少,一般是缓存路由组件
- 常用的两个属性 include/exclude,允许组件有条件的进行缓存。
- 两个生命周期 activated/deactivated,用来得知当前组件是否处于活跃状态。
- keep-alive 的中还运用了 LRU(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰。
2.1版本前做法:
<!–这里是会被缓存的路由–>
<!–因为用的是v-if 所以下面还要创建一个未缓存的路由视图出口–>
- 2.1版本后做法:
新增include和exclude属性,支持逗号分割的字符串、正则表达式、数组
匹配规则:组件的name、组件的局部注册名称。不可匹配匿名,且不能匹配嵌套的组件,exclude优先级更大。
keep-alive包含的组件中会多出activated和deactivated
afterEach离开后执行deactivates 然后再进入另外的activated,有keep-alive的情况下不会触发beforeCreate、created 、beforeMount、 mountedbefore、destroy、destroyed。
route和router的区别
router是vueRouter的一个实例对象,是全局的,包含所有路由和许多关键的对象和属性。

每个路由都会有一个route对象,$route为当前router跳转对象。可以获取对应的name、path、params、query等

v-if和v-show的区别
v-show是单纯的css切换,相当于为元素添加display:none;v-if是将整个元素添加或删除,有一个编译和卸载的过程,会触发重建和销毁。v-if有更高的切换消耗,而v-show有更高的初始渲染消耗.
v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且DOM元素保留;
介绍vuex
是为vue.js提供的状态管理插件,多个组件依赖同一个状态的时候可以使用,避免繁杂的组件间传值操作。有state、getters、mutations、actions五个核心属性。
state可以存放公共状态;
getters可以对状态进行修饰;
mutations用来同步改变状态值,通过this.$store.commit()提交,接受两个参数,一个是方法名,一个是传入的payload对象,mutations里面的方法接受state参数和payload;
actions用来异步改变状态值,通过this.$store.dispatch()提交,actions里面的方法接受content参数和传过来的参数。content是对当前store对象的拷贝。方法里调用content.commit()改变状态,而不是直接改变状态;
modules将状态分成多个模块,以免状态太多导致store对象臃肿不好维护。module文件夹中新建moduleA.js moduleB.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const state={
//…
}
const getters={
//…
}
const mutations={
//…
}
const actions={
//…
}
export default{
state,
getters,
mutations,
actions
}
在store/index.js引入模块
import moduleA from ‘…..’
import moduleB from ‘…..’
const store = new Vuex.Store({
modules:{
moduleA,
moduleB
}
})
export default store组件中批量使用state状态(语法糖):引入mapState辅助函数,通过扩展运算符将state混入到computed对象中。
1 | import {mapState} from ‘vuex’ |
Computed、watch的区别?如何实现的?
Computed具有缓存的功能,依赖的属性值发生变化就会更新视图。在模板中放入过多逻辑会让模板难以维护,而且过多的计算和频繁更新很消耗性能,所以在需要对数据进行复杂处理,且可能多次使用的情况下,尽量采取计算属性的方式。Watch更多的是观察的作用,可以监听数据执行回调,需要深度监听对象中的属性可以打开deep:true选项,对对象中的每一项进行监听。
Vue-router常用路由模式实现原理?
Hash、history。
Hash模式是把前端路由的路径用井号#拼接在真实url后面的模式。#后发生改变时浏览器不会重新发起请求而是触发hashchange事件。
History模式路径中不包含#,依赖于html5的history api中的pushState和replaceState方法。由于改变了地址,刷新时会按照修改后的地址请求后端,需要后端配置处理,对地址访问做映射,否则会404.
Vue2和vue3的区别
性能提升:Vue3 在性能上进行了优化,虚拟dom算法优化比如静态提升、Patch Flag和基于 Proxy 的响应式系统,这些都让 Vue3 比 Vue2 更快 。
(静态提升是指将模板中不会发生变化的节点提取出来,只在首次渲染时创建;Patch Flag 是为动态节点添加标记,在更新时只比较有标记的节点,提高了比较效率)
响应式原理:Vue 2:基于 Object.defineProperty() 方法实现响应式。有一些局限性,比如无法检测对象属性的添加和删除,对于数组,通过索引直接修改元素时无法更新视图。
Vue 3:使用Proxy 直接代理整个对象而非对象属性,就可以监听到新增属性和删除属性,并且可以监听数组的变化。定义响应式变量:vue2直接写在data,vue3则用ref和reactive定义变量
组件选项式 API 与组合式 API:vue2是选项型API, 代码里分割了不同的属性(properties):data,computed属性,methods,等等。Vue3是组合型API,需要使用一个新的setup()方法,此方法在组件初始化构造的时候触发。可以根据逻辑功能来组织代码。
生命周期钩子:vue3命名发生改变,并且可以在组合式 API 中使用。新增了 setup 函数,它在 beforeCreate 和 created 之前执行,同时,生命周期钩子可以在 setup 函数中使用。
Typescript类型支持:从设计上就对 TypeScript 有更好的支持,组合式 API 的函数式风格使得类型推导更加自然和准确。
多根节点组件:Vue3 template下可以包含多个根节点。
打包体积:Vue 3对Tree Shaking有天然支持。因为Vue 3 采用了 ES 模块(ESM)的方式进行代码组织,打包工具(如 Vite、Webpack 等)可以在编译阶段分析模块之间的依赖关系,准确识别出未使用的代码,并将其从最终的打包文件中移除,从而减小包的体积,提高应用的加载速度。

Vue和React的区别
设计理念:
Vue:推崇易用性和简洁性,提供了更多开箱即用的功能,比如模板语法、数据绑定等。
React:函数式编程。更注重灵活性和可扩展性,推崇”一切皆组件”的理念,开发者可以自由组合。
模板与JSX:
Vue:使用基于HTML的模板语法,易于理解和上手。
React:使用JSX语法,它是一种看起来像HTML的JavaScript语法扩展。
响应式系统:
Vue:使用依赖收集的机制,通过Object.defineProperty实现数据响应。
React:使用useState和useEffect钩子来处理状态和副作用,依赖于一个高效的虚拟DOM算法。
组件通信:
Vue:有props和events来实现父子通信,以及provide和inject来实现跨组件通信。
React:有props和context来实现通信,也支持自定义上下文。
构建工具:
Vue:通常与Vue CLI配合使用,提供一键式项目脚手架。
React:通常与Create React App配合使用,同样提供快速的项目启动。
生态系统:
Vue:有Vuex用于状态管理,Vue Router用于路由。
React:有Redux用于状态管理,React Router用于路由。
使用过 Vue SSR 吗?说说 SSR
SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端。
优点:
SSR 有着更好的 SEO、并且首屏加载速度更快
缺点: 开发条件会受到限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。
服务器会有更大的负载需求
vue 中使用了哪些设计模式
1.工厂模式 - 传入参数即可创建实例
虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例
vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.装饰模式: (@装饰器的用法)
6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
你都做过哪些 Vue 的性能优化
这里只列举针对 Vue 的性能优化 整个项目的性能优化是一个大工程 可以另写一篇性能优化的文章 哈哈
- 对象层级不要过深,否则性能就会差
- 不需要响应式的数据不要放到 data 中(可以用 Object.freeze() 冻结数据)
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分使用场景
- v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if
- 大数据列表和表格性能优化-虚拟列表/虚拟表格
- 防止内部泄漏,组件销毁后把全局变量和事件销毁
- 图片懒加载
- 路由懒加载
- 第三方插件的按需引入
- 适当采用 keep-alive 缓存组件
- 防抖、节流运用
- 服务端渲染 SSR or 预渲染