你看到那片麦田了吗?我不吃面包,麦子对我来说没有用,麦田不会让我联想起什么。这让人难过!但你有一头金发。所以,等你驯服了我,一切该有多美妙!那时,金色的麦子,会让我想起你。我会爱上风吹过麦田的声音...... ——《小王子》
前言
起因:因为自己近期有面试,又因为长时间没有面试,所以提前准备,这篇文章以自我面试的方式展开。
该篇文章以自问自答的形式出现,可能会出现很多主观的回答;
如果有不一样的理解(或者有错误)可以在评论区交流,做对方学习路上的一盏灯。
这里就跳过暖场阶段了(自我介绍
,项目相关
),直接快进到框架面(Vue)。
文字版话术,基本不涉及代码
Vue 相关
首先,来谈一谈你对Vue的理解吧!
首先来说,Vue是一个渐进式的JS框架,有很多独立的功能库和插件;它还是一个声明式的UI框架,就是当用户使用Vue开发页面时是声明式描述UI的,并提供了一套声明式,组件化的编程模型;它借助了MVVM
设计模式思想,做双向数据绑定,有一套自己的数据响应式的系统。
你刚刚提到了MVVM,能详细说说吗?
MVVM
,它是一种设计思想,全称是Model-View-ViewModel
,其中,Model
表示数据模型层(提供数据),View
表示视图层(渲染数据),ViewModel
视图模型层(调用数据渲染视图),该模式也就相当于是数据双向绑定的雏形,当数据更新,变更视图,当视图变化通知ViewModel
更新数据的这样的一个过程。
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的生命周期(只讲经常听到的)
生命周期钩子在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有哪些组件通信的方式?
我觉得有以下几种通信方式吧:
- 使用
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 区别?
我觉得可以从这几个方面说它们的区别:
- 语义上的区别:
computed
是计算属性,watch
是侦听器。 - 语法上的区别:
const computedObj = computed(() => ref.value++)
、watch(obj or fn, cb(newValue, oldValue, onInvalidate), options)
。 - 功能上的区别:
computed
具有缓存性,并不是每次都会进行计算、watch
不会缓存,每次依赖数据进行读取(get
)操作的时候都会重新执行。 - 原理上的区别:
computed
本质是一个懒执行的副作用函数(effectFn
),也就是对这个computedObj
进行读取(get
)操作的时候,才会触发副作用函数执行;computed
的缓存性就是通过响应式数据发生变化时,会先执行当前依赖函数的scheduler
调度器,在调度器中通过对dirty
变量的值进行改变来判断下次读取(get
)时是否需要重新计算。watch
本质上也是利用了副作用函数执行时的可调度行。但是在调度器中执行的是用户通过watch
函数注册的回调函数。
能讲一下虚拟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模板是如何被编译的
对Vue这个模板的编译主要有三个阶段,分别是:parse
、transform
、generate
;
因为Vue是一个DSL(Domain Specific Language
领域特定语言),所以编译器会对它进行词法分析
和语法分析
,也就是parse
阶段,会生成一个模板AST(Abstract Syntax Tree
抽象语法树),接下来就会进入转换阶段(tranform
),将模板AST转换成JavaScript AST,最后进入generate
,也就是根据JavaScript AST
生成对应的render
函数,这就是整个编译过程(整个过程本质其实就是对字符串的操作)。
说一说性能优化,什么都可以
编码层面的优化,比如,做事件代理,添加key值,异步组件,防抖节流等等。
加载层面优化,按需导入(结合Tree-shaking机制),图片懒加载,路由懒加载。
打包层面优化,比如在Vue3项目中只使用到了Composition API,那我们打包的时候完全没必要保留Option API部分代码,增大打包文件体积,这时候可以使用构建工具设置__VUE__OPTION__API__
这个特性开关变为false
,这样Option API特性就被关闭,打包的时候就会被Tree-Shaking
机制清除掉。
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
等
如果觉得有帮助的话,欢迎点赞+评论啦