前端一面经典 vue 面试题总结
一般在哪个生命周期请求异步数据
我们可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
能更快获取到服务端数据,减少页面加载时间,用户体验更好;
SSR 不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。
computed 和 watch 的区别和运用的场景?
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
Vue 中组件生命周期调用顺序说一下
组件的调用顺序都是先父后子
,渲染完成的顺序是先子后父
。
组件的销毁操作是先父后子
,销毁完成的顺序是先子后父
。
加载渲染过程
子组件更新过程
父组件更新过程
销毁过程
defineProperty 和 proxy 的区别
Vue 在实例初始化时遍历 data 中的所有属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。这样当追踪数据发生变化时,setter 会被自动调用。
Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
但是这样做有以下问题:
添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进行响应式处理,只能通过
$set
来调用Object.defineProperty()
处理。无法监控到数组下标和长度的变化。
Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于Object.defineProperty()
,其有以下特点:
Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。
Proxy 可以监听数组的变化。
参考:前端vue面试题详细解答
Vue 模版编译原理知道吗,能简单说一下吗?
简单说,Vue 的编译过程就是将template
转化为render
函数的过程。会经历以下阶段:
生成 AST 树
优化
codegen
首先解析模版,生成AST语法树
(一种用 JavaScript 对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue 的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的 DOM 也不会变化。那么优化过程就是深度遍历 AST 树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码
。
Vue 为什么没有类似于 React 中 shouldComponentUpdate 的生命周期?
考点: Vue 的变化侦测原理
前置知识: 依赖收集、虚拟 DOM、响应式系统
根本原因是 Vue 与 React 的变化侦测方式有所不同
React 是 pull 的方式侦测变化,当 React 知道发生变化后,会使用 Virtual Dom Diff 进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要用 shouldComponentUpdate 进行手动操作来减少 diff,从而提高程序整体的性能.
Vue 是 pull+push 的方式侦测变化的,在一开始就知道那个组件发生了变化,因此在 push 的阶段并不需要手动控制 diff,而组件内部采用的 diff 方式实际上是可以引入类似于 shouldComponentUpdate 相关生命周期的,但是通常合理大小的组件不会有过量的 diff,手动优化的价值有限,因此目前 Vue 并没有考虑引入 shouldComponentUpdate 这种手动优化的生命周期.
父子组件生命周期调用顺序(简单)
渲染顺序:先父后子,完成顺序:先子后父
更新顺序:父更新导致子更新,子更新完成后父
销毁顺序:先父后子,完成顺序:先子后父
vue 的优点
轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb;
简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
双向数据绑定:保留了 angular 的特点,在数据操作方面更为简单;
组件化:保留了 react 的优点,实现了 html 的封装和重用,在构建单页面应用方面有着独特的优势;
视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
虚拟 DOM:dom 操作是非常耗费性能的,不再使用原生的 dom 操作节点,极大解放 dom 操作,但具体操作的还是 dom 不过是换了另一种方式;
运行速度更快:相比较与 react 而言,同样是操作虚拟 dom,就性能而言,vue 存在很大的优势。
v-model 的原理?
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text 和 textarea 元素使用 value 属性和 input 事件;
checkbox 和 radio 使用 checked 属性和 change 事件;
select 字段将 value 作为 prop 并将 change 作为事件。
以 input 表单元素为例:
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?
受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
来实现为对象添加响应式属性,那框架本身是如何实现的呢?
我们查看对应的 Vue 源码:vue/src/core/instance/index.js
我们阅读以上源码可知,vm.$set 的实现原理是:
如果目标是数组,直接使用数组的 splice 方法触发相应式;
如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
Vue3.0 和 2.0 的响应式原理区别
Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。
相关代码如下
怎么缓存当前的组件?缓存后怎么更新
缓存组件使用keep-alive
组件,这是一个非常常见且有用的优化手段,vue3
中keep-alive
有比较大的更新,能说的点比较多
思路
缓存用
keep-alive
,它的作用与用法使用细节,例如缓存指定/排除、结合
router
和transition
组件缓存后更新可以利用
activated
或者beforeRouteEnter
原理阐述
回答范例
开发中缓存组件使用
keep-alive
组件,keep-alive
是vue
内置组件,keep-alive
包裹动态组件component
时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM
结合属性
include
和exclude
可以明确指定缓存哪些组件或排除缓存指定组件。vue3
中结合vue-router
时变化较大,之前是keep-alive
包裹router-view
,现在需要反过来用router-view
包裹keep-alive
缓存后如果要获取数据,解决方案可以有以下两种
beforeRouteEnter
:在有vue-router的
项目,每次进入路由的时候,都会执行beforeRouteEnter
actived
:在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子
keep-alive
是一个通用组件,它内部定义了一个map
,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component
组件对应组件的vnode
,如果该组件在map
中存在就直接返回它。由于component
的is
属性是个响应式数据,因此只要它变化,keep-alive
的render
函数就会重新执行
你知道哪些 Vue3 新特性?
官网列举的最值得注意的新特性:v3-migration.vuejs.org(opens new window)
Composition API
SFC Composition API
语法糖Teleport
传送门Fragments
片段Emits
选项自定义渲染器
SFC CSS
变量Suspense
以上这些是 api 相关,另外还有很多框架特性也不能落掉
回答范例
api
层面Vue3
新特性主要包括:Composition API
、SFC Composition API
语法糖、Teleport
传送门、Fragments
片段、Emits
选项、自定义渲染器、SFC CSS
变量、Suspense
另外,
Vue3.0
在框架层面也有很多亮眼的改进:
更快
虚拟
DOM
重写,diff
算法优化编译器优化:静态提升、
patchFlags(静态标记)
、事件监听缓存基于
Proxy
的响应式系统SSR
优化更小 :更好的摇树优化
tree shaking
、Vue3
移除一些不常用的API
更友好 :
vue3
在兼顾vue2
的options API
的同时还推出了composition API
,大大增加了代码的逻辑组织和代码复用能力更容易维护 :
TypeScript
+ 模块化更容易扩展
独立的响应化模块
自定义渲染器
说说 Vue 的生命周期吧
什么时候被调用?
beforeCreate :实例初始化之后,数据观测之前调用
created:实例创建万之后调用。实例完成:数据观测、属性和方法的运算、
watch/event
事件回调。无$el
.beforeMount:在挂载之前调用,相关
render
函数首次被调用mounted:了被新创建的
vm.$el
替换,并挂载到实例上去之后调用改钩子。beforeUpdate:数据更新前调用,发生在虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。
updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。
beforeDestroy:实例销毁前调用,实例仍然可用。
destroyed:实例销毁之后调用,调用后,Vue 实例指示的所有东西都会解绑,所有事件监听器和所有子实例都会被移除
每个生命周期内部可以做什么?
created:实例已经创建完成,因为他是最早触发的,所以可以进行一些数据、资源的请求。
mounted:实例已经挂载完成,可以进行一些 DOM 操作。
beforeUpdate:可以在这个钩子中进一步的更改状态,不会触发重渲染。
updated:可以执行依赖于 DOM 的操作,但是要避免更改状态,可能会导致更新无线循环。
destroyed:可以执行一些优化操作,清空计时器,解除绑定事件。
ajax 放在哪个生命周期?:一般放在 mounted
中,保证逻辑统一性,因为生命周期是同步执行的, ajax
是异步执行的。单数服务端渲染 ssr
同一放在 created
中,因为服务端渲染不支持 mounted
方法。 什么时候使用 beforeDestroy?:当前页面使用 $on
,需要解绑事件。清楚定时器。解除事件绑定, scroll mousemove
。
vue-router 守卫
导航守卫
router.beforeEach
全局前置守卫
to: Route
: 即将要进入的目标(路由对象)from: Route
: 当前导航正要离开的路由next: Function
: 一定要调用该方法来resolve
这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)执行效果依赖 next 方法的调用参数。
next()
: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。next(false)
:取消进入路由,url 地址重置为 from 路由地址(也就是将要离开的路由地址)
路由独享的守卫 你可以在路由配置上直接定义
beforeEnter
守卫
组件内的守卫你可以在路由组件内直接定义以下路由导航守卫
谈一下对 vuex 的个人理解
vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue)
主要包括以下几个模块:
State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
Vue 要做权限管理该怎么做?控制到按钮级别的权限怎么做?
分析
综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。
权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。
思路
权限管理需求分析:页面和按钮权限
权限管理的实现方案:分后端方案和前端方案阐述
说说各自的优缺点
回答范例
权限管理一般需求是页面权限和按钮权限的管理
具体实现的时候分后端和前端两种方案:
前端方案 会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个
asyncRoutes
数组,需要认证的页面在其路由的meta
中添加一个roles
字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)
方式动态添加路由即可后端方案 会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过
addRoutes
动态添加路由信息按钮权限的控制通常会
实现一个指令
,例如v-permission
,将按钮要求角色通过值传给 v-permission
指令,在指令的moutned
钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮
纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!
可能的追问
类似
Tabs
这类组件能不能使用v-permission
指令实现按钮权限控制?
服务端返回的路由信息如何添加到路由器中?
Watch 中的 deep:true 是如何实现的
当用户指定了
watch
中的 deep 属性为true
时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前watcher
存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新
源码相关
Vue 模版编译原理知道吗,能简单说一下吗?
简单说,Vue 的编译过程就是将template
转化为render
函数的过程。会经历以下阶段:
生成 AST 树
优化
codegen
首先解析模版,生成AST语法树
(一种用 JavaScript 对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue 的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的 DOM 也不会变化。那么优化过程就是深度遍历 AST 树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码
。
computed 的实现原理
computed 本质是一个惰性求值的观察者。
computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。
其内部通过 this.dirty 属性标记计算属性是否需要重新求值。
当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,
computed watcher 通过 this.dep.subs.length 判断有没有订阅者,
有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)
没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
评论