- 前言
- 基础面试题
- 问:Vue 生命周期钩子都有哪些?
- 问:父组件和子组件生命周期钩子函数执行顺序?
- 问:在哪个生命周期内调用异步请求?
- 问:父组件可以监听到子组件的生命周期吗?
- 问:keep-alive 是做什么的?
- 问:组件之间的传值通信方式有哪些?
- 1. props/emit
- 2. EventBus
- 3. Vuex 状态管理
- 4. ($parent|$root)/$children
- 5. $ref
- 6. attrs/listeners
- 7. provide/inject
- 问:watch 和 computed 有什么区别?
- 问:为什么 data 必须是一个工厂函数而不能是一个对象?
- 问:Vue 中 style scoped 有作用?
- 问:v-show 与 v-if 有什么区别?
- 问:当给 data 中的对象或数组数据发生变更数据没有视图没有响应是什么原因?
- 问:SPA 和 SSR 有什么区别?
- 问:vue-router 有几种模式?
- 问:$route 和$router 的区别?
- 问:vue-router 守卫都有哪些?
- 问:vue-router 如何做权限控制?
- 问:vue-router 传参有几种方式?
- 你对 Vuex 是如何理解的?
- 底层面试题
- 问:Vue 的两个核心是什么?
- 问:如何理解 MVVM?
- 问:Vue 初始化做了什么?
- 问:如何理解 Vue 的响应式数据?
- 问:Vue 如何进行依赖收集?
- 问:Vue 是如何编译模板的?
- 问:Vue 生命周期钩子实现原理?
- 问:Vue.mixin 使用场景和原理?
- 问:$nextTick 实现原理?
- 问:vue-router 实现原理?
- 问:vuex 实现原理?
- 结束语
前言
现在对于前端来说,问的最多的可能是JavaScript
基础,除此之外问的更多的就是目前的主流框架相关的一些知识点。目前主流的框架Vue、React、Angular
,国内的互联网情况来讲,目前还是Vue
居多。
就目前来看越来越多的面试,和之前的面试不太一样了,两三年前问的更多的是对于主流框架的应用,目前更多的是对于底层的一些问题更多一些。针对这些Vue
相关面试题做一下总结并记录。希望可以帮到更多的小伙伴。
基础面试题
基础面试题即Vue
应用和使用方面的面试题,这一部分大多数还是面向于初级工程师,相对来说要简单一些。
问:Vue 生命周期钩子都有哪些?
答:Vue
生命周期一共有10
个:
- beforeCreate – 在实例初始化之前调用,在此生命周期中是不存在
this
的,因为Vue
实例还没有创建。 - created – 实例初始化之后调用
- beforeMount – 在挂载开始之前被调用,如果是在服务器端渲染时不被调用,由于元素还没有渲染,在此声明周期无法获取到
DOM
元素。 - mounted – 在挂载后被调用,是可以获取元素,并进行操作
- beforeUpdate – 视图层数据更新前调用,在虚拟
DOM
更新之前,可以获取到DOM
j 节点,但是此节点为更新之前的DOM
节点 - updated – 视图层数据更新后调用,此处获取到的
DOM
节点是更新之后DOM
- beforeDestroy – 实例销毁之前调用,在被销毁的组件中进行调用
- destroyed – 实例销毁后调用
beforeDestroy
和destroyed
生命周期阶段可以在此阶段,进行`Vuex
数据重置,取消事件监听等操作,提升性能。
以上是比较常用的生命周期钩子函数,还有两个比较特殊的生命周期钩子,分别是activated
和deactivated
,这两个生命周期只有在组件上使用了keep-alive
之后才会被执行。
- activated – 实例被激活时使用,用于重复激活一个实例的时候,该生命周期于会在
mounted
之后执行。 - deactivated – 实例没有被激活时
问:父组件和子组件生命周期钩子函数执行顺序?
答:
Vue
的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:
加载渲染过程
- 父 beforeCreate
- 父 created
- 父 beforeMount
- 子 beforeCreate
- 子 created
- 子 beforeMount
- 子 mounted
- 父 mounted
子组件更新过程
- 父 beforeUpdate
- 子 beforeUpdate
- 子 updated
- 父 updated
父组件更新过程
- 父 beforeUpdate
- 父 updated
销毁过程
- 父 beforeDestroy
- 子 beforeDestroy
- 子 destroyed
- 父 destroyed
问:在哪个生命周期内调用异步请求?
答:
可以在钩子函数created
、beforeMount
、mounted
中进行调用,因为在这三个钩子函数中,data
已经创建,可以将服务端端返回的数据进行赋值。推荐在 created 钩子函数中调用异步请求,因为在created
钩子函数中调用异步请求,因为能更快获取到服务端数据,减少页面loading
时间,ssr
不支持beforeMount
、mounted
钩子函数,所以放在created
中有助于一致性。
问:父组件可以监听到子组件的生命周期吗?
答:
可以的,有两种方法可以可以做到,第一种则是使用$emit
自定义事件进行事件监听,当子组件执行到某个生命周期时用$emit
通知即可。第二种使用@hook:生命周期名称
,@hook
方法不仅仅是可以监听mounted
,其它的生命周期事件,例如:created
,updated
等都可以监听。
问:keep-alive 是做什么的?
答:keep-alive
是Vue
内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:
- 一般结合路由和动态组件一起使用,用于缓存组件
- 提供
include
和exclude
属性,两者都支持字符串或正则表达式,include
表示只有名称匹配的组件会被缓存,exclude
表示任何名称匹配的组件都不会被缓存 ,其中exclude
的优先级比include
高 - 对应两个钩子函数
activated
和deactivated
,当组件被激活时,触发钩子函数activated
,当组件被移除时,触发钩子函数deactivated
。
问:组件之间的传值通信方式有哪些?
答:组件间通信方式有 7 种:
1. props/emit
父组件传入属性,子组件通过props
接收,可以通过这种方法获取到父组件传入的参数。而子组件则是可以通过$emit('时间名',参数)
像外暴露一个自定义事件,在父组件中的属性监听事件,同时也能获取子组件传出来的参数,使用v-on:事件名称=func
或者@事件名称=func
的方式监听子组件的自定义事件。
还可以通过prop
接收函数为参数,子组件调用该函数,子组件执行函数时,可以传入对应的实参,在父组件接收时以形参的形式接收,执行对应的逻辑,可以达到通信的目的。
2. EventBus
EventBus
又称为事件总线,EventBus
来作为沟通桥梁可以使任意两个组件之间通讯,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件。
如何使用EventBus
,使用new Vue
的方式获得Vue
实例,实质上EventBus
是一个不具备DOM
的组件,它具有的仅仅只是它实例方法而已,因此它非常的轻便。
可以把获取到Vue
实例挂载到Vue.prototype
中也可以存放到一个单独的js
文件中导出,使用哪种取决于业务的具体情况,一般推荐使用第二种方法。
使用EventBus.$emit("事件名称", 参数);
的方式进行事件发布。使用EventBus.$on("事件名称", func)
的方法对事件进行订阅。func
的参数则是对应事件传递过来的参数。
EventBus
的原理是把注册的事件存起来,等触发事件时再调用。定义一个类去处理事件。
3. Vuex 状态管理
Vuex
是Vue
的核心插件、用于任意组件的任意通讯,需注意刷新后有可能Vuex
数据会丢失。
创建全局唯一的状态管理仓库store
,有同步mutations
、异步actions
的方式去管理数据,有缓存数据getters
,还能分成各个模块modules
易于维护
4. ($parent|$root)/$children
这种通信方式推荐使用,这样会使两个组件之间强耦合在一起,对于以后的维护和拓展来说不不是特别的友好。
通过($parent|$root)/$children
获取到对应的组件实例,调用组件内部的方法以达到通信的效果。
5. $ref
通过$ref
引用的方式获取子节点,常用于父组件调用子组件的方法,在$refs
来存储当前所有设置了ref
属性的组件的实例,在实例中可以调用到组件内部的方法。
6. attrs/listeners
$attrs
可以获取父组件传进来但没有通过props
接收的属性,可以使用v-bind="$attrs"
的形式继续向子组件传递参数。
$listeners
会展开父组件的所有监听的事件,一个页面中有两个组件的点击事件触发方法是一样的。可以使用v-on="$listeners"
把事件继续向子组件传递事件。
7. provide/inject
父组件中通过provider
来提供变量,然后在子组件中通过inject
来注入变量。只要在父组件中调用了,那么在这个父组件生效的生命周期内,所有的子组件都可以调用inject
来注入父组件中的值。
问:watch 和 computed 有什么区别?
答:
computed
是计算属性在使用时和data
对象中的数据属性是类似的,watch
是用于监听某一个属性的变化的,computed
擅长的是一个数据受多个数据影响,然而watch
是用于监听某一个属性的变化的。
computed
支持缓存只有依赖数据发生改变,才会重新进行计算,computed
基于它们的响应式依赖进行缓存的,也就是基于data
中声明过或者父组件传递的props
中的数据通过计算得到的值,watch
则不支持缓存,数据变,直接会触发相应的操作。watch
的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;
computed
不支持异步,computed
内有异步操作时无效,无法监听数据的变化,watch
支持异步。
computed
在使用时,computed
属性值是函数,那么默认会走get
方法,函数的返回值就是属性的属性值,在 computed
中的,属性都有一个get
和一个set
方法,当数据变化时,调用set
方法。watch
在使用时,监听数据必须是data
中声明过或者父组件传递过来的props
中的数据,当数据变化时,触发其他操作,函数有两个参数。immediate
(组件加载立即触发回调函数执行),deep
(深度监听)为了发现对象内部值的变化,复杂类型的数据时使用。
computed
内部就是根据Object.definedProperty()
实现的,computed
具备缓存功能,依赖的值不发生变化,就不会重新计算。watch
是监控值的变化,值发生变化时会执行对应的回调函数,computed
和watch
都是基于Watcher
类来执行的。computed
缓存功能依靠一个变量 dirty
,表示值是不是脏的默认是true
,取值后是false
,再次取值时dirty
还是false
直接将还是上一次的取值返回。
问:为什么 data 必须是一个工厂函数而不能是一个对象?
答:
因为Vue
组件复用原则,由于data
是对象为引用类型,当一个组件的数据改变后另一个组件状态会受到影响,然而通过工厂函数的形式返回对象就不会导致这样的问题,因为每个工厂函数所返回的对象都是一个全新的对象相对独立。
问:Vue 中 style scoped 有作用?
答:
在vue
文件中的style
标签上,有一个特殊的属性:scoped
。当一个style
标签拥有scoped
属性时,它的 CSS 样式就只能作用于当前的组件,也就是说,该样式只能适用于当前组件元素。通过该属性,可以使得组件之间的样式不互相污染。scoped
会在DOM
结构及css
样式上加上唯一性的标记data-v-something
属性,即CSS
带属性选择器,以此完成类似作用域的选择方式,从而达到样式私有化,不污染全局的作用。scoped
使用虽然方便但是我们需要慎用,因为在我们需要修改公共组件(三方库或者项目定制的组件)的样式的时候,scoped 往往会造成更多的困难,需要增加额外的复杂度。使用>>>
可以穿透scoped
属性,修改其他第三方组件的样式。使用sass
或less
的样式穿透/deep/
。
问:v-show 与 v-if 有什么区别?
答:
v-if
是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建,v-if
是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS
的display
属性进行切换。所以,v-if
适用于在运行时很少改变条件,不需要频繁切换条件的场景,v-show
则适用于需要非常频繁切换条件的场景。
问:当给 data 中的对象或数组数据发生变更数据没有视图没有响应是什么原因?
答:
因为在Vue
在初始化时,会对data
中的数据使用object.defineproperty
对数据进行挟持,通过get/set
完成数据响应的操作,由于添加的属性,在Vue
在初始化时该属性不存在于data
中,所以该属性没有挟持到无法执行get/set
。Vue
提供过this.$set
方法可以对新增加的数据挟持完成数据相应。
问:SPA 和 SSR 有什么区别?
答:
SPA
应用是现在最常用的开发形式即单页面应用,页面整体式javaScript
渲染出来的,称之为客户端渲染CSR
。SPA
渲染过程由客户端访问URL
发送请求到服务端,返回HTML
结构(但是SPA
的返回的HTML
结构是非常的小的,只有一个基本的结构)。客户端接收到返回结果之后,在客户端开始渲染HTML
,渲染时执行对应javaScript
最后通过JavaScript
渲染成HTML
,渲染完成之后再次向服务端发送数据请求,注意这里时数据请求,服务端返回json
格式数据。客户端接收数据,然后完成最终渲染。
SSR
渲染流程是这样的,客户端发送URL
请求到服务端,服务端读取对应的URL
的模板信息,在服务端做出HTML
和数据的渲染,渲染完成之后返回HTML
结构,客户端这时拿到的之后首屏页面的HTML
结构。所以用户在浏览首屏的时候速度会很快,因为客户端不需要再次发送ajax
请求。并不是做了SSR
我们的页面就不属于SPA
应用了,它仍然是一个独立的SPA
应用。
SSR
是处于CSR
与SPA
应用之间的一个折中的方案,在渲染首屏的时候在服务端做出了渲染,注意仅仅是首屏,其他页面还是需要在客户端渲染的,在服务端接收到请求之后并且渲染出首屏页面,会携带着剩余的路由信息预留给客户端去渲染其他路由的页面。
SSR 优点:
- 更好的 SEO,搜索引擎爬虫爬取工具可以直接查看完全渲染的页面
- 更宽的内容达到时间(
time-to-content
),当权请求页面的时候,服务端渲染完数据之后,把渲染好的页面直接发送给浏览器,并进行渲染。浏览器只需要解析html
不需要去解析javaScript
。
SSR 缺点:
- 开发条件受限,
Vue
组件的某些生命周期钩子函数不能使用 - 开发环境基于
Node.js
- 会造成服务端更多的负载。在
Node.js
中渲染完整的应用程序,显然会比仅仅提供静态文件server
更加占用CPU
资源,因此如果你在预料在高流量下使用,请准备响应的服务负载,并明智的采用缓存策略
问:vue-router 有几种模式?
答:
vue-router
公共三种模式:hash
、history
和abstract
。
hash 模式:
hash
值是url
中#
及后面的部分hash
值改变不会引起页面刷新hash
值的改变会触发hashchange
事件
history 模式:
- 利用
history.pushState
来完成url
跳转而无需刷新页面 - 需要后台配置支持,如果
URL
匹配不到任何静态资源,服务器应该返回应用依赖的index.html
页面
abstract 模式:
- 适用于所有
JavaScript
环境,例如服务器端使用Node.js
- 没有浏览器
API
,路由器将自动被强制进入此模式 - 这个历史记录的主要目的是处理
SSR
,通过调用router.push
或router.replace
将该位置替换为启动位置
问:$route 和$router 的区别?
答:
$router
通过Vue.use(VueRouter)
和Vue
构造函数得到一个router
的实例对象,这个对象中是一个全局的对象,他包含了所有的路由,包含了许多关键的路由信息,还有路由的跳转方法,钩子函数等。
$route
$route
是一个跳转的路由对象,每一个路由都会有一个$route
对象,是一个局部的对象,可以获取对应的name
,path
,params
,query
等。路由对象的属性是只读的,里面的属性是immutable
(不可变)的,不过可以使用watch
监测路由的变化。
问:vue-router 守卫都有哪些?
答:
vue-router
守卫主分为三类守卫分别是:全局守卫、路由守卫和组件守卫
全局守卫
全局守卫字面量理解就是定义在全局钩子函数,当路由触发跳转操作时则会触发对应的钩子函数,全局守卫有三种:
- beforeEach:它在每次导航路由跳转前触发
- beforeResolve: 所有
组件内守卫
和异步路由组件
被解析之后触发 - afterEach:路由跳转完成后触发
beforeEach
全局前置守卫接收三个参数:
- to: 即将要进入的目标路由对象
- from: 当前导航正要离开的路由对象
- next: 一定要调用该方法不然会阻塞路由
重点说一下next
参数,next
参数是一个函数,可以不添加,但是一旦添加,则必须调用一次,否则路由跳转等会停止。next
参数只有在beforeEach
和beforeResolve
两个路由守卫中存在,afterEach
因为已经跳转到了目标路由则没有提供next
方法。如果直接执行next
函数则会直接接入to
所对应的目标路由中。next(false)
则会终端当前路由当行,返回form
路由对应的路由中。在next
中传入跳转方案(如:next('/') 或者 next({ path:'/'})
),则会跳转到对应的地址中。next(error)
则导航会终止,且该错误会被传递给router.onError()
注册过的回调。beforeEach
可以定义多个,会根据创建顺序调用,在所有守卫完成之前导航一直处于等待中。
路由守卫
路由守卫只有一个beforeEnter
,需要在路由配置上定义beforeEnter
守卫,此守卫只在进入路由时触发,在beforeEach
之后紧随执行,不会在params
、query
或hash
改变时触发,beforeEnter
路由守卫的参数是to
、from
、next
,同beforeEach
一样。
组件守卫
组件守卫则是需要定义在组件中的,组件守卫石油三种:
- beforeRouteEnter:路由进入组件之前调用,该钩子在全局守卫
beforeEach
和路由守卫beforeEnter
之后,全局beforeResolve
和全局afterEach
之前调用,该守卫内访问不到组件的实例,也就是this
为undefined
,也就是他在beforeCreate
生命周期前触发 - beforeRouteUpdate:在当前路由改变,但是该组件被复用时调用,举例来说,对于一个带有动态参数的路径
/foo/:id
,在/foo/1
和/foo/2
之间跳转的时候,由于会渲染同样的Foo
组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。可以访问到组件实例this
- beforeRouteLeave:导航离开该组件的对应路由时调用,可以访问组件实例
this
问:vue-router 如何做权限控制?
答:
vue-router
权限控制最常用的方法是在beforeEach
需要跳转的路由进行控制,如果没有权限则无法跳转到路由,一般情况下在路由配置的meta
中添加权限控制字段,当路由发生跳转时,会触发beforeEach
全局路由守卫,在该守卫中对权限进行鉴别即可完成路由权限控制。初次之外还有一种方法,则是使用addRoutes
方法,动态的向vue-router
中添加路由配置,在添加配置之前只在路由中注册具有权限的路由信息即可。
问:vue-router 传参有几种方式?
答:
vue-router
传参一共有三种方式:
param
param
传参需要在配置路由信息的是时候,在路由中注明需要接收的参数(如:url/:id
),其中的id
则是param
需要传递的参数,需要注意的是当以这种形式传递参数的时候,如果没有传递参数是无法正确的匹配到路由的。可以在定义参数前添加?
则代表该参数可有可无,无论是否传递参数都能正确的匹配到对应的路由。参数后面可以添加正则如:url/?:id(\d+)
,以这种形式添加参数可以限制参数的格式。
当使用param
的时候,在配置路由信息的时添加{props:true}
配置项,则传入的param
参数,在页面的中的props
中接收到该参数,除了以这种方式接收以外,还可以使用this.$route.params
中获取到参数。传递param
参数无论使用router-link
还是编程式导航,如果是传入的字符串形式,则可以在对应的路由地址后面直接拼接参数即可。如果是以对象的形式,在对象中添加params
字段,内部对应的是对应的param
参数即可。
query
query
和param
传参只是有略微的不同,query
传参不需要在路由配置中定义有哪些参数,query
是可有可无的不会影响到路由地址的匹配。query
的传递参数使用query
对象或者在地址后面以?
的形式进行拼接。接收的时候则是在this.$route.query
中获取到传递过来的参数。
注:无论是使用 param 还是 query 进行路由传参,当页面刷新的时候,都不会导致参数丢失,因为其参数是直接存放于路由地址中
meta
个人不太建议使用meta
这种形式传参,虽然通过操作也是可以完成传参的目的,但是当页面刷新的时候会导致参数的丢失,但是,这种传参是隐式传参,用户是无法得知的。
使用meta
传参可以在路由跳转之前,在跳转页面通过this.$route
获取到当前路由的对象,在路由对象的meta
中存入对应的参数。然后使用对应的方法进行路由跳转。当跳转完成之后,在目标页面使用beforeRouteEnter
组件钩子函数,对参数进行接收,虽然beforeRouteEnter
无法访问到this
,但是在第三个参数即next
中可以传入一个函数作为参数,这个参数中可以访问到当前组件的实例,在之后在beforeRouteEnter
的第二个参数form
中对象中可以读取到meta
中存储的数据完成传参。
你对 Vuex 是如何理解的?
答:
Vuex
是为Vue
提供的状态管理模式,集中式存贮管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。中有五大核心属性:
- state:
state
主要用于存储数据和存储状态,在根实例中注册store
以后,用this.$store.state
来访问到state
中所存储的数据。存放数据方式为响应式,vue
组件从store
中读取数据,如数据发生变化,组件也会对应的更新。 - mutation:更改
Vuex
的store
中的状态的唯一方法是提交mutation
。 - action:该方法与
mutation
类似,唯一不同的是在action
所执行的是异步方法。 - getter:可以认为是
store
的计算属性,它的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。 - module:将
store
分割成模块,每个模块都具有state
、mutation
、action
、getter
甚至是嵌套子模块。
当组件进行数据修改的时候我们需要调用dispatch
来触发actions
里面的方法。actions
里面的每个方法中都会有一个commit
方法,当方法执行的时候会通过commit
来触发mutations
里面的方法进行数据的修改。mutations
里面的每个函数都会有一个state
参数,这样就可以在mutations
里面进行state
的数据修改,当数据修改完毕后,会传导给页面。页面的数据也会发生改变。
由于传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递虽然也可以通过其他的方法进行参数传递,可能仍会感到无力。我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致代码无法维护。所以我们需要把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,我们的组件树构成了一个巨大的视图
,不管在树的哪个位置,任何组件都能获取状态或者触发行为!另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,我们的代码将会变得更结构化且易维护。
底层面试题
这一部分面试题相比上面要相对难一些,可能直接上升了一个等级,可能需要对Vue
相关API
有足够的了解,并且阅读过相关源码的下才能更好的理解。
问:Vue 的两个核心是什么?
答:分别是:数据驱动和组件系统
数据驱动
Vue
数据观测原理在技术实现上,利用的是ES5
的Object.defineProperty
和存储器属性:getter
和setter
可称为基于依赖收集的观测机制,这些getter
和setter
对用户来说是不可见的,但是在内部它们让Vue
能够追踪依赖,在property
被访问和修改时通知变更。核心是VM
,即ViewModel
,保证数据和视图的一致性。每一个指令都会有一个对应的用来观测数据的对象,叫做watcher
,每个组件实例都对应一个watcher
实例,它会在组件渲染的过程中把接触
过的数据property
记录为依赖。getter
的时候我们会收集依赖,依赖收集就是订阅数据变化watcher
的收集,依赖收集的目的是当响应式数据发生变化时,能够通知相应的订阅者去处理相关的逻辑。setter
的时候会触发依赖更新,之后当依赖项的setter
触发时,会通知watcher
,从而使它关联的组件重新渲染。
组件系统
组件系统是Vue
的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树,Vue
更希望构建的页面是由各个组件构成而非html
,当开发项目时把整个应用程序划分为组件,以使开发更易管理与维护。
组件系统中包含了几个很重要的部分:
- template:模板声明了数据和最终展现给用户的 DOM 之间的映射关系。
- data:一个组件的初始数据状态。对于可复用的组件来说,这通常是私有的状态
- props:组件之间通过参数来进行数据的传递和共享
- methods:对数据的改动操作一般都在组件的方法内进行
- lifecycle hooks:一个组件会触发多个生命周期钩子函数
- assets:Vue.js 当中将用户自定义的指令、过滤器、组件等统称为资源
问:如何理解 MVVM?
答:
MVVM
主要由三个部分组成:Model
、View
、ViewModel
- Model:代表数据模型,也可以在 Model 中定义数据修改和业务逻辑
- View:代表 UI 组件,它负责将数据模型转化成 UI 展现出来
- ViewModel:代表同步 View 和 Model 的对象
在MVVM
中View
和Model
没有直接的关系,全部通过ViewModel
进行交互。ViewModel
负责把Model
的数据同步到View
显示出来,还负责把View
的修改同步回Model
。MVVM
本质就是基于操作数据来操作视图进而操作DOM
,借助MVVM
无需直接操作DOM
,开发者只需完成包含声明绑定的视图模板,编写ViewModel
中有业务,使得View
完全实现自动化。
问:Vue 初始化做了什么?
答:
Vue
在初始化时会接收用户所传入的options
即所需要的参数,之后则会创建Vue
的实例,并且为该实例定义一个唯一的_uid
的标识,用于区分Vue
的实例,把Vue
实例标记成Vue
避免被观察者观测到。
完成实例创建的初始化工作之后,需要对用户选项和系统默认的选项进行合并,首先处理的的是组件的配置内容,把传入的option
与其构造函数本身进行合并,这里蚕蛹的策略是默认配置和传入配置的配置进行合并。如果是内部组件(子组件)实例化,且动态的options
合并会很慢,需要对options
的一些特殊参数进行处理。如果是根组件,将全局配置选项合并到根组件的配置上,其实就是一个选项合并。
接下来则是初始化核心部分,首先初始化Vue
实例生命周期相关的属性,定义了比如:root
、parent
、children
、refs
,接着初始化自定义组件事件的监听,若存在父监听事件,则添加到该实例上,然后初始化render
渲染所需的 slots、渲染函数等。其实就两件事
- 插槽的处理、
- $createElm 也就是 render 函数中的 h 的声明
接下来则是执行beforeCreate
钩子函数,在这里就能看出一个组件在创建前和后分别做了哪些初始化。
执行完beforeCreate
钩子函数之后,初始化注入数据,隔代传参时先inject
。作为一个组件,在要给后辈组件提供数据之前,需要先把祖辈传下来的数据注入进来。对props
、methods
、data
、computed
、watch
进行初始化,包括响应式的处理,在把祖辈传下来的数据注入进来以后再初始化provide
。
最后调用created
钩子函数,初始化完成,可以执行挂载了,挂载到对应DOM
元素上。如果组件构造函数设置了el
选项,会自动挂载,所以就不用再手动调用$mount
去挂载。
问:如何理解 Vue 的响应式数据?
答:
Vue
中实现了一个definedReactive
方法,方法内部借用Object.definedProperty()
给每一个属性都添加了get/set
的属性。definedReactive
只能监控到最外层的对象,对于内层的对象需要递归劫持数据。数组则是重写的 7 个push
、pop
、shift
、unshift
、reverse
、sort
、splice
来给数组做数据拦截,因为这几个方法会改变原数组。对象新增或者删除的属性无法被set
监听到只有对象本身存在的属性修改才会被劫持。或使用Vue
提供的$set()
实现监听,在defineReactive
中存有一个Dep
类,这个用来收集渲染的Watcher
,Watcher
的主要工作则使用用来更新视图。
问:Vue 如何进行依赖收集?
答:
Dep
是一个用来负责收集Watcher
的类,Watcher
是一个封装了渲染视图逻辑的类,用于派发更新的。需要注意的是Watcher
是不能直接更新视图的还需要结合Vnode
经过patch()
中的diff
算法才可以生成真正的DOM
。
然而每一个属性都有自己的dep
属性,来存放依赖的Watcher
,属性发生变化后会通知Watcher
去更新。在用户获取(getter
)数据时Vue
给每一个属性都添加了dep
属性来(collect as Dependency
)收集Watcher
。在用户setting
设置属性值时dep.notify()
通知收集的Watcher
重新渲染。Dep
依赖收集类其和Watcher
类是多对多双向存储的关系。每一个属性都可以有多个Watcher
类,因为属性可能在不同的组件中被使用。同时一个Watcher
类也可以对应多个属性。
每一个属性可以有多个依赖,比如这个属性可能使用在computed
中,watch
中,本身的data
属性中。这些依赖都是使用响应式数据的Dep
来收集的。Watcher
是依赖就像一个中介,能够被Dep
收集也能够被Dep
通知更新。
问:Vue 是如何编译模板的?
答:
编译是把我们写的.vue
文件里的template
标签包含的html
标签变为render
函数,可以这么理解template
模板是对render
的封装,template
在vue
源码里面会变转化为render
函数:
- 将 template 模板字符串转换成 ast 语法树(parser 解析器),这里使用了大量的正则来匹配标签的名称,属性,文本等。
- 对 AST 进行静态节点 static 标记,主要用来做虚拟 DOM 的渲染优化(optimize 优化器),这里会遍历出所有的子节点也做静态标记
- 使用 AST 语法树 重新生成 render 函数代码字符串 code。
问:Vue 生命周期钩子实现原理?
答:
Vue
中的生命周期钩子只是一个回调函数,在创建组件实例化的过程中会调用对应的钩子执行。使用Vue.mixin({})
混入的钩子或生命周期中定义了多个函数,Vue
内部会调用mergeHook()
对钩子进行合并放入到队列中依次执行。
问:Vue.mixin 使用场景和原理?
答:
Vue.mixin
主要用于抽离一个公共的业务逻辑实现复用。其内部执行时会调用mergeOptions()
方法采用策略模式针对不同的属性合并。混入的数据和组件的数据有冲突就采用组件本身的。Vue.mixin({})
存在一些缺陷,导致混入的属性名和组件属性名发生命名冲突,数据依赖的来源问题。
问:$nextTick 实现原理?
答:
vm.$nextTick(cb)
是一个异步的方法为了兼容性做了很多降级处理依次有promise.then,MutationObserver
,setImmediate
,setTimeout
。在数据修改后不会马上更新视图,而是经过set
方法notify
通知Watcher
更新,将需要更新的Watcher
放入到一个异步队列中,nexTick
的回调函数就放在Watcher
的后面,等待主线程中同步代码执行借宿然后依次清空队列中,所以vm.nextTick(callback)
是在dom
更新结束后执行的。
问:vue-router 实现原理?
答:vue-router
最常用的模式有两种分别是hash
模式和history
模式:
hash 模式
hash
模式是vue-router
的默认路由模式,它的标志是在域名之后带有一个#
,通过window.location.hash
获取到当前url
的hash
。·hash·模式下通过hashchange
方法可以监听url
中hash
的变化。hash
模式的特点是兼容性更好,并且hash
的变化会在浏览器的history
中增加一条记录,可以实现浏览器的前进和后退功能。
history 模式
history
模式是另一种前端路由模式,它基于HTML5
的history
对象,通过location.pathname
获取到当前 url 的路由地址。history
模式下,通过pushState
和replaceState
方法可以修改url
地址,结合popstate
方法监听url
中路由的变化。
问:vuex 实现原理?
答:
Vuex
在初始化时,在全局存储了Vue
的实例,在install
函数中,首先会判断是否已经调用了Vue.use(Vuex)
,然后调用applyMixin
方法进行初始化的一些操作,applyMixin
方法只做了一件事情,就是将所有的实例上挂载一个$store
对象,在使用vuex
的时候,会将store
挂载在根组件之上。在第一次调用vuexInit
函数时,options.store
就是根选项的store
,因此会判断其类型是不是function
,若是则执行函数并将结果赋值给根实例的$store
中,否则直接赋值。
Vuex
的state
状态是响应式,是借助vue
的data
是响应式,将state
存入vue
实例组件的data
中,Vuex
的getters
则是借助vue
的计算属性computed
实现数据实时监听。
结束语
以上面试题是笔者在面试过程中面试官们问到的做了一些调研学习和整理,可能会有些许的错误,大家可以在下面评论指出错误,大家共同学习进步。如果有哪些方面没有覆盖到的地方大家也可以评论告诉我,后面回补齐。