2023前端面试系列— Vue篇
Vue 面试专题
简述MVVM
什么是MVVM?
视图模型双向绑定
,是Model-View-ViewModel
的缩写,也就是把MVC
中的Controller
演变成ViewModel。Model
层代表数据模型,View
代表UI组件,ViewModel
是View
和Model
层的桥梁,数据会绑定到viewModel
层并自动将数据渲染到页面中,视图变化的时候会通知viewModel
层更新数据。以前是操作DOM结构更新视图,现在是数据驱动视图
。
MVVM的优点:
1.低耦合
。视图(View)可以独立于Model变化和修改,一个Model可以绑定到不同的View上,当View变化的时候Model可以不变化,当Model变化的时候View也可以不变;
2.可重用性
。你可以把一些视图逻辑放在一个Model里面,让很多View重用这段视图逻辑。
3.独立开发
。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
4.可测试
。
Vue底层实现原理
vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,在数据变动时发布消息给订阅者,触发相应的监听回调
Vue是一个典型的MVVM框架,模型(Model)只是普通的javascript对象,修改它则试图(View)会自动更新。这种设计让状态管理变得非常简单而直观
Observer(数据监听器) : Observer的核心是通过Object.defineProprtty()来监听数据的变动,这个函数内部可以定义setter和getter,每当数据发生变化,就会触发setter。这时候Observer就要通知订阅者,订阅者就是Watcher
Watcher(订阅者) : Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
- 在自身实例化时往属性订阅器(dep)里面添加自己
- 自身必须有一个update()方法
- 待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调
Compile(指令解析器) : Compile主要做的事情是解析模板指令,将模板中变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加鉴定数据的订阅者,一旦数据有变动,收到通知,更新试图
谈谈对vue生命周期的理解?
每个Vue
实例在创建时都会经过一系列的初始化过程,vue
的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件
create阶段
:vue实例被创建
beforeCreate
: 最初调用触发,创建前,此时data和methods中的数据都还没有初始化,data和events都不能用
created
: 创建完毕,data中有值,未挂载,data和events已经初始化好,data已经具有响应式;在这里可以发送请求mount阶段
: vue实例被挂载到真实DOM节点
beforeMount
:在模版编译之后,渲染之前触发,可以发起服务端请求,去数据,ssr中不可用,基本用不上这个hook
mounted
: 在渲染之后触发,此时可以操作DOM,并能访问组件中的DOM以及$ref,SSR中不可用update阶段
:当vue实例里面的data数据变化时,触发组件的重新渲染
beforeUpdate
:更新前,在数据变化后,模版改变前触发,切勿使用它监听数据变化
updated
:更新后,在数据改变后,模版改变后触发,常用于重渲染案后的打点,性能检测或触发vue组件中非vue组件的更新destroy阶段
:vue实例被销毁
beforeDestroy
:实例被销毁前,组件卸载前触发,此时可以手动销毁一些方法,可以在此时清理事件、计时器或者取消订阅操作
destroyed
:卸载完毕后触发,销毁后,可以做最后的打点或事件触发操作
组件生命周期
生命周期(父子组件) 父组件beforeCreate --> 父组件created --> 父组件beforeMount --> 子组件beforeCreate --> 子组件created --> 子组件beforeMount --> 子组件 mounted --> 父组件mounted -->父组件beforeUpdate -->子组件beforeDestroy–> 子组件destroyed --> 父组件updated
加载渲染过程 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
挂载阶段 父created->子created->子mounted->父mounted
父组件更新阶段 父beforeUpdate->父updated
子组件更新阶段 父beforeUpdate->子beforeUpdate->子updated->父updated
销毁阶段 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
computed与watch
computed
计算属性,依赖其它属性计算值,内部任一依赖项的变化都会重新执行该函数,计算属性有缓存,多次重复使用计算属性时会从缓存中获取返回值,计算属性必须要有return
关键词。
watch
侦听到某一数据的变化从而触发函数。当数据为对象类型时,对象中的属性值变化时需要使用深度侦听deep
属性,也可在页面第一次加载时使用立即侦听immdiate
属性。
使用场景 computed
:当一个属性受多个属性影响的时候使用,例:购物车商品结算功能 watch
:当一条数据影响多条数据的时候使用,例:搜索数据
Vue 的响应式原理
Vue 2 中的数据响应式会根据数据类型做不同的处理。如果是对象,则通过Object.defineProperty(obj,key,descriptor)
拦截对象属性访问,当数据被访问或改变时,感知并作出反应;如果是数组,则通过覆盖数组原型的方法,扩展它的7个变更方法(push、pop、shift、unshift、splice、sort、reverse),使这些方法可以额外的做更新通知,从而做出响应。
缺点:
- 初始化时的递归遍历会造成性能损失;
- 通知更新过程需要维护大量
dep
实例和watcher
实例,额外占用内存较多; - 新增或删除对象属性无法拦截,需要通过
Vue.set
及delete
这样的 API 才能生效; - 对于ES6中新产生的Map、Set这些数据结构不支持。
Vue 3 中利用ES6的Proxy
机制代理需要响应化的数据。可以同时支持对象和数组,动态属性增、删都可以拦截,新增数据结构均支持,对象嵌套属性运行时递归,用到时才代理,也不需要维护特别多的依赖关系,性能取得很大进步。
Vue中key的作用?
key
的作用主要是为了更加高效的更新虚拟 DOM
。
Vue 判断两个节点是否相同时,主要是判断两者的key
和元素类型tag
。因此,如果不设置key
,它的值就是undefined
,则可能永远认为这是两个相同的节点,只能去做更新操作,将造成大量的 DOM 更新操作。
组件中的data为什么是一个函数?
1.一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。
2.如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。
为什么v-for和v-if不建议用在一起
1.当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费
2.这种场景建议使用 computed,先对数据进行过滤
注意:3.x 版本中 v-if
总是优先于 v-for
生效。由于语法上存在歧义,建议避免在同一元素上同时使用两者。比起在模板层面管理相关逻辑,更好的办法是通过创建计算属性筛选出列表,并以此创建可见元素。
React/Vue 项目中 key 的作用
-
key的作用是为了在diff算法执行时更快的找到对应的节点,
提高diff速度,更高效的更新虚拟DOM
;vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。
-
为了在数据变化时强制更新组件,以避免
“就地复用”
带来的副作用。当 Vue.js 用
v-for
更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。重复的key会造成渲染错误。
数组扁平化转换
在说到模版编译的时候,有可能会提到数组的转换,一般就用递归处理
将 [1,2,3,[4,5]] 转换成
1 | { |
1 | // 测试数组 |
说说你对 SPA 单页面的理解,它的优缺点分别是什么?
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML
、JavaScript
和 CSS
。一旦页面加载完成,SPA
不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
- 优点:
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
- 基于上面一点,SPA 相对对服务器压力小;
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
- 缺点:
- 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将
JavaScript
、CSS
统一加载,部分页面按需加载; - 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
- SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在
SEO
上其有着天然的弱势。
vue组件的通信方式
-
父子组件通信:
父向子传递数据是通过props
,子向父是通过$emit
触发事件;通过父链/子链也可以通信($parent/$children
);
ref
也可以访问组件实例;provide/inject
;$attrs/$listeners
。 -
兄弟组件通信:
全局事件总线EventBus
、Vuex
。
$emit
/$on
自定义事件 兄弟组件通信
Event Bus
实现跨组件通信Vue.prototype.$bus = new Vue()
自定义事件 -
跨层级组件通信:
全局事件总线EventBus
、Vuex
、provide/inject。
$emit 后面的两个参数是什么
1、父组件可以使用 props 把数据传给子组件。
2、子组件可以使用 $emit,让父组件监听到自定义事件 。
vm.$emit( event, arg );
//触发当前实例上的事件,要传递的参数
vm.$on( event, fn );
//监听event事件后运行 fn;
子组件
1 | <template> |
父组件
1 | <template> |
nextTick的实现
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
nextTick
是 Vue 提供的一个全局 API,由于 Vue 的异步更新策略,导致我们对数据修改后不会直接体现在 DOM 上,此时如果想要立即获取更新后的 DOM 状态,就需要借助该方法。
Vue 在更新DOM
时是异步执行的。当数据发生变化,Vue 将开启一个异步更新队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher
被多次触发,只会被推入队列一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick
方法会在队列中加入一个回调函数,确保该函数在前面的 DOM 操作完成后才调用。
使用场景:
- 如果想要在修改数据后立刻得到更新后的DOM结构,可以使用
Vue.nextTick()
- 在
created
生命周期中进行DOM
操作
使用过插槽么?用的是具名插槽还是匿名插槽或作用域插槽
vue中的插槽是一个非常好用的东西slot说白了就是一个占位的 在vue当中插槽包含三种一种是默认插槽(匿名)一种是具名插槽还有一种就是作用域插槽 匿名插槽就是没有名字的只要默认的都填到这里具名插槽指的是具有名字的
keep-alive的实现
keep-alive是Vue.js的一个内置组件。它能够不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。
作用:实现组件缓存,保持这些组件的状态,以避免反复渲染导致的性能问题。 需要缓存组件 频繁切换,不需要重复渲染
场景:tabs标签页 后台导航,vue性能优化
原理:Vue.js
内部将DOM
节点抽象成了一个个的VNode
节点,keep-alive
组件的缓存也是基于VNode
节点的而不是直接存储DOM
结构。它将满足条件(pruneCache与pruneCache)
的组件在cache
对象中缓存起来,在需要重新渲染的时候再将vnode
节点从cache
对象中取出并渲染。
keep-alive 的属性
它提供了include与exclude两个属性,允许组件有条件地进行缓存。
include定义缓存白名单,keep-alive会缓存命中的组件;exclude定义缓存黑名单,被命中的组件将不会被缓存;max定义缓存组件上限,超出上限使用LRU的策略置换缓存数据。
在动态组件中的应用
1 | <keep-alive :include="whiteList" :exclude="blackList" :max="amount"> |
在vue-router中的应用
1 | <keep-alive :include="whiteList" :exclude="blackList" :max="amount"> |
vue 中完整示例
1 | <keep-alive> |
参考:
keep-alive 官网
mixin
mixin
(混入), 它提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
使用场景: 不同组件中经常会用到一些相同或相似的代码,这些代码的功能相对独立。可以通过mixin 将相同或相似的代码提出来。
缺点:
变量来源不明确
多 mixin 可能会造成命名冲突(解决方式:Vue 3的组合API)
mixin 和组件出现多对多的关系,使项目复杂度变高。
在Vue 中,修饰符处理了许多 DOM 事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理。Vue中修饰符分为以下几种:
Vue 中的修饰符有哪些
在Vue 中,修饰符处理了许多 DOM 事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理。Vue中修饰符分为以下几种:
- 表单修饰符
lazy
填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change
事件之后再进行信息同步。
number
自动将用户输入值转化为数值类型,但如果这个值无法被parseFloat
解析,则会返回原来的值。
trim
自动过滤用户输入的首尾空格,而中间的空格不会被过滤。 - 事件修饰符
stop
阻止了事件冒泡,相当于调用了event.stopPropagation
方法。
prevent
阻止了事件的默认行为,相当于调用了event.preventDefault
方法。
self
只当在event.target
是当前元素自身时触发处理函数。
once
绑定了事件以后只能触发一次,第二次就不会触发。
capture
使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理。
passive
告诉浏览器你不想阻止事件的默认行为。
native
让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用v-on
只会监听自定义事件。 - 鼠标按键修饰符
left
左键点击。
right
右键点击。
middle
中键点击。 - 键值修饰符
键盘修饰符是用来修饰键盘事件(onkeyup
,onkeydown
)的,有如下:
keyCode
存在很多,但vue为我们提供了别名,分为以下两种:
普通键(enter、tab、delete、space、esc、up…)
系统修饰键(ctrl、alt、meta、shift…)
如何实现 v-model,双向绑定
- 概念:
Vue 中双向绑定是一个指令v-model
,可以绑定一个响应式数据到视图,同时视图的变化能改变该值。v-model
是语法糖,默认情况下相当于:value
和@input
,使用v-model
可以减少大量繁琐的事件处理代码,提高开发效率。 - 使用:
通常在表单项上使用v-model
,还可以在自定义组件上使用,表示某个值的输入和输出控制。 - 原理:
v-model
是一个指令,双向绑定实际上是Vue 的编译器完成的,通过输出包含v-model
模版的组件渲染函数,实际上还是value
属性的绑定及input
事件监听,事件回调函数中会做相应变量的更新操作。
Vue Router中的常用路由模式和原理?
- hash 模式:
location.hash
的值就是url中 # 后面的东西。它的特点在于:hash虽然出现url中,但不会被包含在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面。
可以为hash的改变添加监听事件window.addEventListener("hashchange", funcRef, false)
,每一次改变hash (window.location.hash)
,都会在浏览器的访问历史中增加一个记录,利用hash的以上特点,就可以实现前端路由更新视图但不重新请求页面的功能了。
特点:兼容性好但是不美观 - history 模式:
利用 HTML5 History Interface 中新增的pushState()
和replaceState()
方法。
这两个方法应用于浏览器的历史记录栈,在当前已有的back
、forward
、go
的基础上,他们提供了对历史记录进行修改的功能。
这两个方法有个共同点:当调用他们修改浏览器历史记录栈后,虽然当前url改变了,但浏览器不会刷新页面,这就为单页面应用前端路由“更新视图但不重新请求页面”提供了基础
特点:虽然美观,但是刷新会出现 404 需要后端进行配置。
动态路由?
很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由。例如,我们有一个 User组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用动态路径参数(dynamic segment)来达到这个效果:{path: '/user/:id', compenent: User}
,其中:id就是动态路径参数。
Vuex的理解及使用场景
Vuex 是一个专为 Vue 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。
- Vuex 的状态存储是响应式的;当 Vue 组件从 store 中读取状态的时候,
若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新 2. 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation, 这样使得我们可以方便地跟踪每一个状态的变化 Vuex主要包括以下几个核心模块:
State
:定义了应用的状态数据Getter
:在 store 中定义“getter”(可以认为是 store 的计算属性), getter 的返回值会根据它的依赖被缓存起来, 且只有当它的依赖值发生了改变才会被重新计算Mutation
:是唯一更改 store 中状态的方法,且必须是同步函数Action
:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作Module
:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中
页面刷新后Vuex 状态丢失怎么解决?
Vuex 只是在内存中保存状态,刷新后就会丢失,如果要持久化就需要保存起来。
localStorage
就很合适,提交mutation
的时候同时存入localStorage
,在store
中把值取出来作为state
的初始值即可。- 也可以使用第三方插件,推荐使用
vuex-persist
插件,它是为Vuex
持久化储存而生的一个插件,不需要你手动存取storage
,而是直接将状态保存至cookie
或者localStorage
中。
了解哪些 Vue 的性能优化方法?
- 路由懒加载。有效拆分应用大小,访问时才异步加载。
- keep-alive缓存页面。避免重复创建组件实例,且能保留缓存组件状态。
- v-for遍历避免同时使用v-if。实际上在 Vue 3 中已经是一个错误用法了。
- 长列表性能优化,可采用虚拟列表。
- v-once。不再变化的数据使用v-once。
- 事件销毁。组件销毁后把全局变量和定时器销毁。
- 图片懒加载。
- 第三方插件按需引入。
- 子组件分割。较重的状态组件适合拆分。
- 服务端渲染。