面试题1:Vue 生命周期有哪些?
-
创建阶段
beforeCreate
created
-
挂载阶段
beforeMount
mounted
-
更新阶段
beforeUpdate
updated
-
销毁阶段
beforeDestroy
destroyed
生命周期的执行顺序是不会改变的,底层源代码已经写死了,想要改变除非去改源码
拓展:
- 加入
<keep-alive></keep-alive>
后会多两个钩子函数activated
==> 组件创建时执行deactivated
==> 组件销毁前执行
- 这两个钩子函数会在
vue
自带的八个钩子函数
执行完成之后执行 created
之后才会有$data
mounted
之后才会有$el
面试题2:第一次进入组件或者页面,会执行哪些生命周期?
没有子组件:
beforeCreate
created
beforeMount
mounted
有子组件:
父:
beforeCreate
created
beforeMount
子:
beforeCreate
created
beforeMount
mounted
- ...
父:
mounted
都是
父组件
的前三个
,加上所有子组件
的前四个
,最后是父组件的mounted
拓展:
- 加入
<keep-alive></keep-alive>
时第一次进入组件时会执行:beforeCreate
created
beforeMount
mounted
activated
- 加入
<keep-alive></keep-alive>
时第2次往后进入组件时只会执行:activated
面试题3:谈谈你对keep-alive的了解
题1和2中有提到过keep-alive
的一些基本的东西,面试的时候也可以提一嘴,接下来让我们看看一些其他关于keep-alive
的
keep-alive
是vue系统自带的一个组件,它的功能
是用来缓存组件
的,能缓存组件说明不用重复的去请求数据等等,那么它就是用来提升性能
的
但是有些时候某些页面不需要缓存,或者某些时候不应该用缓存的数据,这时候该怎么解决呢?
- 页面不需要缓存
- 使用条件缓存,下面的栗子是在路由元信息中配置了keepAlive选项
<!-- 条件缓存 -->
<router-view #default="{ Component, route }">
<keep-alive>
<component :is="Component" :key="route.name" v-if="!route.meta.keepAlive" />
</keep-alive>
<component :is="Component" :key="route.name" v-if="route.meta.keepAlive" />
</router-view>
- 需要更新缓存
- 加入
keep-alive
之后进入组件会执行activated
钩子函数,可以在里面通过判断一些信息来决定是否去更新数据
- 加入
面试题4:v-if和v-show区别
两者都是控制节点的隐藏和显示的
区别1:
v-if
为true
时创建 DOM 节点,为false
时删除节点,初次渲染时为false
直接不创建节点
区别2:
v-show
是通过给节点添加display
属性的none
来控制节点的显示与隐藏,初次渲染时为false也会创建节点
看完本质我们就可以归纳以下使用场景:当页面中需要频繁的控制节点的显示与隐藏时应当使用
v-show
,当某一节点只是在某些条件下才会显示与隐藏的应当用v-if
,例如用户未登录时提示用户登录的节点,登录的时候不显示该提示节点,这样的场景应该用v-if
,因为没人会闲的去反复登录和登出吧
额,有些题可能会问这两个的性能谁更好,这种就分情况了,只有一两次的操作肯定是
v-if
更优,反复多次肯定是v-show
更优。因为v-if
直接操作了DOM
面试题5:v-if和v-for优先级
这些优先级的题全看尤大大源代码是怎么写的,25题也是一样的
- vue2
v-for > v-if
- vue3
v-if > v-for
面试题6:Vue有哪些常用的修饰符
这个就有很多了,就随便列几个就好了
stop
:阻止事件的冒泡,相当于调用了event.preventPropagation
方法prevent
:阻止事件的默认行为,相当于调用event.preventDefault
方法self
:只当在event.target
是当前元素自身时触发处理函数once
:绑定了事件以后只能触发一次,第二次就不会触发
面试题7:什么是具名插槽和作用域插槽
- 具名插槽:简单来说就是有名字的插槽,在使用时要和 name 匹配插槽才会生效,具体就不演示了,都背面试题了这个算简单的了
- 作用域插槽:简单来说就是传值(父传子嘛),看代码:
在父组件中定义好arr
数据,然后传入slot
中
子组件中解构出arr
然后正常使用就可以了
面试题8:Vue组件有哪些通信方式
一、父传后代
1. 父传子
这个就比较简单了,看代码吧
父组件定义好data数据并传给子组件
<Child :msg="msg" :data=data></Child>
子组件接收
//写法1:
props:['msg']
//写法2:
props:{
msg:{
type:String,
default:'不传则使用默认值'
}
}
2. 后代直接使用父的数据
我们的子组件是可以通过
this.$parent.xxx
来拿到父组件的数据的,孙子辈组件可以通过this.$parent.$parent.xxx
来拿到爷爷辈组件数据的,以此类推,但是一般很少使用这种方式,当作拓展吧,多说一嘴,就是通过this.$parent.xxx
的方式我们是可以直接修改父级的数据的,怎么说呢,因为这种方式就已经不是传值了,而是你知道那里有你直接拿的。
3. 父传孙子辈
方式一
:父传子,子再传孙... ... 实现和上面的一样就不多说了
方式二
:依赖注入provide/inject
父辈
provide(){
return{
val:"依赖注入"
}
}
子代
inject:["val"]
这玩意虽然好用,可以直接透传到祖孙十八代,谁想用谁就能用。但是也有弊端,项目庞大的话你很难知道到底是用的哪一辈传下来的,你得严查祖上十八代才行
二、后代传父
1. 子传父
这个比较麻烦一点,因为父传子是单项数据流,不能修改,会报错。但是可以卡个bug,如果是数组或者对象,其实是可以改里面的值的。
父组件定义自定义事件的处理函数并传给子组件
<Child @zidingyi='handeleZidingyi'></Child>
// zidingyi : 自定义函数的名称(随意写,但要见名知意)
// handeleZidingyi : 自定义函数的处理函数 (在父组件中写)
子组件使用
this.$emit('zidingyi',要传的参数)
// zidingyi : 必须和父传过来的函数名(函数名字!!)一致,表示要调用的函数
// zidingyi : 是zidingyi而不是handeleZidingyi
2. 父直接使用子的数据
既然子可以通过
$parent.xxx
直接访问到父的数据,那么相对应的父就可以直接通过$children
来获取数据,注意的点是:$children
方式拿到的是一个数组,因为亲爹只有一个(干爹另说哈)但亲儿子就可以有很多个了。而且是不能在页面中直接$children[0].xxx
这样使用是会报错的,要先在某个生命周期中拿到之后赋值给一个新的参数,然后在页面中使用新的参数。
像上面这样的方式还有
ref
的方式,给子组件绑了一个ref=child
,然后父就可以直接通过$refs.child.xxx
来获取,这种方式也是可以直接修改子的数据的
三、兄弟组件之间传值
方式一
:子传给父,再由父传给自己的兄弟。你知道有这个东西就行了,不推荐写这种代码!!!
方式二
:bus
传值方式,也叫中央事件总线
:通过创建一个新的vue
实例对象,专门统一注册事件,供所有组件共同操作,达到所有组件随意隔代传值的效果。其实这时候也不仅仅能在兄弟组件之间了,只要是在定义了bus之后,哪个组件都能用了。
方式二有两种实现方法
写法一:
在main.js文件中定义
Vue.prototype.$bus=new Vue({
data:{
arr:[] //用来保存事件名
},
methods:{
//绑定事件
on(eventname,callback){
// 判断事件名是否重复
if(this.arr.includes(eventname)){
throw "eventname events already regist!!"
}else{
this.arr.push(eventname)
this.$on(eventname,callback)
}
},
// 触发事件,传递数据
emit(eventname,...arg){
this.$emit(eventname,...arg)
},
// 解绑
off(eventname,callback){
this.$off(eventname,callback)
}
}
})
使用
this.$bus.on('定义名称' val=>{}) //绑定事件
this.$bus.emit('定义名称',值) //触发事件
this.$bus.off('定义名称') //解绑
写法二:
推荐新建 utils 文件夹,在里面创建 bus.js 文件
// bus.js
import Vue from 'vue'
exprot default new Vue();
使用(伪代码)
====>1. 兄弟都需要引入 import bus form '@/utils/bus'
====>2. A兄弟传
// $emit ===>自定义事件
bus.$emit('zidingyi',参数)
====>3. B兄弟接
// $on ===>接收自定义事件
bus.$on('zidingyi',val =>{
// val就是传过来的数据,赋值给当前组件就可以了
})
面试题9:为什么data选项是一个函数
- 如果
data
是一个函数的话,这样每复用一次组件,就会返回一份新的data
(给每个组件实例创建一个私有的数据空间(闭包),让各个组件实例维护各自的数据) Object
是引用数据类型,里面保存的是内存地址
,单纯的写成对象形式,就使得所有组件实例共用
了一份data
,就会造成一个变了全都会变的结果。
多解释一点吧:就是说每一个
组件
都是一个实例
,并且是可以复用
的,复用的时候数据应该是不一样
的,所以在复用组件的时候应该让其的数据是互不影响
的,不能说在A页面
使用这个组件的时候修改了一些数据,而B页面
使用这个组件的数据也被改变了,这不合适。所以就用闭包函数
的形式将数据返回,每一个组件复用的时候都会产生新的内存空间
,组件数据就互相隔离
了。
tip:
在vue2
中根实例是可以用对象的形式的,因为你不会在一个项目中复用根组件,根组件只能存在一个,但子组件必须是函数形式
面试题10:ref是什么?
用来获取
DOM
或者组件实例
的,vue3
中被重新发扬光大了,例如用来实现响应式数据
面试题11:nextTick是什么?
异步获取DOM
,假如说我们修改了data
中的值之后,直接获取该值相关的DOM
时,拿到的是修改之前
的DOM
,因为js
中修改DOM
是异步
的,而直接获取是同步
的。在vue中为了节约性能,会将所有对于DOM
的操作先收集起来,最后再统一
进行一次
修改,这更明显是需要时间的,即异步。那么我们如何获取到修改之后的DOM呢?根据上面的解释,其实有很多方法可以,只要是异步的就行了。比如setTimeout
等等,但是我们这么写并不优雅,所以vue给我们提供了$nextTick
这个API
来让我们获取数据更新后的DOM。
其实vue底层源码也是这么干的,也是用了js
中那些异步的方式(promise、setTimeout... ...)
只不过是用了优雅降级的方式,先判断浏览器支不支持 promise
,不支持再降级,最后如果都不支持,那就上setTimeout
面试题12:路由导航守卫有哪些?
全局守卫
router.beforeEach((to, form, next) => {
if (to.name === "used") {
loginedIn.value === 0 ? router.push('/login') : next()
} else {
next()
}
})
独享守卫
{
path: '/choice',
name: 'choice',
component: () => import('../views/choicecar/choicecar.vue'),
beforeEnter: (to, form, next) => {
if (loginedIn.value === 0) {
router.push('login')
} else {
next();
}
}
}
组件内守卫
//通过路由规则,进入该组件时被调用
beforeRouteEnter(to,from,next) {
if(toString.meta.isAuth){
if(localStorage.getTime('school')==='qinghuadaxue'){
next()
}else{
alert('学校名不对,无权限查看!')
}
}
},
//通过路由规则,离开该组件时被调用
beforeRouteLeave(to,from,next) {
next()
}
面试题13:Vue有哪些路由模式,区别如何?
路由有两种模式:
哈希模式(hash) 和 历史模式(history)
区别1:
hash
模式url
中会显示#
,而history
模式没有,颜值较高
区别2:
如果找不到当前页面:
history
模式会给后端发送请求,hash
模式不会,history
模式就会造成后端一定压力,需要后端重定向回前端或者前端配置404页面也能解决一定的问题。
区别3:
一些关于打包之后前端自测的问题:
history
模式下打包之后默认是看不到内容的,默认路径下是找不到页面的(也就是说需要做一些配置),而hash
模式是可以看到内容的
面试题14:什么是路由懒加载
xxx懒加载
,可以简单的理解成看到
才加载,例如图片懒加载,就是当 图片标签 进入到可视窗口时能被用户看到才加载。但是路由用户不会看到,但是每个路由都至少对应着一个页面,页面用户是可以看到的,那么自然而然的就是用户看到页面了才去加载该页面的资源。
意义
路由懒加载是性能优化的方式之一,极大的提高了首屏加载的速度,假设我们的应用有上百个页面组成,而用户在第一次登录之前,我们最多应该加载登录页和首页,其它剩下的页面等用户跳转了再加载,听着就感觉很爽是吧
面试题15:什么是动态路由
形如
detail/:xxx
,因为xxx
是不固定的,所以叫动态路由。例如经典的商品跳详情
,商品成千上万,不可能为每一条商品数据都写一个详情页,详情页只有一个,根据不同的商品展示不同的介绍。这时候我们可以根据能标识商品的唯一值(商品id...),用动态路由方式detail/:id
,把商品id携带到详情页,详情页根据这个id去取回数据并展示即可。
面试题17:scoped原理
作用:
让样式在本组件中生效,不影响其他组件
原理:
给节点样式新增自定义属性,然后css根据属性选择器添加样式
上面的代码执行之后就只有
11111
的div
背景是红色
拓展:
- Vue中如何做样式穿透
scss
:父元素/deep/
子元素 { 样式 }
补:之前这么写是可以的,换了sass高版本之后换了一种写法,如下:
scss
:父元素::v-deep
子元素 { 样式 }
面试题18:讲一下MVVM
面试题19:双向绑定原理
在vue2.x中,双向数据绑定是通过数据劫持结合发布订阅模式的方式来实现的,也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之变化。
- vue2的核心:Object.defineProperty()方法
- vue3的核心:ES6的语法Proxy
更详细的一些东西就看基本功扎不扎实了,对发布订阅模式、观察者模式有没有很好的理解和掌握了。因为数据劫持很容易理解和实现,但是数据和视图同步的去改变又是通过怎样的形式去实现呢?一个数据在多个地方使用到又如何去实现同步呢?
-
首先要对数据(data)进行劫持监听,所以需要设置一个监听器Observer,用来监听所有的属性。
-
如果属性发生变化,需要通知订阅者Watcher,看是否需要更新。因为订阅者有多个,所以需要一个消息订阅器(发布者)Dep(订阅者集合的管理数组)来专门收集这些订阅者,在Observer和Watcher之间进行统一管理。
-
还需要一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化为一个订阅者Watcher,并替换模板数据或绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
总结为以下三个步骤:
- 实现一个监听器Observer,用来劫持并监听所有属性,如果发生变化,就通知订阅者。
- 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
- 实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并据此初始化视图和订阅器Watcher。
面试题20:什么是虚拟DOM
简单来说就是对真实DOM的JS描述。就比如对于一个人的描述:张三男身长两米八体重两顿三吧啦吧啦一大堆,然后你就能根据这个描述想象出真实的张三了。虚拟DOM也一样,里面有节点名啊属性啊样式啊子节点啊等等,到时候就而已根据虚拟DOM的描述渲染出一个真实的DOM。
拓展
为什么要有虚拟DOM:因为对DOM的操作是极其浪费性能的,而使用虚拟DOM再加上diff算法,可以很快找出新旧节点的不同,然后用最小的代价去补丁式的更新界面。可能你不会觉得:那生成虚拟DOM然后又计算这那的,然后才更新真实DOM,不也是浪费时间性能吗?我只能说小伙子年轻了哈,浏览器对于数据的操作那简直是手拿把掐,啪一下,很快啊,就做完了,来不及闪的那种。所以采用虚拟DOM是对性能的提高。
面试题21:diff算法
计算新旧节点(VNode)的差异,
diff
整体策略为:深度优先,同层比较
- 比较只会在同层级进行, 不会跨层级比较
- 在diff比较的过程中,循环从两边向中间比较
具体的计算和比较这里就不展开了
面试题23:介绍一下SPA以及SPA有什么缺点?
SPA 即
单页面应用
, 与之对应的就是 MPA 即多页面应用
SPA
只有一个主界面,切换页面时主界面不切换,只是里面的内容动态加载替换而已。
MPA
有多个主界面,或者说都是独立的主页面,切换页面时就是主界面的切换
单页应用优缺点
优点:
- 具有桌面应用的即时性、网站的可移植性和可访问性
- 用户体验好、快,内容的改变不需要重新加载整个页面
- 良好的前后端分离,分工更明确
缺点:
- 不利于搜索引擎的抓取
- 首次渲染速度相对较慢
面试题24:Vue双向绑定和单向绑定
面试题25:props和data优先级谁高?
这些优先级的题全看尤大大源代码是怎么写的
props > data > methods > computed > wacth
面试题26:对Props单向数据流的理解
-
所有的
prop
都使得其父子prop
之间形成了一个单向下行
绑定:父级prop
的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。 -
每次父级组件发生更新时,子组件中所有的
prop
都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变prop
。如果你非要硬着来,Vue 会在浏览器的控制台免费送你一个警告。 -
但是!对于一些复杂数据类型你是可以修改里面的值的,只要你不对地址进行修改,里面的东西你修改了它也检测不到。这也是vue2的弊端之一,比如你用下标的方式修改了数组中的某一项,vue2是检测不到的。其实想实现对于数组里面每一项的劫持vue2也是可以实现的,但是尤大大出于对性能的考虑,
Object.defineProperty()
对于数组的劫持必须要递归,如果数组嵌套的太深,会极大的浪费性能,而且也很少会有人通过下标去修改,例如arr[100] = 'xxx'
,所以vue2底层只对那些能够修改原始
数组的几个API
进行重写了而已。
面试题27:computed、methods、watch有什么区别?
computed
和 methods
这两个最大的区别就是computed
有缓存,methods
没有。我觉得这俩货没啥好比较的。
computed
和 watch
computed
:一对多watch
:多对一
举个栗子吧
假如有个需求:在
data
中定义了val1...val100
,这么100
个数据,默认都等于1
,它们分别双向绑定在页面中的100
个input
标签中,现在要求在页面中同时显示val1+...+val100
的和
,同时改变任何一个val
的值,总和
都要跟着变,那该如何做呢?
其实用watch和computed都可以,但是从代码中我们可以看出一些区别:
// watch写法
watch: {
val1: {
handler(newVal, oldVal) {
this.total = newVal + this.val2+...+this.val100
},
val2: {
handler(newVal, oldVal) {
this.total = this.val1+newVal + ...+this.val100
}
...
val100: {
handler(newVal, oldVal) {
this.total = this.val1+...+this.val99+newVal
}
}
// computed写法
computed: {
total: function () {
return this.val1+...+this.val100
}
}
从上面的代码也可以看出来
一对一
和多对一
的区别了,watch
要监听100
个val
,而computed
只需要计算1
个tatol
其他的区别还可以说
computed
有缓存而watch
没有啊等等
面试题28:Vuex的理解
集中管理状态的中央仓库。可以理解成一个商店,张三李四王麻子,都能去商店买自己想要的东西。vue中每一个组件都能去vuex中拿到想要的数据,因为某些数据在很多地方可能都要用到,比如登录之后的一些用户信息、登录状态等,很多页面都要用到,如果用普通的传值方式来开发,那估计程序员的头发就更少了,能累死个人,所以把它放到vuex中来统一管理这些数据,谁想用谁就来拿。
面试题29:Vuex是单向数据流还是双向数据流?
先说结论:Vuex是单向数据流。在上一个问题中我们对vuex的理解为一个商店。张三李四王麻子等各色的人都能去商店买东西,但是他们只能买,不能改价格。就比如说你看上一台
iPone14 promax 远峰蓝 1TB
,你嫌太贵把价格改成1块3
,你这样去付钱老板估计给你一块砖
。所以 Vuex是单向数据流 ,想改只能通过内部的方法去修改。
面试题30:Vue有哪些逻辑复用方式
- mixin
- hooks
- 自定义指令
- 插件
面试题31:Vue怎么优化性能
面试题32:什么是自定义hook
面试题33:什么是自定义指令
面试题34:你怎么理解Vue插件的
面试题35:Vue怎么缓存当前组件?缓存后想更新怎么办?
这题就是
<keep-alive>
的另一种问法,缓存组件就是用<keep-alive>
,想更新就在active
生命周期中做判断来手动更新。