likes
comments
collection
share

做寒冬里的一团火-Vue面试

作者站长头像
站长
· 阅读数 13

前言

起因:因为自己近期有面试,又因为长时间没有面试,所以提前准备,这篇文章以自我面试的方式展开。

该篇文章以自问自答的形式出现,可能会出现很多主观的回答;如果有不一样的理解(或者有错误)可以在评论区交流,做对方学习路上的一盏灯。

这里就跳过暖场阶段了(自我介绍,项目相关),直接快进到框架面(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的生命周期(只讲经常听到的)

生命周期钩子在Vue2Vue3中有一点不同,在Vue3中的Composition APIOption API中又有点不同:

  1. 在Vue2中有:beforeCreate,created,beforeMount,mounted,beforeUpdate,updated,beforeDestroy,destroyed等。
  2. 在Vue3的Option API中钩子函数,就是把Vue2中的beforedestroy 和 destoryed改为了beforeUnmount 和 unmounted
  3. 在Vue3的Composition API中钩子函数有onMounted, onUpdated, onUnmounted, onBeforeMount,onBeforeUpdate,onBeforeUnmount

面试官:看来还是挺熟悉的,嗯~,能不能说一下Vue有哪些组件通信的方式呢?

Me:当然没问题啦!

Vue有哪些组件通信的方式?

我觉得有以下几种通信方式吧:

  1. 使用props通信,父组件利用属性传值以及绑定事件给子组件,子组件通过defineProps获取属性,defineEmits来注册绑定父组件事件,达到通信效果。
  2. 利用依赖注入来实现通信,在父组件中使用provide(名称, 数据)来提供数据源,在子组件中使用const 名称 = inject(名称)来获取数据源,这样就实现了通信效果,因为无法知道数据源的来源,所以维护起来也相当困难。
  3. 使用跨组件通信方式:EventBus,这是一种设计思想,这里的原理就是导出一个Vue实例,然后借助Vueon,emit功能实现通信效果,写多了之后会异常难以维护。
  4. 借助库来实现,比如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区别?

  1. computed是计算属性,默认computed也是一个watcher具备缓存,只有当依赖数据状态发生改变才会重新计算,否则,会直接读取缓存的数据。
  2. watch是侦听器,当监听数据的状态发生改变时,执行watch的回调函数,适用于数据变化时的异步操作。
  3. method在Vue2中是一个对象,里面每个属性都是方法的定义;在Vue3的Composition API中,方法就直接在setup中定义了。

面试官:嗯,可以,那我问个稍微难点的吧,就是我们用Vue,React都会经常听到虚拟DOM这个词,能讲一下不?

能讲一下虚拟DOM是什么吗?

我觉得虚拟DOM可以从两个方面聊它是什么:

  1. 第一个方面,从框架层面来看,Vue是一种声明式框架,但是声明式框架实现的结果就是封装了命令式的代码,比如要实现在div上添加一个click事件;命令式实现,也就是原生js的实现步骤,获取dom,添加事件,但是在声明式框架Vue上,就直接给div标签上添加一个@click就行了,知道这点之后,我们再把思维放到更新层面,当某一个更新的时候,为了性能,肯定是更新要更新的地方,所以生命是代码会比命令式代码多出一个找前后差异的过程的性能消耗,而虚拟DOM就是为了最小化找出差异这一步骤出现的。
  2. 虚拟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相关

路由有几种模式,说说它们之间的区别!

我认为有三种模式:

  1. hash模式,兼容性非常,基本兼容所有浏览器,但是对SEO不够友好,在路由上会多出一个 # 字符,使路由看起来不够纯粹。
  2. HTML5模式history模式),使用这种历史模式,URL看起来会正常,但是初次访问或者刷新都会向服务器请求,如果在服务器没有一个适当的配置,就会得到一个404,这就很尴尬。
// nginx 配置
location / { 
    try_files $uri $uri/ /index.html; 
}
  1. 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拿到手软!

如果觉得有帮助的话,欢迎点赞+评论啦