vue是组件化开发框架,所以对于vue应用来说组件间的数据通信非常重要。 此题主要考查大家vue基本功,对于vue基础api运用熟练度。 另外一些边界知识如provide/inject/$attrs则提现了面试者的知识广度。
思路分析:
- 总述知道的所有方式
- 按组件关系阐述使用场景
回答范例
- 组件通信常用方式有以下8种:
- props
- $emit/
$on $children/$parent- $attrs/
$listeners - ref
- $root
- eventbus
- vuex
注意vue3中废弃的几个API
v3-migration.vuejs.org/breaking-ch…
v3-migration.vuejs.org/breaking-ch…
v3-migration.vuejs.org/breaking-ch…
- 根据组件之间关系讨论组件通信最为清晰有效
- 父子组件
- ==props/$emit/$parent/ref/$attrs==
- 兄弟组件
- ==$parent/$root/eventbus/vuex==
- 跨层级关系
- ==eventbus/vuex/provide+inject==
思路分析
- 先给出结论
- 为什么是这样的,说出细节
- 哪些场景可能导致我们这样做,该怎么处理
- 总结,拔高
回答范例
- 实践中不应该把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同时用在同一个元素上,显然这是一个重要的注意事项。
源码中找答案
v2:github1s.com/vuejs/vue/b…
v3:github1s.com/vuejs/core/…
思路分析:
- 给出双绑定义
- 双绑带来的好处
- 在哪使用双绑
- 使用方式、使用细节、vue3变化
- 原理实现描述
回答范例
- vue中双向绑定是一个指令==v-model==,可以绑定一个响应式数据到视图,同时视图中变化能改变该值。
- ==v-model==是语法糖,默认情况下相当于 ==:value== 和 ==@input==。使用 ==v-model== 可以减少大量繁琐的事件处理代码,提高开发效率。
- 通常在表单项上使用 ==v-model==,还可以在自定义组件上使用,表示某个值的输入和输出控制。
- 通过 ==== 的方式将xxx的值绑定到表单元素value上;对于checkbox,可以使用 ==true-value== 和false-value指定特殊的值,对于radio可以使用value指定特殊的值;对于select可以通过options元素的value设置特殊的值;还可以结合.lazy,.number,.trim对v-mode的行为做进一步限定;v-model用在自定义组件上时又会有很大不同,vue3中它类似于==sync==修饰符,最终展开的结果是modelValue属性和update:modelValue事件;vue3中我们甚至可以用参数形式指定多个不同的绑定,例如v-model:foo和v-model:bar,非常强大!
- ==v-model==是一个指令,它的神奇魔法实际上是vue的编译器完成的。我做过测试,包含v-model的模板,转换为渲染函数之后,实际上还是是value属性的绑定以及input事件监听,事件回调函数中会做相应变量更新操作。编译器根据表单元素的不同会展开不同的DOM属性和事件对,比如text类型的input和textarea会展开为value和input事件;checkbox和radio类型的input会展开为checked和change事件;select用value作为属性,用change作为事件。
可能的追问:
- ==v-model==和==sync==修饰符有什么区别
- 自定义组件使用v-model如果想要改变事件名或者属性名应该怎么做
此题属于实践题,考察大家对vue常用api使用熟练度,答题时不仅要列出这些解决方案,同时最好说出他们异同。
答题思路
- 按照逻辑扩展和内容扩展来列举,
- 逻辑扩展有:mixins、extends、composition api;
- 内容扩展有slots;
- 分别说出他们使用方法、场景差异和问题。
- 作为扩展,还可以说说vue3中新引入的composition api带来的变化
回答范例
- 常见的组件扩展方法有:mixins,slots,extends等
- 混入mixins是分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
// 复用代码:它是一个配置对象,选项和组件里面一样
const mymixin = {
methods: {
dosomething(){}
}
}
// 全局混入:将混入对象传入
Vue.mixin(mymixin)
// 局部混入:做数组项设置到mixins选项,仅作用于当前组件
const Comp = {
mixins: [mymixin]
}
- 插槽主要用于vue组件中的内容分发,也可以用于组件扩展。
子组件Child
<div>
<slot>这个内容会被父组件传递的内容替换</slot>
</div>
父组件Parent
<div>
<Child>来自老爹的内容</Child>
</div>
如果要精确分发到不同位置可以使用具名插槽,如果要使用子组件中的数据可以使用作用域插槽。
- 组件选项中还有一个不太常用的选项extends,也可以起到扩展组件的目的
// 扩展对象
const myextends = {
methods: {
dosomething(){}
}
}
// 组件扩展:做数组项设置到extends选项,仅作用于当前组件
// 跟混入的不同是它只能扩展单个对象
// 另外如果和混入发生冲突,该选项优先级较高,优先起作用
const Comp = {
extends: myextends
}
- 混入的数据和方法不能明确判断来源且可能和当前组件内变量产生命名冲突,vue3中引入的composition api,可以很好解决这些问题,利用独立出来的响应式模块可以很方便的编写独立逻辑并提供响应式的数据,然后在setup选项中组合使用,增强代码的可读性和维护性。例如:
// 复用逻辑1
function useXX() {}
// 复用逻辑2
function useYY() {}
// 逻辑组合
const Comp = {
setup() {
const {xx} = useXX()
const {yy} = useYY()
return {xx, yy}
}
}
知其所以然
mixins原理:
github1s.com/vuejs/core/…
github1s.com/vuejs/core/…
slots原理:
github1s.com/vuejs/core/…
github1s.com/vuejs/core/…
github1s.com/vuejs/core/…
分析
综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。
权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。
思路
- 权限管理需求分析:页面和按钮权限
- 权限管理的实现方案:分后端方案和前端方案阐述
- 说说各自的优缺点
回答范例
- 权限管理一般需求是页面权限和按钮权限的管理
- 具体实现的时候分后端和前端两种方案:
- 前端方案会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个==asyncRoutes==数组,需要认证的页面在其路由的==meta==中添加一个==roles==字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过 ==router.addRoutes(accessRoutes)== 方式动态添加路由即可。
- 后端方案会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过==addRoutes==动态添加路由信息
- 按钮权限的控制通常会实现一个指令,例如==v-permission==,将按钮要求角色通过值传给v-permission指令,在指令的==moutned==钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。
- 纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!
知其所以然
路由守卫
github1s.com/PanJiaChen/…
路由生成
github1s.com/PanJiaChen/…
动态追加路由
github1s.com/PanJiaChen/…
分析
现有框架几乎都引入了虚拟 DOM 来对真实 DOM 进行抽象,也就是现在大家所熟知的 VNode 和 VDOM,那么为什么需要引入虚拟 DOM 呢?围绕这个疑问来解答即可!
思路
- vdom是什么
- 引入vdom的好处
- vdom如何生成,又如何成为dom
- 在后续的diff中的作用。
回答范例
- 虚拟dom顾名思义就是虚拟的dom对象,它本身就是一个 ==JavaScript== 对象,只不过它是通过不同的属性去描述一个视图结构。
- 通过引入vdom我们可以获得如下好处:
- 将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能
- 方便实现跨平台
- 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
- Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染。
- vdom如何生成?在vue中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom。
- 挂载过程结束后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图。
知其所以然
vnode定义:
github1s.com/vuejs/core/…
观察渲染函数:21-vdom/test-render-v3.html
创建vnode:
- createElementBlock: github1s.com/vuejs/core/…
- createVnode:
github1s.com/vuejs/core/… - 首次调用时刻:
github1s.com/vuejs/core/…
mount:
github1s.com/vuejs/core/…
调试mount过程:mountComponent
21-vdom/test-render-v3.html
思路
- diff算法是干什么的
- 它的必要性
- 它何时执行
- 具体执行方式
- 拔高:说一下vue3中的优化
回答范例
- Vue中的diff算法称为patching算法,它由Snabbdom修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换。
- 最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOM和patching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新。
- vue中diff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作。
- patch过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3的patch为例:
- 首先判断两个节点是否为相同同类节点,不同则删除重新创建
- 如果双方都是文本则更新文本内容
- 如果双方都是元素节点则递归更新子元素,同时更新元素属性
- 更新子节点时又分了几种情况:
- 新的子节点是文本,老的子节点是数组则清空,并设置文本;
- 新的子节点是文本,老的子节点是文本则直接更新文本;
- 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
- 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节blabla
- vue3中引入的更新策略:编译期优化patchFlags、block等
知其所以然
patch关键代码
github1s.com/vuejs/core/…
调试 test-v3.html
分析
官网列举的最值得注意的新特性:v3-migration.vuejs.org/
也就是下面这些:
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重写
- 编译器优化:静态提升、patchFlags、block等
- 基于Proxy的响应式系统
- 更小:更好的摇树优化
- 更容易维护:TypeScript +模块化
- 更容易扩展
- 独立的响应化模块
- 自定义渲染器
知其所以然
体验编译器优化
sfc.vuejs.org/
reactive实现
github1s.com/vuejs/core/…
思路分析:
首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。
- 借助hash或者history api实现url跳转页面不刷新
- 同时监听hashchange事件或者popstate事件处理跳转
- 根据hash值或者state值从routes表中匹配对应component并渲染之
回答范例:
一个SPA应用的路由需要解决的问题是页面跳转内容改变同时不刷新,同时路由还需要以插件形式存在,所以:
- 首先我会定义一个==createRouter==函数,返回路由器实例,实例内部做几件事:
- 保存用户传入的配置项
- 监听hash或者popstate事件
- 回调里根据path匹配对应路由
- 将router定义成一个Vue插件,即实现install方法,内部做两件事:
- 实现两个全局组件:router-link和router-view,分别实现页面跳转和内容显示
- 定义两个全局变量:$route和$router,组件内可以访问当前路由和路由器实例
知其所以然:
- createRouter如何创建实例
github1s.com/vuejs/route… - 事件监听
github1s.com/vuejs/route… - 页面跳转RouterLink
github1s.com/vuejs/route… - 内容显示RouterView
github1s.com/vuejs/route…
缓存组件使用keep-alive组件,这是一个非常常见且有用的优化手段,vue3中keep-alive有比较大的更新,能说的点比较多。
思路
- 缓存用keep-alive,它的作用与用法
- 使用细节,例如缓存指定/排除、结合router和transition
- 组件缓存后更新可以利用activated或者beforeRouteEnter
- 原理阐述
回答范例
- 开发中缓存组件使用keep-alive组件,keep-alive是vue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
<keep-alive>
<component :is="view"></component>
</keep-alive>
- 结合属性include和exclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是==keep-alive==包裹==router-view==,现在需要反过来用==router-view==包裹==keep-alive==:
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>
-
缓存后如果要获取数据,解决方案可以有以下两种:
- beforeRouteEnter:在有vue-router的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){ next(vm=>{ console.log(vm) // 每次进入路由执行 vm.getData() // 获取数据 }) },
- actived:在keep-alive缓存的组件被激活的时候,都会执行actived钩子
activated(){ this.getData() // 获取数据 },
-
keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于component的is属性是个响应式数据,因此只要它变化,keep-alive的render函数就会重新执行。
知其所以然
KeepAlive定义
github1s.com/vuejs/core/…
缓存定义
github1s.com/vuejs/core/…
缓存组件
github1s.com/vuejs/core/…
获取缓存组件
github1s.com/vuejs/core/…
测试缓存特性,test-v3.html
思路 查看vue官方文档: 风格指南:vuejs.org/style-guide… 性能:vuejs.org/guide/best-… 安全:vuejs.org/guide/best-… 访问性:vuejs.org/guide/best-… 发布:vuejs.org/guide/best-… 回答范例 我从编码风格、性能、安全等方面说几条:
- 编码风格方面:
- 命名组件时使用“多词”风格避免和HTML元素冲突
- 使用“细节化”方式定义属性而不是只有一个属性名
- 属性名声明时使用“驼峰命名”,模板或jsx中使用“肉串命名”
- 使用v-for时务必加上key,且不要跟v-if写在一起
- 性能方面:
- 路由懒加载减少应用尺寸
- 利用SSR减少首屏加载时间
- 利用v-once渲染那些不需要更新的内容
- 一些长列表可以利用虚拟滚动技术避免内存过度占用
- 对于深层嵌套对象的大数组可以使用shallowRef或shallowReactive降低开销
- 避免不必要的组件抽象
- 安全:
- 不使用不可信模板,例如使用用户 输入拼接模板
- 小心使用v-html,:url,:style等,避免html、url、样式等注入
回答范例
- 挂载过程指的是app.mount()过程,这个过程中整体上做了两件事:初始化和建立更新机制
- 初始化会创建组件实例、初始化组件状态,创建各种响应式数据
- 建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行patch将前面获得vnode转换为dom;同时首次执行渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数。
知其所以然
测试代码,test-v3.html
mount函数定义
github1s.com/vuejs/core/…
首次render过程
github1s.com/vuejs/core/…