20 道高频 vue 面试题自测
delete 和 Vue.delete 删除数组的区别?
delete
只是被删除的元素变成了empty/undefined
其他的元素的键值还是不变。Vue.delete
直接删除了数组 改变了数组的键值。
Vue 路由 hash 模式和 history 模式
1. hash
模式
早期的前端路由的实现就是基于 location.hash
来实现的。其实现原理很简单,location.hash
的值就是 URL
中 #
后面的内容。比如下面这个网站,它的 location.hash
的值为 '#search'
hash 路由模式的实现主要是基于下面几个特性
URL
中hash
值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash
部分不会被发送;hash
值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash
的切换;可以通过
a
标签,并设置href
属性,当用户点击这个标签后,URL
的hash
值会发生改变;或者使用JavaScript
来对loaction.hash
进行赋值,改变URL
的hash
值;我们可以使用
hashchange
事件来监听hash
值的变化,从而对页面进行跳转(渲染)
每一次改变 hash
(window.location.hash
),都会在浏览器的访问历史中增加一个记录利用 hash
的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了
特点 :兼容性好但是不美观
2. history
模式
history
采用HTML5
的新特性;且提供了两个新方法: pushState()
, replaceState()
可以对浏览器历史记录栈进行修改,以及popState
事件的监听到状态变更
这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL
改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
history 路由模式的实现主要基于存在下面几个特性:
pushState
和repalceState
两个API
来操作实现URL
的变化 ;我们可以使用
popstate
事件来监听url
的变化,从而对页面进行跳转(渲染);history.pushState()
或history.replaceState()
不会触发popstate
事件,这时我们需要手动触发页面跳转(渲染)。
特点 :虽然美观,但是刷新会出现 404
需要后端进行配置
Vue 中修饰符.sync 与 v-model 的区别
sync
的作用
.sync
修饰符可以实现父子组件之间的双向绑定,并且可以实现子组件同步修改父组件的值,相比较与v-model
来说,sync
修饰符就简单很多了一个组件上可以有多个
.sync
修饰符
v-model
的工作原理
相同点
都是语法糖,都可以实现父子组件中的数据的双向通信
区别点
格式不同:
v-model="num"
,:num.sync="num"
v-model
:@input + value
:num.sync
:@update:num
v-model
只能用一次;.sync
可以有多个
keep-alive 使用场景和原理
keep-alive
是Vue
内置的一个组件, 可以实现组件缓存 ,当组件切换时不会对当前组件进行卸载。 一般结合路由和动态组件一起使用 ,用于缓存组件提供
include
和exclude
属性, 允许组件有条件的进行缓存 。两者都支持字符串或正则表达式,include
表示只有名称匹配的组件会被缓存,exclude
表示任何名称匹配的组件都不会被缓存 ,其中exclude
的优先级比include
高对应两个钩子函数
activated
和deactivated
,当组件被激活时,触发钩子函数activated
,当组件被移除时,触发钩子函数deactivated
keep-alive
的中还运用了LRU
(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰
<keep-alive></keep-alive>
包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染比如有一个列表和一个详情,那么用户就会经常执行打开详情=>返回列表=>打开详情…这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用
<keep-alive></keep-alive>
进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染
关于 keep-alive 的基本用法
使用includes
和exclude
:
匹配首先检查组件自身的 name
选项,如果 name
选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值),匿名组件不能被匹配
设置了 keep-alive
缓存的组件,会多出两个生命周期钩子(activated
与deactivated
):
首次进入组件时:
beforeRouteEnter
>beforeCreate
>created
>mounted
>activated
> ... ... >beforeRouteLeave
>deactivated
再次进入组件时:
beforeRouteEnter
>activated
> ... ... >beforeRouteLeave
>deactivated
使用场景
使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive
举个栗子:
当我们从首页
–>列表页
–>商详页
–>再返回
,这时候列表页应该是需要keep-alive
从首页
–>列表页
–>商详页
–>返回到列表页(需要缓存)
–>返回到首页(需要缓存)
–>再次进入列表页(不需要缓存)
,这时候可以按需来控制页面的keep-alive
在路由中设置keepAlive
属性判断是否需要缓存
使用<keep-alive>
思考题:缓存后如何获取数据
解决方案可以有以下两种:
beforeRouteEnter
:每次组件渲染的时候,都会执行beforeRouteEnter
actived
:在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子
扩展补充:LRU 算法是什么?
LRU
的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件key
重新插入到this.keys
的尾部,这样一来,this.keys
中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即this.keys
中第一个缓存的组件
相关代码
keep-alive
是vue
中内置的一个组件
源码位置:src/core/components/keep-alive.js
可以看到该组件没有template
,而是用了render
,在组件渲染的时候会自动执行render
函数
this.cache
是一个对象,用来存储需要缓存的组件,它将以如下形式存储:
在组件销毁的时候执行pruneCacheEntry
函数
在mounted
钩子函数中观测 include
和 exclude
的变化,如下:
如果include
或exclude
发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache
函数,函数如下
在该函数内对this.cache
对象进行遍历,取出每一项的name
值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry
函数将其从this.cache
对象剔除即可
关于keep-alive
的最强大缓存功能是在render
函数中实现
首先获取组件的key
值:
拿到key
值后去this.cache
对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存,如下:
直接从缓存中拿 vnode
的组件实例,此时重新调整该组件key
的顺序,将其从原来的地方删掉并重新放在this.keys
中最后一个
this.cache
对象中没有该key
值的情况,如下:
表明该组件还没有被缓存过,则以该组件的key
为键,组件vnode
为值,将其存入this.cache
中,并且把key
存入this.keys
中
此时再判断this.keys
中缓存组件的数量是否超过了设置的最大缓存数量值this.max
,如果超过了,则把第一个缓存组件删掉
谈一谈对 Vue 组件化的理解
组件化开发能大幅提高开发效率、测试性、复用性等
常用的组件化技术:属性、自定义事件、插槽
降低更新频率,只重新渲染变化的组件
组件的特点:高内聚、低耦合、单向数据流
Watch 中的 deep:true 是如何实现的
当用户指定了
watch
中的 deep 属性为true
时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前watcher
存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新
源码相关
双向绑定的原理是什么
我们都知道 Vue
是数据双向绑定的框架,双向绑定由三个重要部分构成
数据层(Model):应用的数据及业务逻辑
视图层(View):应用的展示效果,各类 UI 组件
业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM
这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理
理解 ViewModel
它的主要职责就是:
数据变化后更新视图
视图变化后更新数据
当然,它还有两个主要部分组成
监听器(
Observer
):对所有数据的属性进行监听解析器(
Compiler
):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
Vue-router 基本使用
mode
hash
history
跳转
编程式(js 跳转)
this.$router.push('/')
声明式(标签跳转)
<router-link to=""></router-link>
vue 路由传参数
使用
query
方法传入的参数使用this.$route.query
接受使用
params
方式传入的参数使用this.$route.params
接受
占位
为什么要使用异步组件
节省打包出的结果,异步组件分开打包,采用
jsonp
的方式进行加载,有效解决文件过大的问题。核心就是包组件定义变成一个函数,依赖
import()
语法,可以实现文件的分割加载。
原理
组件中写 name 属性的好处
可以标识组件的具体名称方便调试和查找对应属性
Vue computed 实现
建立与其他属性(如:
data
、Store
)的联系;属性改变后,通知计算属性重新计算
实现时,主要如下
初始化
data
, 使用Object.defineProperty
把这些属性全部转为getter/setter
。初始化
computed
, 遍历computed
里的每个属性,每个computed
属性都是一个watch
实例。每个属性提供的函数作为属性的getter
,使用Object.defineProperty
转化。Object.defineProperty getter
依赖收集。用于依赖发生变化时,触发属性重新计算。若出现当前
computed
计算属性嵌套其他computed
计算属性时,先进行其他的依赖收集
怎么缓存当前的组件?缓存后怎么更新
缓存组件使用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
函数就会重新执行
Vue.extend 作用和原理
官方解释:
Vue.extend
使用基础Vue
构造器,创建一个“子类”。参数是一个包含组件选项的对象。
其实就是一个子类构造器 是 Vue
组件的核心 api
实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions
把传入组件的 options
和父类的 options
进行了合并
extend
是构造一个组件的语法器。然后这个组件你可以作用到Vue.component
这个全局注册方法里还可以在任意vue
模板里使用组件。 也可以作用到vue
实例或者某个组件中的components
属性中并在内部使用apple
组件。Vue.component
你可以创建 ,也可以取组件。
相关代码如下
谈一下对 vuex 的个人理解
vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue)
主要包括以下几个模块:
State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
请说明 Vue 中 key 的作用和原理,谈谈你对它的理解
key
是为Vue
中的VNode
标记的唯一id
,在patch
过程中通过key
可以判断两个虚拟节点是否是相同节点,通过这个key
,我们的diff
操作可以更准确、更快速diff
算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key
与旧节点进行比对,然后检出差异尽量不要采用索引作为
key
如果不加
key
,那么vue
会选择复用节点(Vue 的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug
更准确 :因为带
key
就不是就地复用了,在sameNode
函数a.key === b.key
对比中可以避免就地复用的情况。所以会更加准确。更快速 :
key
的唯一性可以被Map
数据结构充分利用,相比于遍历查找的时间复杂度O(n)
,Map
的时间复杂度仅仅为O(1)
,比遍历方式更快。
源码如下:
回答范例
分析
这是一道特别常见的问题,主要考查大家对虚拟DOM
和patch
细节的掌握程度,能够反映面试者理解层次
思路分析:
给出结论,
key
的作用是用于优化patch
性能key
的必要性实际使用方式
总结:可从源码层面描述一下
vue
如何判断两个节点是否相同
回答范例:
key
的作用主要是为了更高效的更新虚拟DOM
vue
在patch
过程中 判断两个节点是否是相同节点是key
是一个必要条件 ,渲染一组列表时,key
往往是唯一标识,所以如果不定义key
的话,vue
只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch
过程比较低效,影响性能实际使用中在渲染一组列表时
key
必须设置,而且必须是唯一标识,应该避免使用数组索引作为key
,这可能导致一些隐蔽的bug
;vue
中在使用相同标签元素过渡切换时,也会使用key
属性,其目的也是为了让vue
可以区分它们,否则vue
只会替换其内部属性而不会触发过渡效果从源码中可以知道,
vue
判断两个节点是否相同时主要判断两者的key
和标签类型(如div)
等,因此如果不设置key
,它的值就是undefined
,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom
更新操作,明显是不可取的
如果不使用
key
,Vue
会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key
是为Vue
中vnode
的唯一标记,通过这个key
,我们的diff
操作可以更准确、更快速
diff 程可以概括为:
oldCh
和newCh
各有两个头尾的变量 StartIdx
和EndIdx
,它们的2
个变量相互比较,一共有4
种比较方式。如果4
种比较都没匹配,如果设置了key
,就会用key
进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx
表明oldCh
和newCh
至少有一个已经遍历完了,就会结束比较,这四种比较方式就是首
、尾
、旧尾新头
、旧头新尾
相关代码如下
如何从真实 DOM 到虚拟 DOM
涉及到 Vue 中的模板编译原理,主要过程:
将模板转换成
ast
树,ast
用对象来描述真实的 JS 语法(将真实 DOM 转换成虚拟 DOM)优化树
将
ast
树生成代码
为什么要用 Vuex 或者 Redux
由于传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致代码无法维护。
所以需要把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,组件树构成了一个巨大的"视图",不管在树的哪个位置,任何组件都能获取状态或者触发行为。
另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,代码将会变得更结构化且易维护。
虚拟 DOM 的优劣如何?
优点:
保证性能下限: 虚拟 DOM 可以经过 diff 找出最小差异,然后批量进行 patch,这种操作虽然比不上手动优化,但是比起粗暴的 DOM 操作性能要好很多,因此虚拟 DOM 可以保证性能下限
无需手动操作 DOM: 虚拟 DOM 的 diff 和 patch 都是在一次更新中自动进行的,我们无需手动操作 DOM,极大提高开发效率
跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等
缺点:
无法进行极致优化: 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化,比如 VScode 采用直接手动操作 DOM 的方式进行极端的性能优化
diff 算法
时间复杂度: 个树的完全 diff
算法是一个时间复杂度为 O(n*3)
,vue 进行优化转化成 O(n)
。
理解:
最小量更新,
key
很重要。这个可以是这个节点的唯一标识,告诉diff
算法,在更改前后它们是同一个 DOM 节点扩展
v-for
为什么要有key
,没有key
会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改 DOM),加key
只会移动减少操作 DOM。只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。
只进行同层比较,不会进行跨层比较。
diff 算法的优化策略:四种命中查找,四个指针
旧前与新前(先比开头,后插入和删除节点的这种情况)
旧后与新后(比结尾,前插入或删除的情况)
旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)
旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)
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 这种手动优化的生命周期.
评论