前端 Vue 面试题大汇总

142 阅读16分钟

# Vue 生命周期

Vue实例从开始创建、初始化数据、编译模板、挂载DOM-->渲染、更新-->渲染、销毁等一系列过程,称之为Vue生命周期。

作用:生命周期中有多个钩子函数,可在控制整个Vue实例的过程时更容易形成好的处理逻辑。

beforeCreate:完成实例初始化,this指向被创建的实例,data、computed、wtch和methods都不可以访问,数据观测之前(data observre)被调用。

created:实例创建完成,data、computed、watch、methods可被访问,未挂载DOM,可对data进行操作,若操作DOM需要放在nextTick中。

beforeMount:有了el并找到对应的template编译成render函数。

mounted:完成挂载DOM和渲染,可对DOM进行操作和获取DOM节点,可发起后端请求拿到数据。

beforeUpdate:数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前调用。

updated:组件DOM已完成更新,可执行依赖的DOM操作,不要操作数据防止陷入死循环。

beforeDestroy:实例销毁之前调用,可销毁定时器和接触绑定事件等优化操作。

destroyed:组件已被销毁,事件监听器和子实例都会被移除销毁。

第一次页面加载会触发:beforeCreated、created、beforeMount和mounted,并且DOM渲染在mounted中已经完成了。

可用$on('hook: 生命周期')$once('hook: 生命周期')来简化生命周期的注册,如下

this.$on('hook:beforeDestroy',()={
    // 逻辑处理
})
this.$once('hook:beforeDestroy',()={
    // 逻辑处理
})

# Vue 响应式原理

Vue初始化的时候,使用Object.defineProperty重新定义data中所有属性,增加了数据获取(getter)/设置(setter)的拦截功能。在获取/设置时可以增加一些处理逻辑,这个逻辑叫作依赖收集。当页面取到对应属性时会进行依赖收集,如果属性发生变化则会通知收集的依赖进行更新,而负责收集的就是watcher

watcher有很多种,如负责渲染的watcher会在页面渲染的时候对数据进行取值,并把当前watcher先存起来对应到数据上。当更新数据的时候告诉对应的watcher去更新,从而实现了数据响应式

data一般分为两大类:对象类型数组类型

对象类型

Vue初始化的时候,会调用initData方法初始化data,它会拿到当前用户传入的数据。判断如果已经被观测过则不在观测,如果没有则利用new Observer创建一个实例用来观测数据。如果判断是对象非数组的话则调用this.walk方法把数据进行遍历,在内部使用definReactive(用来定义响应式的核心方法)方法重新定义,而重新定义采用的就是Object.defineProperty。如果当前对象的值还是对象,会自动调用递归观测。当用户取值的时候会调用get方法并收集当前的watcher。在set方法里,数据变化会调用notify方法触发数据对应的依赖进行更新。

数组类型

使用函数劫持发方式重写了数组的方法,并进行了原型链重写,使data中的数组指向了自己定义的数组原型方法。这样的话,当调用数组API时可以通知依赖更新。如果数组中包含着引用类型,则会对数组中的引用类型进行再次监控。
也就是当创建了Observe观测实例后,如果数据是数组的话,判断是否支持自己的原型链,如果不支持则调用protoAugment方法使目标指向arrayMethods方法。arrayMethods就是重写的数组方法,包括push、pop、shift、unshift、splice、sort和reverse共七个方法,内部采用函数劫持的方式。在数组调用重写的方法之后,还是会调用原数组方法去更新数组,只不过重写的方法会通知视图更新。如果使用push、unshift和splice等方法新增数据,还会调用observeArray方法对插入的数据进行再次观测。
如果数组中有引用类型,则继续调用observeArray方法循环遍历每一项,继续深度观测。前提是每一项必须是对象类型,否则observe方法会直接return

# Vue 为何采用异步渲染

如果不采用异步更新,则会每次更新数据都会对当前组件进行重新渲染,因此为了性能考虑,Vue在本轮数据更新结束之后,再去异步更新视图。

当数据变化后会调用notify方法通知watcher进行数据更新。而watcher会调用update方法进行更新(这里就是发布订阅模式)。更新时并不是让watcher立即执行,而是放在一个队列里进行过滤,相同的wathcer只存一个,这个队列就是queueWatcher方法。最后在在调用nextTick方法通过flushSchedulerQueue异步清空watcher队列。

# nextTick 实现原理

nextTickf方法主要是使用了宏任务微任务定义了一个异步方法。多次调用nextTick会将方法存入队列中,通过这个方法异步清空当前队列,所以nextTick方法就是异步方法。

作用$nextTick是在下次DOM更新循环结束之后执行的延迟回调,在修改数据之后使用$nextTick可以在回调中获取更新后的DOM。

默认在内部调用nextTick时会传入flushSchedulerQueue方法,存在一个数组里并让他执行。用户有时也会调用nextTick方法,调用时把用户传过来的cb也放在数组里,都是同一个数组callbacks。多次调用nextTick只会执行一次,等到代码都执行完毕后,会调用timerFunc这个异步方法。在方法里会依次进行判断所支持的类型:

1、如果支持Promise则把timerFunc包裹在Promise中并把flushCallBacks放在了then中,相当于异步执行了flushCallBacks函数。而flushCallBacks函数的作用就是让传过来的方法依次执行

2、如果不是IE浏览器且支持原生的MutationObserver,则会首先声明一个变量并创建一个文本节点。接着创建MutationObserver实例并把flushCallBacks函数传入,调用Observer方法去观测每一个节点。如果节点发生变化就会异步执行flushCallbacks方法。

3、如果支持setImmediate则调用setimmediate方法并传入flushCallBacks异步执行。

4、若以上都不支持则就只能调用setTimeout方法传入flushCallBacks

Vue 中 Computed 和 Watch 的区别

默认computedwatch内部都是一个watcher实现的。
computed有缓存,不会先执行,只有当依赖的属性发生变化才会通知视图更新。
watch没有缓存,默认会先执行,只要监听的属性发生变化就会更新视图。

Computed

调用initComputed方法初始化计算属性时,会获取到用户定义的方法并创建一个watcher把用户定义传进去。这个watcher有个标识:lazy=true,默认不会执行用户定义的函数,还有个标识:dirty=true,默认去求值。watcher内部调用defineComputed方法将计算属性定义在实例上,其底层也是用的Object.defineProperty,并且传入了createdComputedGetter方法定义一个计算属性。在用户取值时调用的是createdComputedGetter返回的computedGetter函数:判断当前的watcher.dirty是否为true,如果为true则调用watcher.evaluate方法求值。

在求值时是调用的this.get()方法,而这个方法就是用户传入的方法,执行时会把方法里的属性依次取值。而在取值前调用了pushTarget方法将watcher放到了全局上,即当取值时会进行依赖收集,把当前的计算属性的watcher收集起来。等数据变化时wathcer重新执行,也就是进入到了update方法中。update并没有直接让watcher执行,而是将dirty=true。这样处理的好处就是如果dirty=true就进行求值,否则就返回上次计算后的值,从而实现了缓存机制

Watch

调用initWatch方法初始化watch的时候,内部传入用户定义的方法调用了createWatcher方法。在createWatcher方法中比较核心的就是$watch方法内部调用了new Watcher并传入了expOrFn回调函数。如果expOrFn是个字符串的话,会包装成一个函数并返回这个字符串。这时lazy=false了,直接调用了this.get()方法获取属性的值。同computed在取值前也执行了pushTarget方法将watcher放在了全局上,当用户取值时就收集watcher。因此当属性值发生改变时watcher就会更新。

如果监听的属性值是个对象,则取对象里的值就不会更新了,因为默认只能对属性进行依赖收集,不能对属性值是对象的进行依赖收集。想要不管属性值是否是对象都能求值进行收集依赖,可设置deep=true调用traverse方法进行递归遍历。

# 谈谈 MVVM 模式

Model: 代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。
View: 代表UI 组件,它负责将数据模型转化成UI 展现出来。
ViewModel: 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。

MVVM架构下,ViewModel之间并没有直接的联系,而是通过ViewModel进行交互。ModelViewModel之间的交互是双向自动的,因此View数据的变化会同步到Model中,而Model数据的变化也会立即反应到View上。而开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由MVVM来统一管理。

MVVM 和 MVC区别?

mvcmvvm其实区别并不大,都是一种设计思想。主要就是mvcController演变成mvvm中的viewModelmvvm主要解决了mvc中大量的DOM操作使页面渲染性能降低,加载速度变慢,影响用户体验。和当Model频繁发生变化,开发者需要主动更新到View

# Vue 组件中 data 为什么必须是一个函数?

因为js本身的特性带来的,同一个组件被复用多次,会创建多个实例。这些实例是同一个构造函数。如果data是一个对象的话,那么所有组件都共享了同一个对象。

作用:为保证组件中数据的独立性要求每个组件必须通过data函数返回一个对象作为组件的状态。

Vue通过extend创建子类之后,会调用mergeOptions方法合并父类和子类的选项,选中就包括data。在循环完父类和子类之后调用mergeField函数的中的strat方法去合并data,如果data不是函数而是个对象,则会报错提示data应该是个函数。

# Vue 中的 v-show 和 v-if 是做什么用的?两者有什么区别?

v-if:会在with方法里进行判断,如果条件为true则创建相应的虚拟节点,否则就创建一个空的虚拟节点也就是不会渲染DOM

v-show: 会在with方法里创建了一个指令就是v-show,在运行的时候处理指令,添加了style: display = none / originalDisplay

v-if才是“真正的”条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当的被销毁和重建。v-if也是惰性的,如果在初次渲染时条件为假,则什么也不做,一直到条件第一次变为真时才会渲染条件块。

相比之下v-show就简单的多,不管初始条件是什么,元素总会被渲染,并且只是简单的基于css进行切换。

一般来说,v-if有更高的切换开销,v-show 有更高的初始渲染开销。因此如需要频繁的切换则使用v-show较好,如在运行时条件不大可能改变则使用v-if较好。

# v-model 的实现原理是什么?

通俗讲v-model可以看成是value + input的语法糖,组件的v-model也确实是这样。

在组件初始化的时候, 如果检测到有model属性,就会调用transformModel方法转化model。如果没有prop属性和event属性, 则默认会给组件propvalue属性, 给eventinput 事件。把prop的属性赋给了data.attrs并把值也给了它, 即data.attrs.value = '我们所赋的值'。会给on绑定input事件, 对应的就是callback

如果在组件内自定义modelpropevent, 这样的话组件初始化的时候, 接受属性事件时不再是valueinput了, 而是我们自定义的属性事件
如果是普通的标签, 则在运行时会自动判断标签的类型, 生成不同的属性domProp和事件on。还增加了指令directive, 针对输入框的输入法加上了一些逻辑并做了校验和处理。

# Vue 中父子组件的调用顺序

组件的调用都是先父后子,渲染完成的过程顺序都是先子后父
组件的销毁操作是先父后子,销毁完成的顺序是先子后父

在页面渲染的时候,先执行父组件的beforeCreate -> created -> befroreMount, 当父组件实例化完成的时候会调用rander方法, 判断组件是不是有子组件, 如果有子组件则继续渲染子组件以此类推。当子组件实例化完成时候, 会把子组件的插入方法先存起来放到instertedVNodeQueue队列里, 最后会调用invokeIntertHook方法把当前的队列依次执行。

更新也是一样先父beforeUpdate -> 子beforeUpdate 再到 子 updated -> 父 updated

# Vue 中组件怎么通讯?

  1. 父子通讯: 父 -> 子props, 子 -> 父$on / $emit
    通过eventsMixin方法中的$on方法维护一个事件的数组,然后将函数名传入$emit方法,循环遍历出函数并执行。
  2. 获得父子组件实例的方式:$parent / $children
    在初始化的时候调用initLifecycle方法初始化$parent$children放在实例上
  3. 在父组件中提供数据供子组件/孙子组件注入进来:Provide / Inject
    通过initProvideinitInjections方法分别把providereject放在$options上。在调用reject的时候,调用resolveInject方法遍历,查看父级是否有此属性,有则就直接return并把它定义在自己的实例上。
  4. Ref获得实例的方式调用组件的属性或方法
    ref被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的$refs对象上。用在DOM上就是DOM实例,用在组件上就是组件实例。
  5. Event bus实现跨组件通讯
    实质上还是基于$on$emit,因为每个实例都有$on$emit并且事件的绑定和触发必须在同一个实例,所以一般会专门定义一个实例去用于通信,如Vue.prototype.$bnts = new Vue
  6. Vuex状态管理实现通讯
  7. $attrs$Listeners实现数据和事件的传递,还有v-bind="$prop"

# Vuex是什么? 怎么使用它? 哪种功能场景使用?

Vuex是一个专为Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

Vuex只能使用在Vue上,因为其高度依赖于Vue的双向绑定和插件系统。调用了Vue.mixin在所有组件的beforeCreate生命周期注入了设置this.$store这个对象。

场景有:单页应用中,组件之间的状态、音乐播放、登录状态、加入购物车

state: Vuex使用单一状态树,存放的数据状态,不可以直接修改里面的数据。
mutations: 定义的方法动态修改Vuexstore中的状态或数据。
getters: 类似Vue的计算属性,主要用来过滤一些数据。
actions: 可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view层通过store.dispath来分发action
modules: 项目特别复杂的时候,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。

actions 和 mutations的区别

action主要处理异步操作,mutation必须同步执行,而action就不受这样的限制,既可以处理同步,也可以处理异步的操作,action改变状态是通过提交mutation实现的。

# 说说对 keep-alive 的了解

keep-alive是一个抽象组件,可实现组件缓存。当组件切换时不会对当前组件进行卸载。
算法: LRU --> 最近最久未使用法
常用的生命周期: activateddeactivated

声明keep-alive时在函数里设置了几个属性:props,created,destroyed,mounted和rander等;

  • props: 调用 keep-alive 组件可设置的属性,共有三个属性如下:
    include: 想缓存的组件
    exclude:不想缓存的组件
    max:最多缓存多少个
  • created: 创建一个缓存列表
  • destroyed: 销毁时清空所有缓存列表
  • mounted: 会监听 include 和 exclude, 动态添加 或 移除缓存.
  • rander: 渲染时拿到第一个组件,拿到第一个组件,判断是不是在缓存里

# $route 和 $router 的区别是什么?

$routerVueRouter实例,是全局路由对象,包含路由跳转方法、钩子函数等。

$route是路由信息对象||跳转的路由对象,每一个路由都会有一个route对象,是一个局部对象,包含path,params,hash,query,fullPath,matched,name等路由信息。

# vue-router 有哪几种路由守卫?

  • 前置守卫:beforeEach
  • 后置守卫:afterEach
  • 全局解析守卫:beforeResolve
  • 路由独享守卫:beforeEnter
  • 全局路由勾子:router.beforeEach
  • 组件路由勾子:beforeRouteEnter
  • 组件路由勾子的next里的回调:beforeRouteEnter

beforeEach和afterEach一般用于页面title的修改以及权限的控制页面的重定向功能,主要有3个参数:
to:即将进入的目标路由对象。
from:当前导航正要离开的路由。
next:function一定要调用该方法resolve这个钩子,执行效果依赖next方法的调用参数。

# v-if 和 v-for 为什么不能连用?

v-for的优先级会比v-if要高, 在调用with方法编译时会先进行循环, 然后再去做v-if的条件判断, 因此性能不高。所以一般会把v-if提出来放在v-for外层, 或者想要连用把渲染数据放在计算属性里进行过滤。

# Vue 中的 v-html 会导致哪些问题

v-html其原理就是用innerHtml实现的, 如不能保证内容是完全可以被依赖的, 可能会导致xxs攻击。
在运行的时候调用updateDOMProps方法或解析配置的属性, 如果判断属性是innerHTML的话, 会清除所有的子元素。

# Vue 中 key 的作用是什么?

需要使用key给每一个节点做唯一标识,diff算法可以正确识别此节点,可以更高效的更新虚拟DOM

diff 算法原理

  • 先同级比较, 在比较子节点。
  • 判断出一方有子节点另一方没有子节点的情况:
    如果新的一方有子节点,老的没有,则把子节点直接插入到老节点里即可。
    如果老的一方有子节点,新的没有,则把老的子节点直接删除。
  • 判断出都有子节点的情况, 递归遍历子采用双指针(头/尾指针)的方式比对节点。

# hash模式 和 history模式

hash:在url中带有#,其原理是onhashchange事件,可在window对象上监听这个事件:

  window.onhashchange = function(event){
    ...
  }

history: 没有#, 其原理是popstate事件,需要后台配置支持。html5中新增两个操作历史栈的API:pushState()replaceState()方法。

  history.pushState(data[,title][,url]); // 向历史记录中追加一条记录
  history.replaceState(data[,title][,url]); // 替换当前页在历史记录中的信息。

这两个方法也可以改变url,页面也不会重新刷新,在当前已有的back、forward、go的基础之上,它们提供了对历史记录进行修改的功能。当它们执行修改时,虽改变了当前的URL,但浏览器不会立即向后端发送请求。

# 对于 vue3.0 特性你有什么了解的吗?

(1). 监测机制的改变

3.0 基于代理ProxyObserver实现,提供全语言覆盖的反应性跟踪。替代了Vue 2采用defineProperty去定义getset, 意味着彻底放弃了兼容IE, 这也取消除了Vue 2当中基于Object.defineProperty的实现所存在的很多限制:

  • 只能监测属性,不能监测对象:
  • 检测属性的添加和删除;
  • 检测数组索引和长度的变更;
  • 支持 Map、Set、WeakMap 和 WeakSet。

新的Observer还提供了以下特性:

1、用于创建Observable的公开API。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。
2、默认采用惰性观察。在 2.x 中,不管反应式数据有多大,都会在启动时被观察到。如果数据集很大,这可能会在应用启动时带来明显的开销。在 3.x 中,只观察用于渲染应用程序最初可见部分的数据。
3、更精确的变更通知。在 2.x 中,通过 Vue.set 强制添加新属性将导致依赖于该对象的 watcher 收到变更通知。在 3.x 中,只有依赖于特定属性的 watcher 才会收到通知。
4、不可变的 observable:我们可以创建值的“不可变”版本(即使是嵌套属性),除非系统在内部暂时将其“解禁”。这个机制可用于冻结 prop 传递或 Vuex 状态树以外的变化。
5、更好的调试功能:我们可以使用新的 renderTracked 和 renderTriggered 钩子精确地跟踪组件在什么时候以及为什么重新渲染。

(2). 模板

模板方面没有大的变更,只改了作用域插槽。2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
同时,对于 render 函数的方面,vue3.0 也进行一系列更改来方便习惯直接使用 api 来生成 vdom 。

(3). 对象式的组件声明方式

vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。
vue3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易。

此外Vue的源码也改用了TypeScript来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。现在vue3.0也全面改用TypeScript来重写了,更是使得对外暴露的api更容易结合TypeScript。静态类型系统对于复杂代码的维护确实很有必要。

(4). 其它方面的更改

支持自定义渲染器,从而使得weex可以通过自定义渲染器的方式来扩展,而不是直接fork源码来改的方式。支持Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。基于treeshaking优化,提供了更多的内置功能。

# Vue 中事件绑定原理

Vue 中事件绑定分为两种:

  1. 原生事件绑定: 采用的是addEventListener实现
  2. 组件事件绑定: 采用的是$on方法实现

click事件为例:普通dom元素绑定事件是@click,编译出来是onclick事件,组件绑定事件是@click组件自定义事件和@click.native原生事件两种,编译出来分别是onclick事件,nativeOnclick事件。组件的nativeOn等价于普通元素的on,而组件的on单独处理。

渲染页面时,普通dom会调用updateDOMListeners方法,内部先把data.on方法拿出来,然后调用updateListeners方法来添加一个监听事件,同时会传入一个add$1方法。内部调用addEventListener方法直接把事件绑定到元素上。而组件会调用updateComponentListeners方法。内部也是调用updateListeners方法但传入的是add方法。这里的add方法与普通元素的domadd$1方法略有不同,采用的是自己定义的发布订阅模式$on方法,解析的是on方法,组件内部通过$emit方法触发的。还有click.native方法是直接把事件绑在了最外层元素上,用的也是updateListeners方法传入add$1方法。

# Vue 中常见的性能优化

  • 编码优化
    (1).不要将所有的数据放在data里,data中的数据都会增加gettersetter,收收集对应的watcher
    (2).在v-for时给每项元素绑定事件必须使用事件代理
    (3).SPA页面采用keep-alive缓存组件
    (4).拆分组件(提高复用性,增加代码的可维护性,减少不必要的渲染)
    (5).v-if当值为false时内部指令不执行具有阻断功能,很多情况下使用v-if代替v-show
    (6).使用key保证唯一性
    (7).使用Object.freeze冻结数据,冻结后不再有gettersetter
    (8).合理使用路由懒加载和异步组件
    (9).数据持久化问题如: 防抖、节流

  • Vue 加载性能优化
    (1).第三方模块按需导入(babel-plugin-component)
    (2).滚动可视区域动态加载(vue-virtual-scroll-list/vue-virtual-scroller) -- 长列表优化\ (3).图片懒加载(vue-lazyload)

  • 用户体验
    (1).app-skeleton架屏
    (2).app-sheapp

  • SEO 优化
    (1).预加载插件prerender-spa-plugin
    (2).服务端渲染ssr

  • 打包优化 (1).使用CDN的方式加载第三方模块
    (2).多线程打包
    (3).splitChunk 抽离公共文件

  • 缓存、压缩
    (1).客户端缓存和服务端缓存
    (2).服务端gzip压缩