做寒冬里的一团火-Vue面试
前言
起因:因为自己近期有面试,又因为长时间没有面试,所以提前准备,这篇文章以自我面试的方式展开。
该篇文章以自问自答的形式出现,可能会出现很多主观的回答;
如果有不一样的理解(或者有错误)可以在评论区交流,做对方学习路上的一盏灯。
这里就跳过暖场阶段了(自我介绍
,项目相关
),直接快进到框架面(Vue)。
纯文字版话术,基本不涉及代码
Vue 相关
首先,来谈一谈你对Vue的理解吧!
首先来说,Vue是一个渐进式的JS框架,有很多独立的功能库和插件;它还是一个声明式的UI框架,就是当用户使用Vue开发页面时是声明式描述UI的,并提供了一套声明式,组件化的编程模型;它借助了MVVM
设计模式思想,做双向数据绑定,有一套自己的数据响应式的系统。
面试官:好的,你刚刚是提到了MVVM,能详细说下吗?
Me:没问题
你刚刚提到了MVVM,能详细说说吗?
MVVM
,它是一种设计思想,全称是Model-View-ViewModel
,其中,Model
表示数据模型层(提供数据),View
表示视图层(渲染数据),ViewModel
视图模型层(调用数据渲染视图),该模式也就相当于是数据双向绑定的雏形,当数据更新,变更视图,当视图变化通知ViewModel
更新数据的这样的一个过程。
面试官:嗯,ok,听你一直在说数据响应式这个东西,那么Vue是怎么实现数据响应式的呢?
Vue是如何实现数据响应式的呢?
首先,这里的话,Vue2与Vue3的实现其实是有一定区别的:
Vue2
:Vue2里使用了Object.defineProperty
通过对数据的get,set
做劫持,然后当数据属性被get
(取值)和set
(赋值)的时候Vue2
都能监听到,但是在这里面也有一个明显的缺陷,那就是对象新增或者删除的属性无法被set
监听到,只有对象本身属性被修改才会被劫持,这可能也是Vue2
中重写了一些数组方法的原因之一吧。
Vue3
:Vue3有所不同,它是使用了新语法Proxy
对象,用它来实现了数据代理的效果,其本质也是自定义get,set
方法,在每次get
的时候,会将副作用函数,也就是依赖函数存入一个Dep容器里,该容器是一种WeakMap
的数据结构,在set
的时候,从容器里取出对应依赖函数并执行,就达到了响应式的效果。
代码的具体实现,我记录在了我一篇博客中(Vue3响应式),然后链接甩面试官脸上(开玩笑0_0)
面试官:还不错,看来是了解过的,接下来说一说Vue生命周期吧!
说一说Vue的生命周期(只讲经常听到的)
生命周期钩子在Vue2
与Vue3
中有一点不同,在Vue3
中的Composition API
与Option API
中又有点不同:
- 在Vue2中有:
beforeCreate
,created
,beforeMount
,mounted
,beforeUpdate
,updated
,beforeDestroy
,destroyed
等。 - 在Vue3的
Option API
中钩子函数,就是把Vue2中的beforedestroy 和 destoryed
改为了beforeUnmount 和 unmounted
。 - 在Vue3的
Composition API
中钩子函数有onMounted, onUpdated, onUnmounted, onBeforeMount,onBeforeUpdate,onBeforeUnmount
。
面试官:看来还是挺熟悉的,嗯~,能不能说一下Vue有哪些组件通信的方式呢?
Me:当然没问题啦!
Vue有哪些组件通信的方式?
我觉得有以下几种通信方式吧:
- 使用
props
通信,父组件利用属性传值以及绑定事件给子组件,子组件通过defineProps
获取属性,defineEmits
来注册绑定父组件事件,达到通信效果。 - 利用依赖注入来实现通信,在父组件中使用
provide(名称, 数据)
来提供数据源,在子组件中使用const 名称 = inject(名称)
来获取数据源,这样就实现了通信效果,因为无法知道数据源的来源,所以维护起来也相当困难。 - 使用跨组件通信方式:
EventBus
,这是一种设计思想,这里的原理就是导出一个Vue
实例,然后借助Vue
的on,emit
功能实现通信效果,写多了之后会异常难以维护。 - 借助库来实现,比如
Vuex, Pinia
。
面试官:可以,不错,看来用得都还蛮熟悉的,既然你提到了管理库,那就说说这两个库吧。
说说Vuex和Pinia这两个库?
首先,Vuex 和 Pinia
都是Vue
官方推出的状态管理库,允许跨组件或页面共享状态,在使用上,其实差别并不大,在Vuex
中有五个核心概念:State,Getter,Mutation,Action,Module
,在Pinia
中,将Mutation 和 Action
合并成了Action
,还提供了插件扩展的功能。
State
:存放数据状态Getter
:相当于计算属性一样,接收state为参数Mutation
:同步函数,更改数据状态的地方Action
:异步请求等,不能直接改变状态Module
:将store进行模块分割插件
:扩展Pinia功能
面试官:听你说到了计算属性,能聊聊计算属性,侦听器,方法的区别吗?
computed watch 和 method区别?
computed
是计算属性,默认computed
也是一个watcher
具备缓存,只有当依赖数据状态发生改变才会重新计算,否则,会直接读取缓存的数据。watch
是侦听器,当监听数据的状态发生改变时,执行watch
的回调函数,适用于数据变化时的异步操作。method
在Vue2中是一个对象,里面每个属性都是方法的定义;在Vue3的Composition API中,方法就直接在setup中定义了。
面试官:嗯,可以,那我问个稍微难点的吧,就是我们用Vue,React都会经常听到虚拟DOM
这个词,能讲一下不?
能讲一下虚拟DOM是什么吗?
我觉得虚拟DOM
可以从两个方面聊它是什么:
- 第一个方面,从框架层面来看,Vue是一种声明式框架,但是声明式框架实现的结果就是封装了命令式的代码,比如要实现
在div上添加一个click事件
;命令式实现,也就是原生js的实现步骤,获取dom,添加事件,但是在声明式框架Vue上,就直接给div标签上添加一个@click
就行了,知道这点之后,我们再把思维放到更新层面,当某一个更新的时候,为了性能,肯定是更新要更新的地方,所以生命是代码会比命令式代码多出一个找前后差异的过程
的性能消耗,而虚拟DOM
就是为了最小化找出差异这一步骤出现的。 - 从
虚拟DOM
本质来看,它就是一个js对象
,是一个对真实DOM的描述信息,比如有:tag
属性表示标签类型,children
属性表示子元素,prop
属性表示标签属性等等。
面试官:看来你的理解还不错,这里说到了找出前后差异的过程,是怎么做的呢?
diff算法的实现
因为大多数更新都不是全面的更新,而是一小部分的更新,所以为了提升性能,让更新只更新需要更新的地方(绕口令0_0),经过业界大佬们的完善出了这个diff算法。
在Vue中的diff算法,主要采取了几种方式双端对比,交叉对比,key对比
,也就是通过头头,头尾,尾头,尾尾,key的比较,将不同的地方收集patch
,然后实现局部更新。
面试官:Vue问的差不多了,讲讲性能优化方面的吧,什么优化都算!
说一说性能优化,什么都可以
编码层面的优化,比如,做事件代理,添加key值,异步组件,防抖节流等等。
加载层面优化,按需导入(结合Tree-shaking机制),图片懒加载,路由懒加载。
打包层面优化,比如在Vue3项目中只使用到了Composition API,那我们打包的时候完全没必要保留Option API部分代码,增大打包文件体积,这时候可以使用构建工具设置__VUE__OPTION__API__
这个特性开关变为false
,这样Option API特性就被关闭,打包的时候就会被Tree-Shaking
机制清除掉。
面试官:好,关于Vue框架,问的差不多了,整体掌握还不错,接下来聊聊与前端息息相关的路由吧!
VueRouter相关
路由有几种模式,说说它们之间的区别!
我认为有三种模式:
hash模式
,兼容性非常,基本兼容所有浏览器,但是对SEO不够友好,在路由上会多出一个 # 字符,使路由看起来不够纯粹。HTML5模式
(history模式
),使用这种历史模式,URL看起来会正常,但是初次访问或者刷新都会向服务器请求,如果在服务器没有一个适当的配置,就会得到一个404
,这就很尴尬。
// nginx 配置
location / {
try_files $uri $uri/ /index.html;
}
abstract
,支持所有 JavScript 运行环境,比如Node服务器,当发现没有浏览器API时,路由就会强制进入这个模式。
面试官:嗯嗯,不错,那比如用户未登录前,直接访问其他页面,但是我们不让它访问,这时候我们要怎么样才能做到?
如何拦截路由
在VueRouter
中提供了拦截器
有全局前置守卫,全局解析守卫,全局后置守卫
。
// 全局前置守卫
const router = createRouter({ ... })
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
})
// 全局解析守卫
router.beforeResolve(async to => {
// 是获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置。
}
// 全局后置守卫
router.afterEach((to, from) => {
// 不接受 next
})
面试官:可以可以,再问一个
如何获取路由信息,还有跳转路由
useRouter,返回一个对象,使用这个对象中push
方法来进行路由跳转,它本质上其实就是向history栈
中添加一个路由,就是添加一个history
记录。
useRoute,返回一个route对象
,他是一个路由对象,包含当前对应路由的所有信息,有name, path, params, query
等
面试官:欧克欧克,了解差不多了,今天就面到这吧,回去等通知!(😂😂😂)
开个玩笑啦!祝兄弟萌offer
拿到手软!
如果觉得有帮助的话,欢迎点赞+评论啦
转载自:https://juejin.cn/post/7204057769493184567