Vue面试题-更新中...

373 阅读16分钟

面试题1:Vue 生命周期有哪些?

  1. 创建阶段 beforeCreate    created

  2. 挂载阶段 beforeMount    mounted

  3. 更新阶段 beforeUpdate    updated

  4. 销毁阶段 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-iftrue 时创建 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

image.png

子组件中解构出arr然后正常使用就可以了

image.png

面试题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根据属性选择器添加样式

image.png

上面的代码执行之后就只有11111div 背景是红色

拓展:

  • Vue中如何做样式穿透

scss:父元素 /deep/ 子元素 { 样式 }

补:之前这么写是可以的,换了sass高版本之后换了一种写法,如下:

scss:父元素 ::v-deep 子元素 { 样式 }

面试题18:讲一下MVVM

面试题19:双向绑定原理

在vue2.x中,双向数据绑定是通过数据劫持结合发布订阅模式的方式来实现的,也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之变化。

  • vue2的核心:Object.defineProperty()方法
  • vue3的核心:ES6的语法Proxy

更详细的一些东西就看基本功扎不扎实了,对发布订阅模式、观察者模式有没有很好的理解和掌握了。因为数据劫持很容易理解和实现,但是数据和视图同步的去改变又是通过怎样的形式去实现呢?一个数据在多个地方使用到又如何去实现同步呢?

image.png

  • 首先要对数据(data)进行劫持监听,所以需要设置一个监听器Observer,用来监听所有的属性。

  • 如果属性发生变化,需要通知订阅者Watcher,看是否需要更新。因为订阅者有多个,所以需要一个消息订阅器(发布者)Dep(订阅者集合的管理数组)来专门收集这些订阅者,在Observer和Watcher之间进行统一管理。

  • 还需要一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化为一个订阅者Watcher,并替换模板数据或绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。

总结为以下三个步骤:

  1. 实现一个监听器Observer,用来劫持并监听所有属性,如果发生变化,就通知订阅者。
  2. 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
  3. 实现一个解析器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只有一个主界面,切换页面时主界面不切换,只是里面的内容动态加载替换而已。

image.png

  • MPA有多个主界面,或者说都是独立的主页面,切换页面时就是主界面的切换

image.png

单页应用优缺点

优点:

  • 具有桌面应用的即时性、网站的可移植性和可访问性
  • 用户体验好、快,内容的改变不需要重新加载整个页面
  • 良好的前后端分离,分工更明确

缺点:

  • 不利于搜索引擎的抓取
  • 首次渲染速度相对较慢

面试题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有什么区别?

computedmethods

这两个最大的区别就是computed有缓存,methods没有。我觉得这俩货没啥好比较的。

computedwatch

  • computed:一对多
  • watch:多对一

举个栗子吧

假如有个需求:在data中定义了val1...val100,这么100个数据,默认都等于1,它们分别双向绑定在页面中的100input标签中,现在要求在页面中同时显示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要监听100val,而computed只需要计算1tatol

其他的区别还可以说computed有缓存而watch没有啊等等

面试题28:Vuex的理解

集中管理状态的中央仓库。可以理解成一个商店,张三李四王麻子,都能去商店买自己想要的东西。vue中每一个组件都能去vuex中拿到想要的数据,因为某些数据在很多地方可能都要用到,比如登录之后的一些用户信息、登录状态等,很多页面都要用到,如果用普通的传值方式来开发,那估计程序员的头发就更少了,能累死个人,所以把它放到vuex中来统一管理这些数据,谁想用谁就来拿。

面试题29:Vuex是单向数据流还是双向数据流?

先说结论:Vuex是单向数据流。在上一个问题中我们对vuex的理解为一个商店。张三李四王麻子等各色的人都能去商店买东西,但是他们只能买,不能改价格。就比如说你看上一台 iPone14 promax 远峰蓝 1TB ,你嫌太贵把价格改成1块3,你这样去付钱老板估计给你一块砖。所以 Vuex是单向数据流 ,想改只能通过内部的方法去修改。

面试题30:Vue有哪些逻辑复用方式

  1. mixin
  2. hooks
  3. 自定义指令
  4. 插件

面试题31:Vue怎么优化性能

面试题32:什么是自定义hook

面试题33:什么是自定义指令

面试题34:你怎么理解Vue插件的

面试题35:Vue怎么缓存当前组件?缓存后想更新怎么办?

这题就是 <keep-alive>的另一种问法,缓存组件就是用 <keep-alive>,想更新就在 active生命周期中做判断来手动更新。

面试题36:Vue3有哪些更新?