前端 vue 面试题(持续更新中)
Watch 中的 deep:true 是如何实现的
当用户指定了
watch
中的 deep 属性为true
时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前watcher
存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新
源码相关
什么是作用域插槽
插槽
创建组件虚拟节点时,会将组件儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类
{a:[vnode],b[vnode]}
渲染组件时会拿对应的
slot
属性的节点进行替换操作。(插槽的作用域为父组件)
作用域插槽
作用域插槽在解析的时候不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。(插槽的作用域为子组件)
普通插槽渲染的作用域是父组件,作用域插槽的渲染作用域是当前子组件。
router-link 和 router-view 是如何起作用的
分析
vue-router
中两个重要组件router-link
和router-view
,分别起到导航作用和内容渲染作用,但是回答如何生效还真有一定难度
回答范例
vue-router
中两个重要组件router-link
和router-view
,分别起到路由导航作用和组件内容渲染作用使用中
router-link
默认生成一个a
标签,设置to
属性定义跳转path
。实际上也可以通过custom
和插槽自定义最终的展现形式。router-view
是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name
可以显示具名组件,起到更强的布局作用。router-link
组件内部根据custom
属性判断如何渲染最终生成节点,内部提供导航方法navigate
,用户点击之后实际调用的是该方法,此方法最终会修改响应式的路由变量,然后重新去routes
匹配出数组结果,router-view
则根据其所处深度deep
在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。
双向绑定的原理是什么
我们都知道 Vue
是数据双向绑定的框架,双向绑定由三个重要部分构成
数据层(Model):应用的数据及业务逻辑
视图层(View):应用的展示效果,各类 UI 组件
业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM
这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理
理解 ViewModel
它的主要职责就是:
数据变化后更新视图
视图变化后更新数据
当然,它还有两个主要部分组成
监听器(
Observer
):对所有数据的属性进行监听解析器(
Compiler
):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
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
可以有多个
实现双向绑定
我们还是以Vue
为例,先来看看Vue
中的双向绑定流程是什么的
new Vue()
首先执行初始化,对data
执行响应化处理,这个过程发生Observe
中同时对模板执行编译,找到其中动态绑定的数据,从
data
中获取并初始化视图,这个过程发生在Compile
中同时定义⼀个更新函数和
Watcher
,将来对应数据变化时Watcher
会调用更新函数由于
data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep
来管理多个Watcher
将来 data 中数据⼀旦发生变化,会首先找到对应的
Dep
,通知所有Watcher
执行更新函数
流程图如下:
先来一个构造函数:执行初始化,对data
执行响应化处理
对data
选项执行响应化具体操作
编译Compile
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
依赖收集
视图中会用到data
中某key
,这称为依赖。同⼀个key
可能出现多次,每次都需要收集出来用⼀个Watcher
来维护它们,此过程称为依赖收集多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知
实现思路
defineReactive
时为每⼀个key
创建⼀个Dep
实例初始化视图时读取某个
key
,例如name1
,创建⼀个watcher1
由于触发
name1
的getter
方法,便将watcher1
添加到name1
对应的Dep
中当
name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新
声明Dep
创建watcher
时触发getter
依赖收集,创建Dep
实例
参考 前端进阶面试题详细解答
Vue 要做权限管理该怎么做?控制到按钮级别的权限怎么做?
分析
综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。
权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。
思路
权限管理需求分析:页面和按钮权限
权限管理的实现方案:分后端方案和前端方案阐述
说说各自的优缺点
回答范例
权限管理一般需求是页面权限和按钮权限的管理
具体实现的时候分后端和前端两种方案:
前端方案 会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个
asyncRoutes
数组,需要认证的页面在其路由的meta
中添加一个roles
字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)
方式动态添加路由即可后端方案 会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过
addRoutes
动态添加路由信息按钮权限的控制通常会
实现一个指令
,例如v-permission
,将按钮要求角色通过值传给 v-permission
指令,在指令的moutned
钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮
纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!
可能的追问
类似
Tabs
这类组件能不能使用v-permission
指令实现按钮权限控制?
服务端返回的路由信息如何添加到路由器中?
怎样理解 Vue 的单向数据流
数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会 防止从子组件意外改变父级组件的状态 ,从而导致你的应用的数据流向难以理解
注意 :在子组件直接用 v-model
绑定父组件传过来的 prop
这样是不规范的写法 开发环境会报警告
如果实在要改变父组件的 prop
值,可以在 data
里面定义一个变量 并用 prop
的值初始化它 之后用$emit
通知父组件去修改
有两种常见的试图改变一个 prop 的情形 :
这个
prop
用来传递一个初始值;这个子组件接下来希望将其作为一个本地的prop
数据来使用。 在这种情况下,最好定义一个本地的data
属性并将这个prop
用作其初始值
这个
prop
以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个prop
的值来定义一个计算属性
了解 history 有哪些方法吗?说下它们的区别
history
这个对象在html5
的时候新加入两个api
history.pushState()
和history.repalceState()
这两个API
可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。
从参数上来说:
另外还有:
window.history.back()
后退window.history.forward()
前进window.history.go(1)
前进或者后退几步
从触发事件的监听上来说:
pushState()
和replaceState()
不能被popstate
事件所监听而后面三者可以,且用户点击浏览器前进后退键时也可以
Vue 组件渲染和更新过程
渲染组件时,会通过
Vue.extend
方法构建子组件的构造函数,并进行实例化。最终手动调用$mount()
进行挂载。更新组件时会进行patchVnode
流程,核心就是diff
算法
v-if 和 v-show 区别
v-show
隐藏则是为该元素添加css--display:none
,dom
元素依旧还在。v-if
显示隐藏是将dom
元素整个添加或删除编译过程:
v-if
切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show
只是简单的基于css
切换编译条件:
v-if
是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染v-show
由false
变为true
的时候不会触发组件的生命周期v-if
由false
变为true
的时候,触发组件的beforeCreate
、create
、beforeMount
、mounted
钩子,由true
变为false
的时候触发组件的beforeDestory
、destoryed
方法性能消耗:
v-if
有更高的切换消耗;v-show
有更高的初始渲染消耗
v-show 与 v-if 的使用场景
v-if
与v-show
都能控制dom
元素在页面的显示v-if
相比v-show
开销更大的(直接操作dom节
点增加与删除)如果需要非常频繁地切换,则使用 v-show 较好
如果在运行时条件很少改变,则使用
v-if
较好
v-show 与 v-if 原理分析
v-show
原理
不管初始条件是什么,元素总是会被渲染
我们看一下在 vue 中是如何实现的
代码很好理解,有transition
就执行transition
,没有就直接设置display
属性
v-if
原理
v-if
在实现上比v-show
要复杂的多,因为还有else
else-if
等条件需要处理,这里我们也只摘抄源码中处理 v-if
的一小部分
返回一个node
节点,render
函数通过表达式的值来决定是否生成DOM
说说 vue 内置指令
$route
和$router
的区别
$route
是“路由信息对象”,包括path
,params
,hash
,query
,fullPath
,matched
,name
等路由信息参数。而
$router
是“路由实例”对象包括了路由的跳转方法,钩子函数等
v-if 和 v-for 哪个优先级更高
实践中不应该把
v-for
和v-if
放一起在
vue2
中,v-for
的优先级是高于v-if
,把它们放在一起,输出的渲染函数中可以看出会先执行循环再判断条件,哪怕我们只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表,这会比较浪费;另外需要注意的是在vue3
中则完全相反,v-if
的优先级高于v-for
,所以v-if
执行时,它调用的变量还不存在,就会导致异常通常有两种情况下导致我们这样做:
为了过滤列表中的项目 (比如
v-for="user in users" v-if="user.isActive"
)。此时定义一个计算属性 (比如activeUsers
),让其返回过滤后的列表即可(比如users.filter(u=>u.isActive)
)为了避免渲染本应该被隐藏的列表 (比如
v-for="user in users" v-if="shouldShowUsers"
)。此时把v-if
移动至容器元素上 (比如ul
、ol
)或者外面包一层template
即可文档中明确指出永远不要把
v-if
和v-for
同时用在同一个元素上,显然这是一个重要的注意事项源码里面关于代码生成的部分,能够清晰的看到是先处理
v-if
还是v-for
,顺序上vue2
和vue3
正好相反,因此产生了一些症状的不同,但是不管怎样都是不能把它们写在一起的
vue2.x 源码分析
在 vue 模板编译的时候,会将指令系统转化成可执行的
render
函数
编写一个p
标签,同时使用v-if
与 v-for
创建vue
实例,存放isShow
与items
数据
模板指令的代码都会生成在render
函数中,通过app.$options.render
就能得到渲染函数
_l
是vue
的列表渲染函数,函数内部都会进行一次if
判断初步得到结论:
v-for
优先级是比v-i
f 高再将
v-for
与v-if
置于不同标签
再输出下render
函数
这时候我们可以看到,v-for
与v-if
作用在不同标签时候,是先进行判断,再进行列表的渲染
我们再在查看下 vue 源码
源码位置:\vue-dev\src\compiler\codegen\index.js
在进行if
判断的时候,v-for
是比v-if
先进行判断
最终结论:v-for
优先级比v-if
高
什么是递归组件?举个例子说明下?
分析
递归组件我们用的比较少,但是在Tree
、Menu
这类组件中会被用到。
体验
组件通过组件名称引用它自己,这种情况就是递归组件
回答范例
如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
实际开发中类似
Tree
、Menu
这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件
name
属性,用来查找组件定义,如果使用SFC
,则可以通过SFC
文件名推断。组件内部通常也要有递归结束条件,比如model.children
这样的判断。查看生成渲染函数可知,递归组件查找时会传递一个布尔值给
resolveComponent
,这样实际获取的组件就是当前组件本身
原理
递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)
就是在传递maybeSelfReference
resolveAsset
中最终返回的是组件自身:
说说 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 中 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
至少有一个已经遍历完了,就会结束比较,这四种比较方式就是首
、尾
、旧尾新头
、旧头新尾
相关代码如下
Vue computed 实现
建立与其他属性(如:
data
、Store
)的联系;属性改变后,通知计算属性重新计算
实现时,主要如下
初始化
data
, 使用Object.defineProperty
把这些属性全部转为getter/setter
。初始化
computed
, 遍历computed
里的每个属性,每个computed
属性都是一个watch
实例。每个属性提供的函数作为属性的getter
,使用Object.defineProperty
转化。Object.defineProperty getter
依赖收集。用于依赖发生变化时,触发属性重新计算。若出现当前
computed
计算属性嵌套其他computed
计算属性时,先进行其他的依赖收集
既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 检测差异
响应式数据变化,
Vue
确实可以在数据变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher
用于更新的话,会产生大量的watcher
从而降低性能而且粒度过细也得导致更新不准确的问题,所以
vue
采用了组件级的watcher
配合diff
来检测差异
生命周期钩子是如何实现的
Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)
相关代码如下
评论