前言

记录有关 Vue.js 的相关知识问题,方便复习。

参考博客
2021 高频前端面试汇总之 Vue 篇

Vue 基础

使用Object.defineProperty()来进行数据劫持有什么缺点?

在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,都不能触发组件的重新渲染,因为Object.defineProperty不能拦截到这些操作。

更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。

在 Vue3.0 中已经不使用这种方式了,则是通过使用Proxy对对象进行处理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为Proxy是 ES6 的语法

ComputedMethods的区别

可以将同一个函数定义为一个method或者一个计算属性。对于最终的结果,两种方式是相同的。

不同点:

  • computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;

  • method 调用总会执行该函数。

v-ifv-show的区别

  • 手段:v-if是动态的向 DOM 树内添加或者删除 DOM 元素;v-show是通过设置 DOM 元素的display样式属性控制显示隐藏;

  • 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于 css 切换;

  • 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译;v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,并且 DOM 元素保留;

  • 性能消耗: v-if有更高的切换消耗;v-show有更高的初始渲染消耗。

  • 使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。

data为什么是一个函数而不是对象

JavaScript 中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生改变。

而在 Vue 中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。

所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的 data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。

对 SPA 单页面的理解,它的优缺点分别是什么?

SPA(single-page application)尽在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;

  • 基于上面一点,SPA 相对对服务器压力小;

  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;

  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;

  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

v-for 和 v-if

能说一说双向绑定使用和原理吗?

双向绑定是vue的特色之一

  1. 双向绑定的定义
    双向绑定是一个指令v-model,可以绑定一个响应式数据到视图,同时视图中变化能改变改值.
  2. 双向绑定带来的好处
    使用v-model可以减少大量繁琐的事件处理代码,提高开发效率。
  3. 在哪使用双向绑定
    通常在表单项上使用v-model,还可以在自定义组件(比如自己做的用户交互输入)上使用,表示某个值的输入和输出控制
  4. 使用方式、使用细节、vue3 变化
    原生的表单可以直接使用v-model将值绑定到 value 上,并且还可以结合.lazy,.number,.trim对 v-model 的行为做进一步限定;但是v-model用在自定义组件上会有很大差别,vue3 中它类似于sync修饰符,最终展开的结果是modelValue属性和update:modelValue事件;vue3 中甚至可以用参数形式指定多个不同的绑定,例如v-model:foov-model:bar,非常强大.
  5. 原理实现描述
    我做过测试,输出包含v-model模版的组件渲染函数,发现它会被转换为value属性的绑定以及一个事件监听,事件回调函数中会做相应变量更新操作,说明v-model实际上是 vue 的编译器完成的。

子组件可以直接修改父组件的数据么,说明原因。

实际上能改父组件的数据,但在组件化开发过程中有个单项数据流原则,不在子组件修改父组件是个常识问题。

  1. 讲讲单项数据流原则,表明为何不能这么做
  • 所有的 prop 都使得其父子之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。如果子改父,Vue 就会在浏览器抛出警告。
  1. 举几个常见场景的例子说说解决方案。
  • 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:
1
2
const props = defineProps(["initialCounter"]);
const counter = ref(props.initialCounter);
  • 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
1
2
const props = defineProps(["size"]); // prop变化,计算属性自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase());
  1. 结合实践讲讲如果需要修改父组件状态应该如何做
    实践中如果确实想要改变父组件属性应该 emit 一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的 prop,但是我们还是能够直接改内嵌的对象或属性。

怎么定义动态路由?怎么获取传过来的动态参数?

  1. 什么是动态路由
    很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由。
  2. 什么时候使用动态路由,怎么定义动态路由
    例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router 中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User },其中*:id* 就是路径参数
  3. 参数如何获取
    路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来。
  4. 细节、注意事项
    参数还可以有多个,例如/users/:username/posts/:postId;除了 $route.params 之外,_$route_ 对象还公开了其他有用的信息,如 $route.query$route.hash 等。

key 的作用?

考察对虚拟 DOM 和 patch 细节的掌握程度,能够反映面试者理解层次。

  1. 给出结论,key 的作用是用于优化 patch 性能。
    key 的作用主要是为了更高效的更新虚拟 DOM。
  2. key 的必要性。
    vue 在 patch 过程中判断两个节点是否是相同节点是 key 是一个必要条件,渲染一组列表时,key 往往是唯一标识,所以如果不定义 key 的话,vue 只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个 patch 过程比较低效,影响性能。
  3. 实际使用方式。
    实际使用中在渲染一组列表时 key 必须设置,而且必须是唯一标识,应该避免使用数组索引作为 key,这可能导致一些隐蔽的 bug;vue 中在使用相同标签元素过渡切换时,也会使用 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。
  4. 总结:可以从源码层面描述一下 vue 如何判断两个节点是否相同。
    从源码中可以知道,vue 判断两个节点是否相同时主要判断两者的 key 和元素类型等,因此如果不设置 key,它的值就是 undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的 dom 更新操作,明显是不可取的。

说一下 Vue 子组件和父组件创建和挂载顺序。

  1. 给结论
    创建过程自上而下,挂载过程自下而上;即:
  • parent created
  • child created
  • child mounted
  • parent mounted
  1. 阐述理由
    之所以会这样是因为 Vue 创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加 mounted 钩子到队列,等到 patch 结束再执行它们,可见子组件的 mounted 钩子是先进入到队列中的,因此等到 patch 结束执行这些钩子时也先执行。

你知道哪些 Vue 的最佳实践?

我从编码风格、性能、安全方面说几条:

  1. 编码风格方面。
  • 命名组件时使用“多词”风格避免和 HTML 元素冲突
  • 使用“细节化”方式定义属性而不是只有一个属性名
  • 属性名声明时使用“驼峰命名”,模板或 jsx 中使用“肉串命名”
  • 使用 v-for 时务必加上 key,且不要跟 v-if 写在一起
  1. 性能方面。
  • 路由懒加载减少应用尺寸
  • 利用 SSR 减少首屏加载时间
  • 利用 v-once 渲染那些不需要更新的内容
  • 一些长列表可以利用虚拟滚动技术避免内存过度占用
  • 对于深层嵌套对象的大数组可以使用 shallowRef 或 shallowReactive 降低开销
  • 避免不必要的组件抽象
  1. 安全。
  • 不使用不可信模板,例如使用用户输入拼接模板:template:
    + userProvidedString +
  • 小心使用 v-html,:url,:style 等,避免 html、url、样式等注入攻击

Vue3.0 设计目标是什么,做了哪些优化?

还是问新特性,陈述典型新特性,分析给我们带来的变化。

  1. Vue3 的最大设计目标是替代 Vue2(皮一下),为了实现这一点,Vue3 在以下几个方面做了很大改进,如:易用性、框架性能、扩展性、可维护性、开发体验等
  2. 易用性方面主要是 API 简化,比如 v-model 在 Vue3 中变成了 Vue2 中 v-model 和 sync 修饰符的结合体,用户不用区分两者不同,也不用选择困难。类似的简化还有用于渲染函数内部生成 VNode 的 h(type, props, children),其中 props 不用考虑区分属性、特性、事件等,框架替我们判断,易用性大增。
  3. 开发体验方面,新组件 Teleport 传送门、Fragments 、Suspense 等都会简化特定场景的代码编写,SFC Composition API 语法糖更是极大提升我们开发体验。
  4. 扩展性方面提升如独立的 reactivity 模块,custom renderer API 等
  5. 可维护性方面主要是 Composition API,更容易编写高复用性的业务逻辑。还有对 TypeScript 支持的提升。
  6. 性能方面的改进也很显著,例如编译器优化、改造成了基于 proxy 的响应式

Vue 组件为什么只能有一个根元素?

  • vue2 中组件确实只能有一个根,但 vue3 中组件已经可以多根节点了。
  • 之所以需要这样是因为 vdom 是一颗单根树形结构,patch 方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个 vdom,自然应该满足这个要求。
  • vue3 中之所以可以写多个根节点,是因为引入了 Fragment 的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个 Fragment 节点,把多个根节点作为它的 children。将来 patch 的时候,如果发现是一个 Fragment 节点,则直接遍历 children 创建或更新。

怎么实现路由懒加载呢?

  1. 必要性
    当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段。
  2. 何时用
    一般来说,对所有的路由都使用动态导入是个好主意。
  3. 怎么用
    给 component 选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:
1
{ path: '/users/:id', component: () => import('./views/UserDetails') }
  1. 使用细节
    结合注释() => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')可以做 webpack 代码分块

SPA 和 SSR 的区别是什么?

  1. 两者分析
    SPA(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称 CSR。SSR(Server Side Render)即 服务端渲染。一般也称为 多页面应用(Mulpile Page Application),简称 MPA。
  2. 两者优缺点分析
    SPA 应用只会首次请求 html 文件,后续只需要请求 JSON 数据即可,因此用户体验更好,节约流量,服务端压力也较小。但是首屏加载的时间会变长,而且 SEO 不友好。为了解决以上缺点,就有了 SSR 方案,由于 HTML 内容在服务器一次性生成出来,首屏加载快,搜索引擎也可以很方便的抓取页面信息。但同时 SSR 方案也会有性能,开发受限等问题。
  3. 使用场景差异
    在选择上,如果我们的应用存在首屏加载优化需求,SEO 需求时,就可以考虑 SSR。
  4. 其他选择
    但并不是只有这一种替代方案,比如对一些不常变化的静态网站,SSR 反而浪费资源,我们可以考虑预渲染(prerender)方案,通过插件提前渲染部分 html 内容。另外 nuxt.js/next.js 中给我们提供了 SSG(Static Site Generate)静态网站生成方案也是很好的静态站点解决方案,结合一些 CI 手段,可以起到很好的优化效果,且能节约服务器资源。

你有写过自定义指令吗?自定义指令的应用场景有哪些?

  1. 定义
    Vue 有一组默认指令,比如 v-model 或 v-for,同时 Vue 也允许用户注册自定义指令来扩展 Vue 能力
  2. 何时用
    自定义指令主要完成一些可复用低层级 DOM 操作
  3. 如何用
  • 定义自定义指令有两种方式:对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在 mounted 和 updated 时执行
  • 注册自定义指令类似组件,可以使用app.directive() 全局注册,使用{directives:{xxx}}局部注册
  • 使用时在注册名称前加上 v-即可,比如 v-focus
  1. 常用指令
    还没使用过,如果我使用的话。会把防抖、图片懒加载这种做成一个自定义指令

  2. vue3 变化
    vue3 中指令定义发生了比较大的变化,主要是钩子的名称保持和组件一致,这样开发人员容易记忆,不易犯错。另外在 v3.2 之后,可以在 setup 中以一个小写 v 开头方便的定义自定义指令,更简单了!

v-once 的使用场景有哪些?

  1. v-once是什么
    v-once是 vue 的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。

  2. 什么时候使用
    如果我们有一些元素或者组件在初始化渲染之后不再需要变化,这种情况下适合使用v-once,这样哪怕这些数据变化,vue 也会跳过更新,是一种代码优化手段。

  3. 如何使用
    我们只需要作用的组件或元素上加上 v-once 即可。

  4. 扩展v-memo
    vue3.2 之后,又增加了``指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了。

  5. 探索原理
    编译器发现元素上面有 v-once 时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算。

什么是递归组件?

递归组件用得比较少,但是在TreeMenu这类组件中会被用到

  1. 定义
    如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
  2. 使用场景
    实际开发中类似 Tree、Menu 这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。
  3. 使用细节
    使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件 name 属性,用来查找组件定义,如果使用 SFC,则可以通过 SFC 文件名推断。组件内部通常也要有递归结束条件,比如 model.children 这样的判断。
  4. 原理阐述
    查看生成渲染函数可知,递归组件查找时会传递一个布尔值给 resolveComponent,这样实际获取的组件就是当前组件本身。

说说你对虚拟 DOM 的理解?

  1. vdom 是什么?
    虚拟 dom 顾名思义就是虚拟的 dom 对象,它本身就是一个 JavaScript 对象,只不过它是通过不同的属性去描述一个视图结构。
  2. 引入 vdom 的好处
    将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能
  • 操作 dom 是比较昂贵的操作,频繁的 dom 操作容易引起页面的重绘和回流,但是通过抽象 VNode 进行中间处理,可以有效减少直接操作 dom 的次数,从而减少页面重绘和回流。
    方便实现跨平台
  • 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
  • Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染。
  1. vdom 如何生成,又如何成为 dom
    在 vue 中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler 编译为渲染函数,在接下来的挂载(mount)过程中会调用 render 函数,返回的对象就是虚拟 dom。但它们还不是真正的 dom,所以会在后续的 patch 过程中进一步转化为 dom。

  2. 在后续的 diff 中的作用
    挂载过程结束后,vue 程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新 render,此时就会生成新的 vdom,和上一次的渲染结果 diff 就能得到变化的地方,从而转换为最小量的 dom 操作,高效更新视图。

怎么缓存当前的组件? 缓存后怎么更新?

  1. 缓存用 keep-alive,它的作用与用法
    开发中缓存组件使用 keep-alive 组件,keep-alive 是 vue 内置组件,keep-alive 包裹动态组件 component 时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。

    1
    2
    3
    <keep-alive>
    <component :is="view"></component>
    </keep-alive>
  2. 使用细节,例如缓存指定/排除、结合 router 和 transion
    结合属性 include 和 exclude 可以明确指定缓存哪些组件或排除缓存指定组件。vue3 中结合 vue-router 时变化较大,之前是 keep-alive 包裹 router-view,现在需要反过来用 router-view 包裹 keep-alive:

    1
    2
    3
    4
    5
    <router-view v-slot="{ Component }">
    <keep-alive>
    <component :is="Component"></component>
    </keep-alive>
    </router-view>
  3. 组件缓存后更新可以利用 activated 或者 beforeRouteEnter

  • beforeRouteEnter:在有 vue-router 的项目,每次进入路由的时候,都会执行 beforeRouteEnter
1
2
3
4
5
6
7
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},
  • actived:在 keep-alive 缓存的组件被激活的时候,都会执行 actived 钩子
1
2
3
activated(){
this.getData() // 获取数据
},
  1. 原理阐述
    keep-alive 是一个通用组件,它内部定义了一个 map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的 component 组件对应组件的 vnode,如果该组件在 map 中存在就直接返回它。由于 component 的 is 属性是个响应式数据,因此只要它变化,keep-alive 的 render 函数就会重新执行。