前端高频面试题 —— Vue

141 阅读7分钟

1. vue的生命周期有哪些?

Vue 实例从创建到销毁的过程中,会经历一系列的阶段,这些阶段被称为生命周期。以下是 Vue 2 中主要的生命周期钩子函数:

创建阶段

  • beforeCreate

    • 此时实例刚刚被创建,数据观测(data observer)和事件机制(event/watcher setup)都尚未初始化。在这个钩子函数中,无法访问到 Vue 实例中的数据(data)和方法(methods)等。
  • created

    • 实例已经完成了数据观测、属性和方法的运算,并且已经完成了事件监听的配置。但是,此时 DOM 还没有被挂载,所以不能直接操作 DOM 元素。这个阶段通常用于进行一些数据的初始化操作,或者在渲染之前进行数据的预处理。

挂载阶段

  • beforeMount

    • 在挂载开始之前被调用,此时模板已经编译完成,但还没有将编译后的模板挂载到 DOM 上。可以在这个钩子函数中对模板进行最后的修改。
  • mounted

    • 实例已经挂载到 DOM 上,这意味着组件的 DOM 已经渲染完成,可以在这个钩子函数中进行 DOM 操作,例如获取 DOM 元素的高度、宽度等信息,或者进行一些基于 DOM 的第三方库的初始化。

更新阶段

  • beforeUpdate

    • 当数据发生变化时,在虚拟 DOM 重新渲染和打补丁之前被调用。在这个钩子函数中,可以获取到更新前的 DOM 状态,但是不能直接操作 DOM,因为此时 DOM 还没有更新。
  • updated

    • 当虚拟 DOM 重新渲染和打补丁完成后被调用。此时 DOM 已经更新,可以在这个钩子函数中操作更新后的 DOM 元素。但是要注意,在这个钩子函数中如果再次修改数据,可能会导致无限循环的更新。

销毁阶段

  • beforeDestroy

    • 在实例销毁之前被调用。在这个钩子函数中,实例仍然完全可用,可以进行一些收尾工作,例如清除定时器、取消订阅等。
  • destroyed

    • 实例已经被销毁,所有的指令、事件监听器等都已经被移除,子组件也已经被销毁。此时不能再访问到实例中的数据和方法。

Vue 3 中的生命周期钩子函数与 Vue 2 类似,但也有一些变化。例如,Vue 3 中使用setup函数作为组件的入口,在setup函数中可以通过onBeforeMountonMountedonBeforeUpdateonUpdatedonBeforeUnmountonUnmounted等函数来注册生命周期钩子。这些函数需要从vue模块中导入。

2. vue的双向数据绑定是如何实现的?

Vue 2

  • 通过数据劫持结合发布 - 订阅者模式来实现双向数据绑定的。Object.defineProperty () 方法来劫持各个属性的 setter 和 getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

缺点

  • 无法监听数组的变化

    • 无法直接检测数组元素的添加、删除和替换操作(如通过索引直接修改数组元素arr[0] = 1,或者修改数组长度arr.length = 0)。Vue 不得不重写了数组的一些方法(如pushpopshiftunshiftsplicesortreverse)来实现对这些操作的响应式监听,这增加了代码的复杂性。
  • 无法监听对象属性的新增和删除

    • 当直接给一个已经被Object.defineProperty处理过的对象添加新的属性或者删除属性时,这些操作无法被自动劫持。在 Vue 中,需要使用Vue.set()方法来添加新的响应式属性,使用Vue.delete()方法来删除响应式属性。
  • 性能问题

    • 在对复杂对象进行深度监听时,需要递归地使用Object.defineProperty对每个属性进行劫持,这在数据量较大时可能会导致性能下降。

Vue 3

  • 基于Proxy对象实现响应式。Proxy可以直接代理整个对象,而不是像Object.defineProperty那样只能劫持对象的已有属性。

3.nextTick的作用和实现原理是什么?

作用

  • 在 Vue 中,nextTick主要用于在下次 DOM 更新循环结束之后执行延迟回调。当数据更新后,DOM 需要重新渲染,但更新不是同步完成的。如果在数据更新后立即操作 DOM,可能获取不到最新的 DOM 状态,而nextTick可以确保在 DOM 更新完成后执行相关操作。例如,在修改数据后,想获取更新后的某个 DOM 元素的高度、宽度等信息,就需要使用nextTick

实现原理(Vue 2)

  • 微任务和宏任务结合

    • Vue 2 中nextTick的实现主要是基于微任务(Promise、MutationObserver)和宏任务(setTimeout)的组合使用。
    • 优先使用微任务,如果当前环境不支持微任务(如一些老旧浏览器不支持 Promise 和 MutationObserver),则降级使用宏任务(setTimeout)。
  • 维护一个 callbacks 队列

    • nextTick被调用时,会将传入的回调函数添加到一个 callbacks 队列中。
    • 然后,根据环境选择使用微任务或者宏任务来创建一个异步任务,当这个异步任务执行时,会遍历 callbacks 队列并执行其中的所有回调函数,从而实现了在下次 DOM 更新循环结束之后执行这些回调。

实现原理(Vue 3)

  • Vue 3 中nextTick的实现原理与 Vue 2 类似,同样是基于微任务和宏任务。不过,Vue 3 中对nextTick的实现进行了一些优化,使其代码更加简洁和高效。它也会优先使用 Promise 作为微任务,如果不支持则降级使用宏任务。

4.v-ifv-show的区别

可以在一个 <template> 元素上使用 v-ifv-show 不支持在 <template> 元素上使用

v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。

v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。

相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。

总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

5. vue中组件之间的通信方式有哪些?

父子组件通信

父传子(Props)

  • 父组件通过属性(props)向子组件传递数据。
  • 在子组件中通过props选项接收父组件传递过来的数据。

子传父($emit)

  • 子组件通过触发自定义事件并传递参数,将数据传递给父组件。
  • 在父组件中监听子组件触发的事件,并在事件处理函数中接收子组件传递过来的数据。

兄弟组件通信

通过共同的父组件作为媒介

  • 兄弟组件之间可以通过共同的父组件进行通信。一个兄弟组件通过触发父组件的方法将数据传递给父组件,父组件再将数据传递给另一个兄弟组件。

使用事件总线(EventBus)

  • 创建一个全局的事件总线对象,可以是一个新的 Vue 实例。
  • 兄弟组件分别在各自的方法中通过事件总线触发和监听事件,实现通信。

非直接相关组件通信

使用 Vuex

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  • 组件可以通过dispatch触发 actions,actions 可以提交 mutations 来修改状态,组件通过mapStatemapGetters等辅助函数获取 Vuex 中的状态。

获取父子组件实例 $parent、$children

Ref 获取实例的方式调用组件的属性或者方法

Provide、inject  依赖注入,父组件可以直接向某个后代组件传值

6. 关于computedwatchmethods及区别

computed和watch的区别

computed

  • 主要用于计算属性,基于其依赖的数据自动触发的。只要依赖的数据发生变化,计算属性就会重新计算并更新。
  • 计算属性具有缓存特性,如果依赖的数据没有变化,多次访问计算属性会直接返回缓存的值,而不会重复执行计算。
  • 不支持异步,当computed中有异步操作时,无法监听数据的变化。

watch

  • 用于监听特定数据的变化,默认情况下不会自动触发,只有当被监听的数据发生变化时才会触发,不会缓存。
  • 通过设置deep:true选项,可以实现对对象内部属性的深度监听
  • 当设置 immediate: true 时,watch会在组件初始化时立即触发一次回调函数,然后在被监听的数据属性发生变化时再次触发。
  • 在watch的回调函数中,可以执行异步操作,例如发起网络请求或进行耗时的计算。

computed和methods的区别

computed:计算属性的函数在其依赖的数据发生变化时会自动重新计算,并且具有缓存特性,如果依赖的数据没有变化,多次访问计算属性会直接返回缓存的值,而不会重复执行计算。

methods:在每次被调用时都会执行函数中的代码,没有缓存机制

7.data为什么是函数而不是对象

为了确保每个组件实例都拥有独立的数据,避免数据污染和冲突,从而保证组件的独立性和可复用性。当data被定义为函数时,每个组件实例在创建时都会调用这个函数,返回一个新的数据对象,这样每个组件实例都拥有自己的数据副本,不会相互干扰。这种方式实现了数据的封装和隔离,避免了数据共享导致的意外的副作用和难以追踪的数据变化。

  • data是一个对象时,如果多个组件实例共享同一个对象作为它们的data,那么一个组件对数据的修改会影响到其他组件。
  • 而当data是一个函数时,每个组件实例在创建时都会调用这个函数,返回一个独立的数据对象,从而确保每个组件都有自己独立的数据副本。

8. vue中key的作用

高效的 DOM 更新: Vue 在对比新旧虚拟 DOM 时,会根据元素的 key 值来判断元素是否是同一节点。如果 key 值相同,Vue 会认为这是同一个元素,只会更新元素的属性和内容,而不会进行不必要的重新创建和销毁操作。

避免不必要的重新渲染:当使用 v-for 指令渲染列表时,如果列表中的元素没有设置 key,Vue 会认为每个元素都是相同的,当数据发生变化时,可能会导致整个列表重新渲染。而设置了 key 后,Vue 可以更准确地判断哪些元素需要更新,哪些元素可以保持不变,从而避免不必要的重新渲染,提高性能。

总之,在 Vue 中,key 是一个非常重要的属性,它可以提高渲染性能,维护组件状态,避免状态错乱等问题。在使用 v-for 指令渲染列表或动态操作元素时,应该始终为元素设置唯一的 key 值。

9. vue中如何做强制刷新

重新加载页面

可以使用location.reload()方法强制刷新整个页面,但这种方式会导致页面重新加载,可能会丢失一些状态信息,用户体验不是很好。

this.$router.go(0)

在 Vue 中,this.$router.go(0)可以实现类似强制刷新的效果,但并不完全等同于真正意义上的强制刷新。

this.$router.go(0)会使浏览器重新加载当前页面,就像用户手动刷新页面一样。这种方式会导致页面重新加载,可能会丢失一些状态信息,并且用户体验也不是很好,因为页面会有一个明显的重新加载的过程。

相比之下,如果只是希望组件重新渲染而不进行整个页面的重新加载,可以考虑使用v-if控制组件的显示和隐藏、使用key属性变化触发组件重新渲染等方式来实现更优雅的更新效果。

使用v-if控制组件重新渲染

可以通过一个变量和v-if指令来控制组件的显示和隐藏,从而实现组件的重新渲染,间接达到刷新的效果。

使用key属性

在 Vue 中,当一个组件的key属性发生变化时,Vue 会强制重新渲染该组件。

provide/inject

在 Vue 中,单纯使用provideinject不能直接实现强制刷新。

provideinject主要用于实现祖先组件向后代组件传递数据,它并不会自动触发组件的重新渲染。

但是,可以结合其他方式来实现类似强制刷新的效果。比如可以在提供数据的组件中,通过改变提供的数据的值来触发使用该数据的后代组件的更新。

<template>
  <div>
    <button @click="forceRefreshInDescendant">强制刷新后代组件</button>
    <child-component></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';
export default {
  components: { ChildComponent },
  provide() {
    return {
      dataToShare: this.sharedData
    };
  },
  data() {
    return {
      sharedData: '初始数据',
    };
  },
  methods: {
    forceRefreshInDescendant() {
      this.sharedData = '新的数据';
    }
  }
};
</script>

在后代组件中通过inject获取数据,并在数据变化时进行相应的更新操作

<template>
  <div>{{ injectedData }}</div>
</template>

<script>
export default {
  inject: ['dataToShare'],
  computed: {
    injectedData() {
      return this.dataToShare;
    }
  }
};
</script>

这样,当点击按钮触发父组件方法改变提供的数据时,后代组件会因为数据变化而更新,实现了一种类似强制刷新的效果。