前言

本文整理了高频vue面试考点,并将难度分为简单中等困难三个难度。

简单

MVC和MVVM的区别

MVC

MVC的全称是 Model View Controller 是模型,视图,控制器的缩写,是一种软件设计规范。

  • Model模型:是应用程序中用于处理程序数据逻辑的部分。通常模型对象负责从数据库里面存取数据
  • View视图:是应用程序中处理数据显示的部分。通常视图是依据模型数据渲染的。
  • Controller:是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。

MVC的思想:一句话就是Controller负责将Model的数据取出来用View显示出来,换句话说就是在Controller里面把Model的值赋给View

MVVM

MVVM新增了VM类
ViewModel层:做了两件事情完成了双向绑定。

  1. 将Model模型转化为了View视图。即将后端的数据转化为所看到的页面。使用的方式是数据绑定。
  2. 将View视图转化为了Model模型。即将页面转化为了后端的数据。使用的方式是DOM事件监听。

MVVM和MVC最大的区别就是:他实现了View和Model的自动同步,也就是当Model的属性改变的时候,我们不需要手动去操作DOM元素,来改变View的显示,而是改变了属性之后View中的元素也会随之改变。

注意 Vue并没有完全遵循MVVM的思想,因为MVVM的思想是不允许Model和View直接通信 但是Vue提供了$refs这个属性让我们可以通信操作dom

为什么data是一个函数

数据以函数返回值的形式定义,每复用一次组件就返回一份全新的data,类似给组件的实例创建一个私有的数据空间,这样实例之间互不影响。如果直接写成对象的形式,实例之间就共享了同一份data数据。

Vue组件通讯有那些方式?

  1. props父子间向子组件传递通过props,
  2. 父通过$refs获取子组件信息用$on挂载事件回调函数,子通过emit触发事件。
  3. $parent,$children获取当前的父组件和子组件详细的方法并调用。
  4. 全局事件总线(mainjs的beforeCreate里面挂载到vue实例上)(vue3取消 使用mitt插件)。
  5. localStorage
  6. Vuex
  7. provide和inject
  8. pubsubjs

Vue的生命周期方法有那些?在那一步发送请求

beforeCreate

在什么阶段被使用:实例初始化之前,数据检测和数据代理之前使用

不能做什么?不能访问data methods computed watch中的数据和方法

具体场景:

  1. 页面的拦截,看情况判断能否通行,如果不能省掉创建vue实例
  2. 页面的重定向,不正确进入重定向到别的页面
  3. loading 在mounted之后销毁

created

在什么阶段被使用:初始化之后,数据代理和数据检测开始

可以访问到data methods computed watch中的数据和方法

不能做什么?因为实例还没有被挂载 所以不能获取dom节点(非要获取nexttick)

具体场景:

  1. 数据的初始化
  2. 数据交互 axios(不需要用到dom)

beforeMount

在什么阶段被使用:在挂载开始之前被调用

不能做什么?此时页面呈现的是未经vue编译的dom结构,所有对dom的操作最后都不奏效

mounted

在什么阶段被使用:真实DOM挂载完毕,数据完成双向绑定,可以访问到DOM节点

具体场景:

  1. 数据交互(涉及到dom)

beforeUpdate

在什么阶段使用:数据更新的时候调用,发生在虚拟DOM重新渲染和打补丁之前。

可以做什么:可以进一步更改状态不会触发重渲染

updated

在什么阶段使用:更新已经完成

不能做什么:避免对数据进行修改,否则造成无限循环。

beforeDestroy

在什么阶段使用:实例销毁之前使用

具体场景:清除定时器,取消事件。

destroyed

在什么阶段使用:实例销毁后

activated

keep-alive专属,组件被激活的时候调用。

deactivated

keep-alive专属,组件被销毁的时候调用。

异步请求在哪一步发起?

可以在created,beforeMount,mounted中进行,因为在这三个钩子中data已经创建,可以将服务器返回的数据进行赋值。

  • 对DOM有需求的话一般是放在mouted中,此时DOM已经加载完毕
  • 对DOM没有需求可以放created,但是只是在created发出了请求,真正的数据还是在mouted阶段才获取。

v-if和v-show的区别

v-if是真正的条件渲染,在编译过程中会转化成三元表达式,条件不满足不渲染此节点。
v-show会被编译成指令,如果条件不满足对应的节点隐藏,相当于display:none

使用场景
v-if适用于运行的时候很少改变条件,不需要频繁切换的场景。因为会很影响性能。
v-show适用于频繁切换的场景。

拓展

display:none,opacity:0,visibility:hidden之间的区别?

三者都是隐藏。

  1. 是否占据空间
    关键在于display隐藏后不占据位置。visibility和opacity还是占据元素位置。
  2. 子元素是否继承。
    display的父元素都不存在了,自然子元素不会被继承。
    visibility会被子元素继承,可以使用visible来显示子元素
    opacity会被子元素继承,但不可以设置1来重新显示子元素。
  3. 事件绑定
    display的元素不存在了,自然无法使用他绑定的元素
    visibility不会触发他上面绑定的事件
    opacity会触发。
  4. 过度动画
    想要使一个元素慢慢消失,只能用opacity。

说说Vue的内置指令。

v-once 定义它的元素只渲染一次包括它的所有子节点,首次渲染完之后将视为静态内容。
v-clock 该指令保存在元素上直到关联实例结束编译,解决初始化慢导致页面闪动的最佳实践。
v-bind 绑定属性,动态更改HTML元素上的属性可以简写成:
v-on 监听dom事件 可以简写成@
v-html 和innerHTML一样 注意防止xss攻击
v-text 更新元素的textContent
v-model 变为value和input的语法糖
v-if/v-else/v-else-if 配合template使用。
v-show 使用指令来实现隐藏和显示
v-for

  • 循环显示渲染列表
  • 优先级比v-if高,最好不要一起使用—用computed
  • 注意增加唯一的key—不要使用index作为key
    v-pre 跳过这个元素以及它的子元素的编译,加快编译速度。

怎么理解Vue的单向数据流

数据总是从父组件流到子组件,子组件理应没有权限修改父组件的数据,因为这样会导致数据流错乱。
当然如果需要修改父组件props穿过来的数据。最好定义一个data接收这个值再改变。

computed和watch的区别和运用的场景

computed和watch都是对属性的变化做出改变的一个函数了。区别在于computed是计算属性,更偏向于计算的结果这个状态,而watch是监视属性,可以看属性的old和new值,偏向于知道这个过程中发生了什么。
场景:
computed用在模板渲染比较多,某个值有依赖其他的响应式对象甚至是计算属性计算而来。
watch用在观测某个值的变化完成复杂的逻辑功能。

computed不能计算异步 watch可以监视异步

v-if和v-for为什么不建议一起使用。

首先是v-for的优先级高于v-if 会先解析v-for在去解析v-if 所以每次判断都会遍历整个渲染的数据再去找里面不需要的内容。改进的方法是使用computed 不用if。

中等

Vue响应式数据的原理

整体思路是数据劫持+观测者模式

在对象内部使用Object.defineProperty将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应的属性的时候,每个属性都有自己的dep属性,存放她所依赖的watcher,当属性变化后会通知watcher派发更新。

手写

Vue如何检测数组变化

数组考虑性能原因没有使用defineProperty对数组每一项进行拦截,而是选择对7种数组方法进行重写(push,shift,pop,splice,unshift,sort,reverse)

所以在vue种修改数组的索引和长度是无法检测到的。需要通过以上七种变异方法触发数组对应的watcher才能更新。

换言之,如果使用了其他方法操作vue中的数组,都是不安全的。

vue3.0用过吗 了解多少

  • 响应式原理的改变。使用了Proxy替代了Object.defineProperty
  • 使用了Composition API,组件的入口变成了setup
  • 生命周期函数的变化 setup的集成。
  • 性能上面的提升

Vue3和2的响应式原理区别

我们知道vue2种的响应式不能很好的解决数组的问题,修改数组索引和长度无法检测,且对象删除和增加的操作也无法检测到。

vue3就利用了Proxy针对这几项进行了改变。可以直接监听对象和数组的变化。并且有多达13种拦截方法。

Vue的父子组件生命周期钩子执行顺序

  • 加载过程 很容易理解 因为父组件需要先有雏形才能放子组件,可以理解为进行到beforeMount才能放子组件
    父beforeCreate
    父created
    父beforeMount
    子beforeCreate
    子created
    子beforeMount
    子mounted
    父mounted
    注意在这个过程中是子先mounted

  • 子组件更新过程
    父beforeUpdate
    子beforeUpdate
    子updated
    父updated

  • 父组件更新过程
    父beforeUpdate
    父updated

  • 销毁过程
    父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

虚拟DOM是什么 有什么优缺点

我们知道,在浏览器中一直操作真实DOM是很浪费性能的,这就是虚拟DOM产生的原因。vue2的virtual DOM本质就是用一个原生的js对象去描述一个DOM节点,是对真实DOM的一层抽象。

优点:

  • 保护性能下限,框架的虚拟DOM需要适配任何上层API可能的操作,他的一些DOM操作必须是能够有配合的,所以他的性能并不是最优的。但是比起我们粗暴的操作DOM性能要好很多。因此框架的虚拟DOM至少能够保证在你不需要性能优化的前提下依然还可以提供不错的性能。
  • 无需手动操作DOM,我们不需要手动操作DOM,只需要写好viewmodel的逻辑,框架就会根据虚拟DOM和数据进行双向绑定,帮助我们更新视图提高我们的开发效率。
  • 跨平台:虚拟dom本质上是js对象,而DOM和平台强相关,相比之下,虚拟dom可以更方便的进行跨平台操作,比如服务器渲染,weex开发等。

缺点:

  • 刚刚也说了,虚拟DOM只能保证我们的性能下限,因此对于性能要求高的引用来说,虚拟DOM无法针对性的进行性能优化;
  • 当首次渲染大量DOM的时候,由于多了一层虚拟DOM的计算,所以会比innerHTML插入慢。

v-model的原理

v-model本质上只是一个语法糖,处理value和input

他在内部为不同的输入元素使用不同的property并抛出不同的事件。

  • text和textarea元素使用value property和input事件
  • checkbox和radio使用checked property和change事件
  • select字段将value作为prop并将change作为事件。

注意对于需要使用输入法的语言,你会发现v-model并不会在输入法组合文字的过程中得到更新。

在普通标签上

1
2
<input v-model="sth" />  //这一行等于下一行
<input v-bind:value="sth" v-on:input="sth = $event.target.value" />

在组件上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<currency-input v-model="price"></currentcy-input>
<!--上行代码是下行的语法糖
<currency-input :value="price" @input="price = arguments[0]"></currency-input>
-->

<!-- 子组件定义 -->
Vue.component('currency-input', {
template: `
<span>
<input
ref="input"
:value="value"
@input="$emit('input', $event.target.value)"
>
</span>
`,
props: ['value'],
})

v-for为什么要加key

如果不使用key,vue会使用一种最大限度减少动态元素并且尝试就地修改复用相同类型元素的算法。key是为Vue中的vnode的唯一标记,通过这个标记,我们的diff操作可以更准确,更迅速。

更准确:因为带key就不是就地复用了,在sameNode函数a的key和b的key对比中可以避免就地复用的情况。所以会更加准确。

更快速:利用key的唯一性生成map对象来获取对应节点,比遍历方法更快。

vue事件绑定原理

原生事件绑定是通过addEventListener绑定给真实元素的,组件事件绑定是通过Vue自定义的$on来实现的。如果要在组件上使用原生事件,需要加.native修饰符,这样就相当于在父组件中把子组件当作普通的HTML标签,然后加上原生事件。

$on,$emit是基于发布订阅模式的,维护一个事件中心,on的时候将事件按名称存在事件中心中,称之为订阅者,然后emit将对应的事件进行发布,去执行事件中心里对应的的监听器。

手写发布订阅

vue-router路由钩子函数是什么 执行顺序是什么

钩子函数种类有全局守卫,路由守卫,组件守卫

全局守卫3个:
1.router.beforeEach 全局前置守卫
2.router.beforeResolve 全局解析守卫 在beforeRouteEnter调用后调用。
3.router.afterEach 全局后置钩子 进入路由后

路由独享守卫1个:
beforeEnter

组件守卫3个:
1.beforeRouteEnter 进入路由前 不能访问this
2.beforeRouteUpdate 路由复用同一个组件时
3.beforeRouteLeave 离开当前路由时。我们用它来禁止用户离开,比如还未保存草稿,或者在用户离开前,将setInterval销毁,防止离开之后,定时器还在调用。

完整的导航解析过程:

  1. 导航被触发
  2. 在失活的组件里调用beforeRouteLeave守卫
    3,调用全局的beforeEach守卫
  3. 在重用的组件里调用beforeRouteUpdate守卫
    5,在路由配置里调用beforeEnter
  4. 解析异步路由组件
  5. 在被激活的组件里调用beforeRouteEnter
  6. 调用全局的beforeResolve守卫
  7. 导航被确认
  8. 调用全局的afterEach守卫
  9. 触发DOM更新
  10. 调用beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入。

vue-router 动态路由是什么?有什么问题

我们经常需要把某种模式匹配到的路由全部映射到同个组件。例如我们有一个User组件,对于所有ID各不相同的用户,都需要用这个组件来渲染。那么我们可以在vue-router的路由路径中使用动态路由参数来达到这个效果。

1
2
3
4
5
6
import User from 'user.vue';
const router = new VueRouter({
routes:[
{path:"/user/:id",component:User}
]
})

vue-router组件复用导致路由参数失效怎么办?

  1. 通过watch监听路由参数再发请求。
    1
    2
    3
    4
    5
    watch:{
    "$route":function(){
    this.getData(this.$route.params.xxx);
    }
    }
  2. 用key来防止复用
    1
    <router-view :key="$router.fullPath">

Vuex的理解

vuex是专门为vue设计的全局状态管理工具,用于多个组件中数据共享。数据缓存等。但无法持久化,内部的核心原理是创造一个全局实例new vue

主要包括以下几个模块:

  • State 定义了应用状态的数据结构,可以在这里设置默认的初始状态
  • Getter 允许组件从Store中获取数据,mapGetters辅助函数仅仅是将store中的getter映射到局部计算属性
  • Mutation 是唯一更改store中状态的方法,且必须是同步函数
  • Action 用于提交mutation 而不是直接变更状态,可以包含任意异步操作。
  • Module 允许将单一的Store拆分为多个store且同时保存在单一的状态树中。

Vuex页面刷新数据丢失怎么解决

数据持久化使用本地存储或者第三方插件vuex-persist

Vuex为什么要分模块而且加命名空间

模块:由于使用单一的状态树,应用的所有状态会集中到一个比较大的对象。当应用变得复杂的时候,store对象就有可能变得相当臃肿。为了解决以上的问题,Vuex允许我们将store分割成模块module。每个模块拥有自己的state,mutation,action和getter

命名空间:默认情况下,模块内部的action,mutation,getter是注册在全局命名空间的—-这样能够使得多个模块能够对同一个mutation或者action做出相应。如果想要模块有更高的封装度和复用性,建议添加namespaced:true的方式使得他成为命名空间。当模块被注册的时候,他的所有getter,action以及mutation都会更具模块注册的路径调整名字。

使用过Vue SSR吗?说一说SSR

SSR也就是服务端渲染,也就是将vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端。

优点:
SSR有更好的SEO,并且是因为按需分配,首屏的加载速度更快

缺点:
开发条件受到限制。服务端渲染只支持beforeCreate和created两个钩子。当我们需要一些外部拓展库的时候需要进行特殊处理,服务端渲染应用程序也需要处于nodejs的运行环境。

且服务端会有更大的负载需求。

Vue使用了哪些设计者模式

  1. 工厂模式-传入参数即可创建实例。
    虚拟dom根据参数的不同返回基础标签的vnode和组件vnode
  2. 单例模式-整个程序有且仅有一个实例
    vue和vue-router的插件注册方法install判断如果系统存在实例就直接返回掉
  3. 发布-订阅模式(vue事件机制)
    4,观察者模式-响应式数据原理
  4. 装饰模式-@装饰器用法
  5. 策略模式-策略模式指对象有某个行为,但在不同的场景下,该行为有不同的实现方案,比如选项的合并策略。

你做过哪些vue的性能优化

以下针对vue

  • 对象层级不要太深,否则性能就会差。
  • 不需要响应式的数据不要放在data中,可以使用Object.freeze冻结数据
  • v-if和v-show区别使用
  • computed和watch区别使用场景。
  • v-for遍历必须加key key最好是id 且避免使用v-if
  • 大数据列表和表格性能优化-虚拟列表/虚拟表格
  • 防止内部泄漏,组件销毁后把全局变量和事件销毁。
  • 图片懒加载
  • 路由懒加载
  • 第三方插件按需引入
  • 适当采用keep-alive缓存组件
  • 防抖节流避免反复请求
  • 服务端渲染SSR or 预渲染

困难

Vue.mixin的使用场景和原理

在日常的开发中,我们经常会遇到在不同的组件中会需要用到一些相同或者类似的代码,这些代码的功能相对独立,可以通过vue的mixin抽离公共的业务逻辑,原理类似于原型链的继承,当组件初始化的时候会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行和并。当组件和混入对象拥有同名的选项的时候,这些选项将以恰当的方式“合并”

手撕mixin

nextTick的使用场景和原理

nextTick中的回调是在下次DOM循环更新结束之后执行的回调。在修改数据之后立刻使用这个方法,获取更新后的DOM。主要的思路是采用微任务优先的方式调用异步方法去执行nextTick包装的方法。

手撕nextTick

keep-alive使用场景

keep-alive是vue内置的一个组件,可以实现组件缓存,当组件切换的时候不会对当前组件进行卸载。

  • 常用的两个属性include和exclude 允许组件有条件的进行缓存
  • 生命周期两个 activated 和 deactivated。用来知道组件是否处于激活状态
  • keep-alive中话运用了LRU算法,选择最近最久未使用的组件进行淘汰

手撕keep-alive

拓展 LRU算法是什么?

LRU 的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件 key 重新插入到 this.keys 的尾部,这样一来,this.keys 中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即 this.keys 中第一个缓存的组件。

Vue.set方法原理

了解了vue响应式原理的同学都知道在两种情况下修改数据vue是不会触发视图更新的。

1是给实例添加新的属性
2是直接更改数组的下标来修改数组的值

而set的原理就是我们给数组和对象本身都增加了ob属性,代表的是observer实例。当给对象新增不存在的属性,首先会把新的属性进行响应式的跟踪,然后触发对象ob的dep收集到的watcher去更新,当修改数组索引的时候我们调用数组本身的splice方法去更新数组。

手撕vueset

Vue.extend原理

他的原理是使用基础vue构造器,创建一个子类,参数是一个包含组件选项的对象。

其实就是一个子类构造器,是vue组件的核心api

实现的思路是使用原型继承的方法返回了vue的子类,并且利用mergeOption把传入组件的options和父类的options进行了合并

手撕vueextend

写过自定义指令吗 原理是啥

指令本质上是装饰器,是vue对html元素的拓展,给html元素增加自定义功能。vue编译dom的时候,会找到指令对象,执行指令相关的方法。

自定义指令有五个生命周期,分别是bind,inserted,update,componentUpdated,unbind

1
2
3
4
5
1. bind 只调用一次,指令第一次绑定到元素的时候调用,在这里可以进行一次性的初始化设置。
2. inserted 被绑定的元素插入父节点的时候调用,只保证父节点存在,但不一定已被插入文档中
3. update 被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的更新。
4. componentUpdated 被绑定元素所在模板完成一次更新的时候调用。
5. unbind:只调用一次,指令与元素解绑的时候调用。

原理

  1. 在生成ast语法树时,遇到指令会给当前元素添加directives属性
  2. 通过genDirectives生成指令代码
  3. 在patch前将指令的钩子提取到cbs中,在patch过程中调用对应的钩子
  4. 当执行指令对应的钩子函数的时候,调用对应指令定义的方法。

Vue修饰符有哪些

事件修饰符
为了方便 下面的指令都省略.

  • stop阻止事件继续传播
  • prevent 阻止标签默认行为
  • capture 使用事件捕获模式,即元素自身触发的事件先在此处理,然后交由内部元素进行处理
  • self 只当在event.target是当前元素的时候才触发处理函数
  • once 事件只会触发一次
  • passive 告诉浏览器不要阻止事件的默认行为。

v-model修饰符

  • lazy 通过这个修饰符,转变为在change事件再同步
  • number 自动将用户输入的值转换为数值类型
  • trim 自动过滤用户输入的首位空格

键盘事件的修饰符

  • enter
  • tab
  • delete(捕获删除 和 退格键)
  • esc
  • space
  • up down left right

系统修饰符

  • ctrl
  • alt
  • shift
  • meta

鼠标按键修饰符

  • left right middle

Vue模板编译原理

vue的编译过程就是将template转化为render函数的过程,分为以下几步:
第一步 模板字符串 转化为elements ASTs—解析器
第二步 对AST进行静态节点标记,主要用来做虚拟DOM的渲染优化—优化器
第三步 使用elements ASTs 生成render函数代码字符串—代码生成器

手撕模板编译

生命周期钩子是如何实现的

vue生命周期钩子的核心是利用发布订阅模式先把用户传入的生命周期钩子订阅好,内部采用数组的方式存储,然后在创建组件实例的过程中会一次执行对应的钩子方法—发布

手撕生命周期钩子实现

函数式组件使用场景和原理

函数式组件与普通组件的区别

  1. 函数式组件需要在声明组件的时候指定functional:true
  2. 不需要实例化,所以没有this,他的this通过render函数的第二个参数context来代替
  3. 没有生命周期钩子函数,不能使用computed和watch
  4. 不能通过emit对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件.
  5. 因为函数式组件是没有实例化的,所以在外部通过ref去引用组件的时候,实际引用的是HTMLElement
  6. 函数式组件的props可以不用显示声明,所以在没有props里面声明的属性都会被隐式的解析为prop。

优点

  1. 由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件
  2. 函数式组件的结构比较简单,代码结构清晰

使用场景:
一个简单的展示组件,作为容器组件的使用,比如router-view就是一个函数式组件

高阶组件—用于接收一个组件作为参数,返回一个被包装的组件

相关代码如下:

1
2
3
4
5
6
7
if (isTrue(Ctor.options.functional)) {
// 带有functional的属性的就是函数式组件
return createFunctionalComponent(Ctor, propsData, data, context, children);
}
const listeners = data.on;
data.on = data.nativeOn;
installComponentHooks(data); // 安装组件相关钩子 (函数式组件没有调用此方法,从而性能高于普通组件)

能说下vue-router中常用的路由模式实现原理吗

hash模式

  1. location.hash的值实际就是URL后面#的东西,他的特点在于hash虽然出现URL中,但不会被包含在HTTP请求中,对后端完全没有影响,所以改变hash不会重新加载页面
  2. 可以为hash的改变添加监听事件
    1
    window.addEventListener("hashchange", funcRef, false);
    3.每改变一次hash都会在浏览器的访问历史中增加一个记录利用hash的以上特点,就可以实现前端路由更新视图但不请求页面的功能了。

特点:兼容性好但是不美观

history模式
利用了HTML5 History Interface 中新增的pushState和replaceState方法

这两个方法应用于浏览器的历史记录站,在当前已有的back,forward,go的基础上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用它们修改的浏览器历史记录栈后,虽然当前的URL改变了,但是浏览器不会刷新页面,这就为单页应用前端路由更新视图但不重新请求页面提供了基础。

特点,虽然美观,但是刷新后出现404需要后端配置

diff算法了解吗