Vue3 面试题

432 阅读20分钟

1. v-html 会导致什么问题

v-html的内容会作为普通的 HTML 插入。但在网站上动态渲染任意 HTML 是不安全的,容易导致 XSS 攻击,所以只能在可信内容上使用该指令。

2. 响应式 API:ref 和 reactive

  1. reactive方法仅对对象类型(对象、数组、Map、Set等)有效,而对基本类型无效。这是因为 reactive 内部的 Proxy 不适用基本类型。而ref可以对任何类型值做响应式处理(当值为对象类型时,会用reactive自动转换它的value属性)。
  2. 数据访问方式:访问 ref 数据时需要以.value属性的形式(模板中会解包),reactive 数据直接访问即可。
  3. 响应性丢失:当响应式对象的属性赋值给其他变量或解构时,ref 可以不让它失去响应性;而 reactive 则会丢失响应性。
let state = reactive({ count: 0 });
let n = state.count; // 赋值,此时 n 不具有响应性
let { count } = state; // 解构,此时 count 不具有响应性

const obj = { // 此时 ref 用于普通对象
    foo: ref(1),
    bar: ref(2)
} 
const { foo, bar } = obj;

日常开发时全用 ref!!ref 的 value 属性是响应式的,reactive 返回的 proxy 对象是响应式的。

3. shallowReactive、shallowRef、toRefs 函数

Vue 中的状态都是默认深层响应式的。如果想要对象仅在顶层具有响应性,则:

  • shallowReactive不会对对象进行深层响应式转换,只有根级别的属性才是响应式的。
  • shallowRef不会对内部值进行深层响应式转换,只有对.value的访问是响应式的
  • shallowReadonly的对象中只有根级别的属性变成了只读的。

toRefs():toRefs会将一个响应式对象转换为一个普通对象,并且这个普通对象的每个属性都是源对象中属性的 ref,每个 ref 都是使用toRef创建的。

应用场景:toRefs解决了响应式对象在解构/展开时的响应性丢失问题。

4. readonly 和 const 的区别

readonly返回一个对象的只读 Proxy,且内部的属性也都是只读的。

  • const 可以用于基本类型和引用类型。当作用于引用类型时,属性可以改变,对象引用不变即可。
  • readonly 用于对象属性,不能给属性重新赋值。

5. watch 和 watchEffect 的区别

监听数据并在数据变化时执行回调函数,通常是一些副作用,如更改 DOM、发送网络请求等。

  1. watch默认是懒监听的,只在数据变化时才执行回调;而watchEffect初始化时就会执行一次(watch 传参 immediate 也可实现)。
  2. watch可以添加需要监听的数据,因此能更加明确是由哪个状态触发的回调;而watchEffect是在执行回调时自动分析出的监听数据源。
  3. watch可以访问所监听数据的旧值和新值,而watchEffect访问不到旧值
  • watch 的参数:
    • 第一个参数是监听的数据源;
    • 第二个参数是回调函数,接收新值、旧值和清理副作用的回调;
    • 第三个参数支持 immediate:true、deep:true 等选项。
  • watchEffect 的参数
    • 第一个参数是回调函数;
    • 第二个参数可以改变回调的触发时机等。

通常无脑 watch。

6. watch 和 computed 的区别

watch是侦听属性,每当监听的数据变化时都会执行回调。

computed是计算属性,它接收一个 getter 函数然后默认返回一个只读ref。另外,计算属性会自动追踪其响应式依赖(遇到非响应式的依赖不会重新计算)。


  • 缓存性computed会返回一个只读的ref,只有当它的响应式依赖变化时才会重新计算watch没有缓存性(或者说没有这种说法)。
  • 设计思想/应用场景computed适合“一个数据依赖于其他数据、将多个state合成一个新的state”的情况,比如计算逻辑;watch适合“当数据变化时需要做一些事情”,比如数据请求、更新DOM等业务逻辑。(管道思想和观察者思路)
  • 支持异步computed不支持异步;watch支持。

7. 响应式原理

(1)Vue2 的响应式原理:

Vue2 通过Object.defineProperty来劫持监听对象的所有属性,当读取属性时会触发getter进行依赖收集,修改属性时会触发setter通知相关依赖进行更新操作。

(2)Vue3 的响应式原理:

Vue3 使用Proxy代理目标对象。具体来说,通过reactive()来创建响应式数据,reactive 的返回值是一个 Proxy 对象。当访问 Proxy 对象时会触发get方法调用track()来收集依赖;当修改 Proxy 对象时会触发set方法调用trigger()来执行相应的依赖。

为什么需要 Reflect?

  • Reflect 和 Proxy 的方法是一一对应的。这可以让Proxy对象搭配Reflect负责完成对目标对象的原生行为。
  • 修改Proxy/Object对象某些方法的返回值,以满足要求。

(3)Proxy 和 Object.defineProperty 对比

  1. Proxy 是代理整个对象,而 O.d 只能代理某个属性;
  2. O.d 监听不到对象上的新增属性、删除属性行为;
  3. O.d 监听不到数组的变化,只能通过劫持重写几个数组方法(性能代价,length 属性不能被删除、修改);
  4. O.d 不支持 Map、Set 这些新增的数据结构;
  5. Proxy 有13种拦截方法,如 has、ownKeys等;
  6. Proxy 兼容性较差,IE 不支持。

8. v-model 双向绑定原理

双向绑定:响应式数据到视图,同时视图的变化能改变该数据。

v-model 可以在组件上使用以实现双向绑定,本质是一个语法糖。比如:

  • 将内部原生<input>元素的value属性绑定到modelValue属性上。
  • 当监听到原生的input事件触发时,会去触发自定义事件update:modelValue
<CustomInput
    :modelValue="searchText"
    @update:modelValue="newVal => searchText = newVal"
/>

9. v-if 和 v-show 的区别

  1. v-if会真实的销毁、重建 dom,而v-show只是改变display属性值(visibility 和 none)。
  2. 初始渲染条件为 false 时,v-show 会将元素先渲染出来,v-if 不会。
  3. 应用场景:由于 v-if 有更高的切换开销,v-show 有更高的初始渲染开销。所以当判断条件会频繁改变(tab页签)时推荐使用 v-show;条件很少改变(根据用户权限显示不同内容)推荐使用 v-if。
  4. v-show 不支持在<template>元素上使用,也不能和v-else搭配使用。

10. v-for 列表渲染

(1) v-for 为什么要设置 key?

Vue 中当列表项元素的顺序改变时,DOM 元素并不会跟着移动,而是默认就地更新每个元素,让它们在原来的索引上渲染。但不能很好的复用 dom、不利于性能优化,因此需要设置 key 来唯一标识元素以最小化更新 dom、高效 diff。

(2)key 值设置有什么考量?index 可以吗?

key 值的设置需要确保 “如果数组中该元素没有发生变化,那么它的 key 值也应该不变” 。期望绑定key 值唯一且是基础类型,一般设置item.id

不能使用 index,当插入或删除某元素时会导致后面元素的 key 值发生变化,进而导致重新渲染。

(3)v-for 是按什么顺序遍历对象的?如何保证顺序?

  • 如果有iterator接口就执行next方法;对象的话就调用Object.keys()
  • 不同浏览器不能保证遍历顺序一致,想保证顺序可以将对象放在数组中。

就地更新?不会移动DOM元素的顺序,而是就地更新内容。比如在列表中新增一个元素,按照就地更新策略会在列表最后创建一个DOM元素,然后新增元素和它后面的元素的值都会更新。

11. v-if 和 v-for

不推荐两者同时使用,v-if优先级高于v-for、会先被执行(vue2中相反),所以 v-if 无法访问到 v-for 作用域内定义的变量。

如果需要同时出现,那可以在 v-if 的元素外层加一个 v-for 指令的 template 标签

<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

12. vue3 的全局API:nextTick()

响应式状态改变时,DOM 更新并不会同步生效,而是缓存到队列中一起执行。nextTick()是将回调函数推迟到下次 dom 更新结束后执行。

(1)原理

vue3 的 nextTick 利用了事件循环机制,它将回调函数放到 promise的then中来当作一个微任务执行,可以拿到最新的 DOM。

补充:微任务 => DOM 渲染 => 宏任务。虽然看起来与 nextTick 矛盾,但实际上 nextTick 确实可以拿到最新的 dom,这是因为此时浏览器已经计算好了 dom,只是还没更新渲染。

(2)应用场景

  • created中的 dom 操作一定要放在nextTick回调里,因为此时 dom 还未更新。而在mounted中操作 dom 就没有问题。

  • 如果在数据变化后要执行某个操作,但该操作又依赖于更新后的 dom 元素,那这个操作应该放在nextTick的回调函数中。

13. 内置组件:keep-alive

当在多个组件之间动态切换时,<KeepAlive>负责缓存被移除的组件实例、保留它们被切换时的状态,避免重新渲染导致性能降低

  • 提供 include 和 exclude 属性:两者都接受字符串或正则做参数。include 表示只有名称匹配的组件才会缓存,exclude 表示任何名称匹配的组件都不会被缓存,后者优先级高。

  • keep-alive 的组件会多出两个生命周期onActivatedonDeactivated:当一个组件实例从DOM上移除(不会销毁)但被keep-alive缓存时会触发 onDeactivated;当被缓存的组件重新插入DOM中时会触发 onActivated。这两个钩子适用于缓存的组件以及它们的后代组件。

  • 可以通过 max 属性指定可缓存的最大组件实例数,之后更新时采用LRU(最近最少使用)算法,淘汰最近最久未使用的组件。

<KeepAlive>
    <component :is="activeComponent">  // 动态组件由<component>元素和 is 属性实现
</KeepAlive>

14. vue3 中常见的生命周期

生命周期描述
onBeforeMount组件挂载之前执行,Vue开始解析模板,调用 render() 生成了 vDOM
onMounted组件挂载完成后执行,通常用于执行一些副作用,如发送请求、获取数据、和DOM相关的其他副作用等。
onBeforeUpdate组件更新DOM之前调用。适合在更新之前访问现有的 DOM
onUpdated组件更新DOM后调用。父组件的更新钩子会在子组件的更新钩子之后调用
onBeforeUnmount组件卸载之前调用
onUnmounted组件卸载之后调用,可以手动清理一些副作用,如计时器、DOM事件监听器
onActivatedkeep-alive 缓存的组件激活时调用
onDeactivatedkeep-alive 缓存的组件停用时调用
onErrorCaptured在捕获到后代组件传递的错误时执行,可以阻止错误继续向上传递
  • beforeCreate 中的 data 和 methods 还未初始化;created中的 data 和 methods 已经完成初始化,但是模板还没有编译,也就是说还不能获取 dom。
  • 生命周期的实现是利用发布订阅模式。

(1)常用哪些生命周期做哪些事?

  • onMounted\~onBeforeUnmounted:都可以获取 dom 元素
  • created也可以执行一些发送请求等副作用,但不如mounted好。因为 created 中 dom 还没渲染,此时发送 Ajax 请求可能会导致 JS 和 GUI 线程矛盾进行出现渲染中断的问题。
  • 在SSR中时应该在created中发送请求,因为 SSR 不支持 beforeMount、mounted。

(2)组件的生命周期调用顺序?

  • 加载渲染过程:父onBeforeMount->子onBeforeMount->子onMounted->父onMounted
  • 子组件更新过程:
    • 影响父组件:父onBeforeUpdate->子onBeforeUpdate->子onUpdated->父onUpdated
    • 不影响父组件(通常):子onBeforeUpdate->子onUpdated
  • 父组件更新过程:
    • 影响子组件(通常):父onBeforeUpdate->子onBeforeUpdate->子onUpdated->父onUpdated
    • 不影响子组件:父onBeforeUpdate->父onUpdated
  • 销毁过程:父onBeforeUnmount->子onBeforeUnmount->子onUnmounted->父onUnmounted

15. setup 函数

setup 是组件中使用组合式 API 的入口。

setup 中没有选项式 API 的 beforeCreate、created,这是因为 setup 就是组件实例创建的过程,在作用上取代了这两个钩子,所以这两个钩子中的代码都可以直接写在 setup 函数中。

16. 模板引用:ref 属性

ref 可以获取对 DOM 元素或子组件实例的引用。只可以在组件挂载后才能访问引用信息。

  • ref 属性可以直接访问底层 DOM 元素。示例
  • ref 也可以用于子组件上,这时引用获得的值是子组件实例。引用信息将会被注册在父组件的$refs对象上,此时可以用于父子组件通信。

17. Vue 组件间通信方式

(1)父子组件通信:props、emit/on、ref

  • props/emit:父组件通过props向子组件传递数据,子组件通过$emit触发事件向父组件传递数据(父组件v-on监听该事件)。

  • ref 属性:当 ref 用于子组件时,引用会指向子组件实例,因此可以用于父组件向子组件传值

<template>
    <child ref="msg"></child>
</template>

<script>
    this.$refs.msg.getMessage('我是子组件');
</script>

子组件可以修改父组件的 props 吗?

组件化开发遵循单向数据流原则,不建议修改父组件的 props,会导致数据流向难以理解、不容易维护等问题。

(2)兄弟组件通信:vuex、eventBus

  • 状态管理工具 Vuex/Pinia,支持任何通信方式。
  • 事件总线:mitt,支持任何通信方式。
    • 初始化一个 Vue 实例作为事件总线 Vue.prototype.$bus = new Vue
    • vue3 中移除了事件总线,但可以借助第三方库mitt来完成,适用于全局通信
    • 大多数情况下不推荐使用全局事件总线的方式来实现组件通信,后期维护会比较麻烦。

(3)跨层级组件通信:vuex、eventBus、provide/inject

provide/inject依赖注入:祖先组件通过provide向后代组件注入值,后代组件通过inject接收祖先组件提供的值。

18. 透传 Attribute

传递给一个组件,但没有被组件声明为propsemits的 attribute 或事件监听函数。常见的class、style、id

  • 当组件中只有一个根元素时,透传的 attribute 会自动添加到该根元素上;
  • 当组件中有多个根元素时,则不会自动透传 attribute,需要显式绑定$attrs的节点;
  • 如果子组件的根元素已经有了classstyle,那么它会和透传的 attribute 进行合并。

19. 插槽

插槽允许让父组件向子组件传递一些模板片段,让子组件进行渲染。

插槽的作用域:插槽内容可以访问到父组件的数据,但无法访问子组件数据。

  • 默认插槽:当父组件没有提供内容时,我们可以为子组件的插槽指定默认内容。
  • 具名插槽:当子组件需要多个slot时,可以通过name属性来区分插槽,其中没有指定name的插槽为default,并且父组件中需要使用<template>元素为具名插槽传入内容。
  • 作用域插槽:子组件可以在<slot>中绑定 props 来将数据传给父组件,让父组件中的插槽内容能够访问到子组件的数据。
    • 默认作用域插槽
    • 具名作用域插槽

20. 组合式 API 和 React Hooks

两者都是为了更好的逻辑组合与复用!

  1. 生命周期:hooks弱化了生命周期的概念,推荐使用函数组件;composition api 继续沿用生命周期;
  2. 数据的可变性:hooks 中数据是immutable的;VCA 的数据则是mutable的,因为 vue 的响应式是基于对象属性的;
  3. 调用顺序:hooks 必须保证每次调用的顺序一致,否则会出现数据错乱;VCA 不存在这些限制。
  4. (不用每次渲染时重复调用):组件每次渲染时 Hooks 都会重新执行一遍,而 VCA 只调用 setup 的代码一次,之后只更新变化的部分。

21. 虚拟 DOM 和 key 属性

参考 React 即可

  • 什么是虚拟dom?
  • 虚拟dom的优缺点?
  • 怎么实现虚拟dom?
  • key 的作用?
  • 虚拟dom映射到真实dom:经历虚拟dom的create、diff、patch等阶段。

22. Vue3 的 快速 diff 算法

  1. 预处理:先把可以直接排除的数据去掉,降低 diff 的操作量。
  • 对齐头部和尾部的节点:分别从头部向后、从尾部向前遍历新旧节点,直到节点的 key 值不同时跳出循环;
  • 针对仅有节点新增或被删除的情况:如果旧列表已经全部 patch,新列表还没有 patch 完,则会创建新节点,反之会卸载旧节点。
  1. 在预处理后我们通常会得到两个没有 patch 完的序列:
  • 建立新节点 key 和下标的映射:遍历没有 patch 过的新列表,通过 map 保存 key 值和在新节点列表中的下标 {key: index}
  • 处理删除的操作:遍历旧列表,通过之前的 map 判断旧节点是否还存在于新节点中,不存在则要移除;
  • 处理移动/新增的操作:通过getSequence函数基于贪心和二分查找算法得到最长递增子序列,然后根据最长递增子序列、对剩下的元素(子序列之外的元素)进行移动操作。最长递增子序列对应的是最少的移动次数,有利于提高性能。

23. Vue3 与 Vue2 的区别

(1)组合式 API

vue2 使用的是选项式 API,vue3 使用组合式 API 的写法。

  • 组合式 API 直接将数据定义在函数内,能够更好的组织和复用逻辑,之后重构时也较容易。
  • 选项式 API 的代码位置比较明确,数据放在 data 里,方法放在 methods 里,易于学习和使用。

(2)生命周期

  • vue3 的setup()代替了 vue2 的created、beforeCreate
  • 写法上,大部分生命周期的名称 +“on”,例如 mounted 和 onMounted。
  • beforeDestory 和 destoryed 改为 onBeforeUnmount、onUnmounted。

(3)多根节点/支持 Fragments

vue2 在模板中只能有一个根节点;但 vue3 支持片段(Fragments),即组件可以拥有多个根节点。

(4)响应式原理

(5)diff 算法优化

1. Vue2 的双端 diff 算法

  • 首先对比新旧节点判断tag。如果不为相同节点,则删除该节点重新创建节点进行替换;

  • 如果相同节点,则处理它们的子节点(递归比较):

    • 旧节点有子节点&新节点没有子节点,直接删除旧节点的子节点;
    • 旧节点没有子节点&新节点有子节点,将新节点的子节点添加到旧节点上;
    • 新旧节点都没有子节点,判断是否有文本节点,有则直接进行对比;
    • 新旧节点都有子节点,进行双端 diff算法。

    双端 diff 算法就是从新旧子节点列表的头尾节点开始对比,借助 key 值对比两个列表的头头、头尾、尾头和尾尾四种情况来找出可复用节点,然后不断向中间靠拢直到遍历完成。

2. Vue3 快速 diff 的优势

  • vue2 是全量 diff;vue3 是静态标记 + 非全量 diff。
  • vue3 使用了最长递增子序列减少了 DOM 移动次数,优化了 diff 过程

(6)v-for 和 v-if 的优先级

vue2 中 v-for 优先级高,vue3 中 v-if 优先级高。

24. Vue 模板编译原理

模板编译目的是将模板转换成 render 函数。 之后 render 函数能生成虚拟 DOM,最后进行渲染。

  • 解析模板:将模板转换成 AST 语法树,用 JS 对象的形式描述整个模板;
  • 优化 AST 树:编译器会做一些优化工作,例如静态节点提取(对那些非响应式的节点进行标记,之后就可以跳过对它们的 diff),以提高渲染性能;
  • 生成render:将优化后的 AST 树转换为可执行的代码,即渲染函数(渲染函数会返回一个虚拟 DOM 节点)。

AST 语法树是源代码语法结构的一种抽象表示。

25. 渲染过程:模板渲染为真实 DOM 节点

  • 编译:Vue 模板被编译为 render 函数。24. Vue模板编译原理

  • 挂载:render 函数生成后,vue 会调用它来生成虚拟 DOM,并基于虚拟 dom 创建真实 dom。

  • 更新:当数据发生变化后,会创建一个更新后的虚拟 DOM 树。然后对比新旧虚拟 DOM 树计算出需要更新的部分,并渲染到真实 DOM 上。

26. template 预编译

Vue 组件只会在实例化的时候模板编译一次,生成 render 函数后就不会再编译了。因此,编译对组件的运行是一种性能消耗。

预编译指的是在项目构建的过程中完成模板编译,这样能让组件在实际运行时跳过模板编译,进而提升性能。

27. Vue 事件绑定原理

  • 原生事件绑定是通过addEventListener绑定到真实 DOM 元素的;
  • 组件事件绑定是通过v-on指令实现的,通过emit触发事件。

28. 服务端渲染 SSR

CSR 指的是页面上的内容是我们加载的 js 文件渲染出来的,服务端只返回一个 html 模板。

image.png

SSR 指的是页面上的内容是由服务端渲染生成的,服务端直接返回拼接好的 html,然后浏览器直接显示服务端返回的 html 就可以了。

image.png

  • SSR 优点
    • 更快的首屏加载速度:服务端渲染的 HTML 无需等待所有的 js 文件下载完成,可以直接显示,因此首屏渲染速度更快、用户体验更好。
    • 更好的 SEO:SPA 页面的内容是通过 Ajax 获取,但爬虫抓取不到通过 Ajax 获取到的内容。
  • SSR 缺点
    • Vue SSR 只支持beforeCreatecreated两个钩子,因此对一些外部扩展库需要进行处理;
    • 更多的服务端负载:在 Node 中渲染一个完整的应用会占用更多的 CPU 资源。

怎么使用的? 同构就是一套 React/Vue 代码在服务器上运行一遍,到达浏览器后又运行一遍。SSR 负责完成页面结构,CSR 负责绑定事件、实现交互。

29. SSR 和 SSG(预渲染)的区别

SSG 又叫静态站点生成、预渲染,同样也有着更快的首屏加载、更好的 SEO。

  • 输出资源:
    • SSG 输出的是静态HTML,被服务器托管;
    • SSR 输出的是动态HTML,需要随着请求去渲染资源。
  • 使用场景:
    • SSG 适用于数据是静态的页面,并且在多次部署期间不会改变。例如去优化一些 about/contact 等页面的 SEO、文档站点、博客等;
    • SSR 适用于页面数据处理较多、含有动态数据的网站,比如电商网站。

静态资源:编译好的html/css/js文件,图片视频、文件等。

30. Vue 中手动的性能优化

  1. 编码阶段
    • 采用 keep-alive 缓存组件
    • 路由懒加载
    • 图片懒加载
    • 防抖、节流
    • v-for 必须保证 key 唯一
    • v-if 和 v-show 区分使用场景
    • 按需导入第三方模块(babel-plugin-component)
    • 长列表时滚动到可视区域再动态加载
  2. SEO 优化
    • 预渲染
    • 服务端渲染
  3. 基础的 Web 技术优化
    • 浏览器缓存
    • 使用 CDN
    • 服务端开启 gzip 压缩

31. Vue 的理解

  1. 响应式系统:组件状态都是响应式的 JS 对象,当状态改变时视图会自动更新。(Vue 的响应式是基于追踪对象属性的读写的)
  2. 虚拟dom、diff 后更新:当更改响应式状态后,DOM 会自动更新。但更新并不是同步的,将缓冲到更新周期的“下个时机”。
  3. 指令系统:v-if、v-for、v-model 等。

参考

  1. vue3 diff