Vue3面试题总结

371 阅读16分钟

Vue3新特性

API特性

  • Composition API:可以更好的逻辑复用和代码组织,同一功能的代码不至于像以前一样太分散,虽然 Vue2 中可以用 minxin 来实现复用代码,但也存在问题,比如:方法或属性名会冲突、代码来源也不清楚等

  • SFC Composition API语法糖:

  • Teleport传送门:可以让子组件能够在视觉上跳出父组件(如父组件overflow:hidden)

  • Fragments:支持多个根节点,Vue2 中,编写每个组件都需要一个父级标签进行包裹,而Vue3 不需要,内部会默认添加 Fragments

  • SFC CSS变量:支持在 <style></style> 里使用 v-bind,给 CSS 绑定 JS 变量(color: v-bind(str)),且支持 JS 表达式 (需要用引号包裹起来);

  • Suspense:可以在组件渲染之前的等待时间显示指定内容,比如loading

  • v-memo:新增指令可以缓存 html 模板,比如 v-for 列表不会变化的就缓存,简单说就是用内存换时间

在 框架 设计层面:

  • 代码打包体积更小:许多VueAPI可以被Tree-Shaking,因为使用了es6moduletree-shaking 依赖于 es6模块的静态结构特性;

  • 响应式 的优化:用 Proxy 代替 Object.defineProperty,可以监听到数组下标变化,及对象新增属性,因为监听的不是对象属性,而是对象本身,还可拦截 applyhas 等方法;

  • 虚拟DOM的优化:保存静态节点直接复用(静态提升)、以及添加更新类型标记patchflag)(动态绑定的元素)

    • 静态提升:静态提升就是不参与更新的静态节点,只会创建一次,在之后每次渲染的时候会不停的被复用;
    • 更新类型标记:在对比VNode的时候,只对比带有更新类型标记的节点,大大减少了对比Vnode时需要遍历的节点数量;还可以通过 flag 的信息得知当前节点需要对比的内容类型;
    • 优化的效果Vue3的渲染效率不再和模板大小成正比,而是与模板中的动态节点数量成正比;
  • Diff算法 的优化:Diff算法 使用 最长递增子序列 优化了对比流程,使得 虚拟DOM 生成速度提升 200%

在 兼容性 方面:

  • Vue3 不兼容 IE11,因为IE11不兼容Proxy

Vue2和Vue3的区别详细说法

  • 响应式原理改成ES6中的proxy,解决了数组无法通过下标修改,无法监听到对象属性的新增和删除问题,提升了响应式的效率
    • 并不是完全抛弃了defineProperty,ref还是用去给一个空对象,定义一个value属性做响应式
  • 组合式API写法,函数式编程,方便按需引入。因为tree-shaking功能必须配合按需引入写法,vue3配合tree-shaking能让打包体积更小。
  • 性能优化,增加了静态节点标记。会标记静态节点,不对静态节点进行比对,增加了效率
  • diff算法,v2用的是双端算法,v3用快速diff算法(最长递增子序列)
  • v3推荐使用hook进行复用逻辑提取。v2使用mixin
  • 封装组件时,v-model,监听事件和传递值的改变
  • TS的配合

Composition API 和 Options API?

  • Vue2中通过定义一个options对象进行组件的配置,包括props、data、methods、computed、watch等选项。这种方式的优点在于结构清晰、易于理解,在小型项目中比较实用。
  • Vue3中以函数的形式编写代码,提高了代码的可维护性和重用性。Composition API还提供了模块化、类型推断等功能,可以更好地实现面向对象编程的思想。
  • composition API解决了this指向不明的问题。

Proxy和Object.defineProperty的区别?

两者都可以实现JS对象的响应式

作用对象的不同

  • Proxy可以代理整个对象,包括对象的所有属性、数组的所有元素以及类似数组对象的所有元素。
  • Object.defineProperty只能代理对象上定义的属性

监听属性的不同

  • Proxy可以监听到新增属性和删除属性的操作
  • Object.defineProperty只能监听到已经定义的属性的变化。

Vue3生命周期

  • onBeforeMount:在挂载之前被调用,渲染函数render首次被调用
  • onMounted:组件挂载时调用
  • onBeforeUpdate:数据已经完成更新页面刷新前调用,发生在虚拟DOM打补丁之前。
  • onUpdated:因数据更改导致的虚拟DOM重新渲染和打补丁时调用,页面已经刷新完成
  • onBeforeUnmount:在卸载组件实例之前调用,此阶段的组件实例依旧是正常的。
  • onUnmounted():卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
  • onActivated:被keep-alive缓存的组件激活时调用
  • onDeactivated:被keep-alive缓存的组件停用时调用
  • onErrorCaptured:当捕获一个来自子孙组件的错误时被调用,有三个参数:错误对象、发生错误的组件实例、一个包含错误来源信息的字符串;此钩子会返回false来阻止该错误继续向上传播。

页面初始化时,直接触发的钩子函数:onBeforeMountonMountedonRenderTracked

数据发生改变后触的钩子函数:onBeforeUpdateonUpdatedonRenderTrackedonRenderTriggered

组件被卸载时触发的钩子函数:onBeforeUnmountonUnmounted()

页面上绑定了响应式数据然后只要响应式数据做了取值操作就会触发onRenderTracked该钩子函数,包括初始化渲染时也会调用。

当虚拟DOM重新渲染被触发时调用,接收debugger event作为参数,此事件告诉是什么操作触发了重新渲染从而触发onRenderTriggered钩子函数。

这两个钩子仅在开发模式下可用,且在服务器端渲染期间不会被调用

Vue3数据响应式原理(数据驱动试图更新)

Proxy只代理对象的第一层,V3是怎么处理的?

判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测

监测数组的时候可能会触发多次get/set,如何防止多次触发?

判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。

Vue2中v-model与Vue3v-model的区别

v-model 本质就是 :value + input 方法的语法糖。可以通过 model 属性的 prop 和 event 属性来进行自定义。原生的 v-model,会根据标签的不同生成不同的事件和属性。

  1. text 和 textarea 元素使用 value 属性和 input 事件
  2. checkbox 和 radio 使用 checked 属性和 change 事件
  3. select 字段将 value 作为 prop 并将 change 作为事件

vue2

  • v-model只能使用一次
  • 默认传递value属性,接受input事件
  • 默认传递的属性和事件可通过model选项进行修改

Vue3

  • v-model只能使用一次,但是可以在v-model后添加参数使用多次
  • 默认传递modelvalue属性,接受update:modelValue事件,当在v-model后添加参数时如v-model:myPropName,则传递为myPropName属性,接受update:myPropName事件
  • 默认传递的属性和事件无法修改,必须是modelValue和update:modelValue

双向绑定的原理(数据和视图相互驱动更新)

  • 首先对数据进行劫持监听,设置监听器Observer,监听所有属性,有变动就通知订阅者watcher
  • watcher可以收到属性的变化通知并执行相应的函数,更新视图
  • watcher可能会有多个,需要一个消息订阅器Dep收集订阅者watcher
  • Compile,对每个节点元素进行扫描和解析,讲相关指令对应初始化成一个订阅者,替换模版数据或者绑定相应的函数,订阅者watcher接收到相应属性的变化,就会执行对应的更新函数,更新视图

Vue3中watch和watchEffect的区别

watch

watch 用于观察一个或多个响应式引用或计算属性,并在它们更改时执行一个函数。

import { ref, watch } from 'vue';

export default {
  setup() {
    const count = ref(0);

    watch(count, (newVal, oldVal) => {
      console.log(`count changed from ${oldVal} to ${newVal}`);
    });
  }
}
/*选项*/
-   immediate: 是否立即执行回调函数。
-   deep: 是否深度观察(适用于对象或数组)。
-   flush: 控制回调何时执行(`pre``post``sync`)。

watchEffect

watchEffect 用于立即执行一个函数,并响应该函数内部所依赖的所有响应式引用或计算属性。

import { ref, watchEffect } from 'vue';

export default {
  setup() {
    const count = ref(0);

    watchEffect(() => {
      console.log(`count is now ${count.value}`);
    });
  }
}

watch、watchEffect的区别

  • 旧值与新值: watch 回调提供新值和旧值,而 watchEffect 不提供。
  • 自动侦测依赖 vs 显式声明: watchEffect 自动侦测函数内所用到的所有响应式引用,而 watch 需要明确指定要观察的引用。
  • 立即执行: watchEffect 创建时会立即执行一次,而 watch 默认不会,除非设置了 immediate 选项。
  • 多源观察: watch 可以观察多个源,但 watchEffect 观察函数内的所有响应式引用。

分别使用场景

  • watch
    • 需要访问旧值和新值时
    • 需要基于某个条件观察某个值时
    • 需要配置选项时(deep、flush等)
  • watchEffect
    • 需要依赖多个响应式引用,所有改变都触发同一函数
    • 不需要旧值,只关注新值时

ref、reactive、toRef、toRefs

  • ref: 函数可以接收原始数据类型引用数据类型。-  ref函数创建的响应式数据,在模板中可以直接被使用,在 JS 中需要通过 .value 的形式才能使用。
  • reactive: 函数只能接收引用数据类型
  • toRef:针对一个响应式对象的属性创建一个ref,使得该属性具有响应式,两者之间保持引用关系。
  • toRefs: 将一个响应式对象转为普通对象,对象的每一个属性都是对应的ref,两者保持引用关系
import {ref,reactive} from 'vue'
const count = ref(0)
console.log(count.value) // 0

const state = reactive({
  count:0
})
console.log(state.count) // 0

Ref 原理

  • ref内部封装一个RefImpl类,并设置get/set,当通过.value调用时就会触发劫持,从而实现响应式。
  • 当接收的是对象或者数组时,内部仍然是 reactive 去实现一个响应式;

Reactive 原理

  • reactive内部使用Proxy代理传入的对象,从而实现响应式。
  • 使用 Proxy 拦截数据的更新和获取操作,再使用 Reflect 完成原本的操作(getset

scoped的原理

style标签设置scoped属性时,css样式只能作用于当前的组件。 vue 中的 scoped 属性的效果主要通过 PostCSS 转译实现的。PostCSS 给一个组件中的所有 DOM 添加了一个独一无二的动态属性,然后,给 CSS 选择器额外添加一个对应的属性选择器来选择该组件中 DOM,这种做法使得样式只作用于含有该属性的 DOM,即组件内部 DOM。

V2与V3中diff算法的区别

V2的diff算法

Vue 2.x使用的是双向指针遍历的算法,通过逐层比对新旧虚拟DOM树节点的方式计算出需要更新的最小操作集合。但这种算法的缺点是,由于遍历是从左到右、从上到下进行的,当发生节点删除或移动时,会导致其它节点位置的计算出现错误,因此会造成大量无效的重新渲染。

V3的diff算法

Vue 3.x使用的是单向遍历算法,只扫描新虚拟DOM树上的节点,判断是否需要更新,跳过不需要更新的节点,进一步减少了不必要的操作。此外,在虚拟DOM创建后,Vue 3会缓存虚拟DOM节点的描述信息,以便于复用,这也会带来性能上的优势。同时,Vue 3还引入了静态提升技术,在编译时将一些静态的节点及其子节点预先处理成HTML字符串,大大提升了渲染性能。

V3diff算法中的最长递增子序列

定义:在一个给定的数值序列中,找到一个子序列,使得这个子序列元素的数值依次递增,并且这个子序列的长度尽可能地大。最长递增子序列中的元素在原序列中不一定是连续的。 题目:

一个整数数组 nums ,找到其中最长严格递增子序列的长度。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

解决方法:

// 1:动态规划
var lengthOfLIS = function (nums) {
  if(nums.length == 0 ){return 0 }
  const dp = new Array(nums.length).fill(1);
  for (let i = 0; i < nums.length; i++) {
    // i与i前面的元素比较
    for (let j = 0; j < i; j++) {
      // 找比i小的元素,找到一个,就让当前序列的最长子序列长度加1
      if (nums[i] > nums[j]) {
        dp[i] = Math.max(dp[i], dp[j] + 1);
      }
    }
  }
  // 找出最大的子序列
  return Math.max(...dp);
}

组件通信

  • Props 和 Events:父组件通过 props 向子组件传递数据,子组件通过事件向父组件传递数据。

  • Provide 和 Inject:祖先组件通过 provide 提供数据或方法,后代组件通过 inject 获取数据或方法。

  • Vuex:使用 Vuex 状态管理库进行全局状态管理。

Vue3的新特性、性能提升

  • 响应式系统升级。从 defineProperty 升级到 ES6 原生的 Proxy,不需要初始化遍历所有属性,就可以监听新增和删除的属性。
  • 引入了 Composition API(组合式 API)。
  • 多根组件。可以直接在 template 中使用多个根级别的元素。
  • 引入了 Teleport(传送)。可以将组件的内容渲染到指定 DOM 节点的新特性。一般用于创建全局弹窗和对话框等组件。
  • 编译优化。重写了虚拟 DOM,提升了渲染速度。diff 时静态节点会被直接跳过。
  • 源码体积优化,打包优化。

setup的作用

概念

setup函数是在组件创建时执行的函数,接收两个参数:props和context。props是一个对象,包含了组件接收到所有props属性;context是一个对象,包含了一些与组件实例相关的属性和方法。

使用场景

  • 声明响应式的数据和计算属性
  • 注册事件处理函数
  • 定义生命周期钩子函数
  • 注册子组件
  • 访问父级组件的属性和方法
  • 访问路由和状态管理器等全局对象

setup和created谁先执行

setup开始创建组件之前,在beforeCreatecreated之前执行。创建的是data和method

setup中为啥没有beforeCreate和created?

因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

Vue3新增组件

  • Drawer抽屉组件
    • 支持left|right|top|bottom4个方向弹出抽屉
    • 支持定制抽屉宽度,以及动态拖拽改变抽屉的宽度
    • 支持default|header|footer等插槽
  • Guide引导组件
  • PopConform气泡确认框组件

Pinia和vuex的区别

共同点

都是Vue框架设计的状态管理库,提供在Vue应用程序中管理和共享状态的机制。管理数据流的一种设计模式。

不同点

  • Vuex是全局单一状态树的概念,所有组件共享一个状态。Pinia采用分模块的状态树,每个模块有自己的状态。
  • Vuex需要定义state、mutations、actions和getters管理状态。Pinia中只需要定义一个类似state的响应式对象,和用一些方法代替actions、mutations和getters复杂的方法。
  • Vuex中,可以通过mapState、mapGetters、mapMutations和mapActions辅助函数简化状态的访问和调用。Pinia没有提供类似的辅助函数。是通过使用Composition API和Vue3的响应式工具来访问和操作状态。
  • Pinia更好支持TS,更容易进行类型推断和类型安全检查。

vue-router

当页面变化时,监听hashchangepopstate事件,做路由转换。

第一种:hash模式

hash 模式是一种把前端路由的路径用井号 # 拼接在真实 url 后面的模式。当井号 # 后面的路径发生变化时,浏览器并不会重新发起请求,而是会发 onhashchange 事件。改变hash值,会在浏览器的访问历史中增加一个记录。

hash原理

hash 通过监听浏览器 onhashchange 事件变化,查找对应路由应用。通过改变 location.hash 改变页面路由。

改变hash值的方式
  • a标签href属性可以设置页面元素的ID
  • window.location.hash
  • history.forward(),history.back()

第二种:history模式

history API 是 H5 提供的新特性,允许开发者直接更改前端路由,即更新浏览器 URL 地址而不重新发起请求。通过调用window.history对象的方法实现页面的无刷新跳转。

history原理(每当history对象变化时,触发popstate事件)

利用 html5 的history Interface 中新增的 pushState() 和 replaceState() 方法,改变页面路径。

生产环境中history存在问题

history  模式的时候路径会随着  http 请求发送给服务器,项目打包部署时,需要后端配置 nginx,刷新当前页面会重新发送请求,如果  nginx  没有匹配到当前url,就会出现404的页面。

解决问题:

在nginx做代理转发,在  nginx 中配置按顺序检查参数中的资源是否存在,如果都没有找到,让   nginx  内部重定向到项目首页。

history的方法
  • history.back():移动到上一个网址,等同于浏览器的后退键。对于第一个访问的网址,无效
  • history.forward():移动到下一个网址,浏览器的前进键。最后一个网址,无效
  • history.go():接收一个整数的参数。默认为0,刷新页面。go(1):前进。go(-1):后退
  • history.pushState(),添加一条记录,不刷新页面
  • history.replaceState():修改记录