首页 > 头条 > 正文

29个Vue经典面试题(附源码级详解)

青灯夜游
发布: 2022-07-27 21:16:56
转载
6050 人浏览过

本篇文章给大家总结分享29+个Vue经典面试题(附源码级详解),带你梳理基础知识,增强Vue知识储备,值得收藏,快来看看吧!

01-Vue 3.0的设计目标是什么?做了哪些优化?

分析

还是问新特性,陈述典型新特性,分析其给你带来的变化即可。(学习视频分享:vue视频教程

思路

从以下几方面分门别类阐述:易用性、性能、扩展性、可维护性、开发体验等

范例

  • Vue3的最大设计目标是替代Vue2(皮一下),为了实现这一点,Vue3在以下几个方面做了很大改进,如:易用性、框架性能、扩展性、可维护性、开发体验等

  • 易用性方面主要是API简化,比如v-model在Vue3中变成了Vue2中v-modelsync修饰符的结合体,用户不用区分两者不同,也不用选择困难。类似的简化还有用于渲染函数内部生成VNode的h(type, props, children),其中props不用考虑区分属性、特性、事件等,框架替我们判断,易用性大增。

  • 开发体验方面,新组件Teleport传送门、FragmentsSuspense等都会简化特定场景的代码编写,SFC Composition API语法糖更是极大提升我们开发体验。

  • 扩展性方面提升如独立的reactivity模块,custom renderer API等

  • 可维护性方面主要是Composition API,更容易编写高复用性的业务逻辑。还有对TypeScript支持的提升。

  • 性能方面的改进也很显著,例如编译期优化、基于Proxy的响应式系统

  • 。。。

可能的追问

  1. Vue3做了哪些编译优化?
  2. ProxydefineProperty有什么不同?

02-你了解哪些Vue性能优化方法?

分析

这是一道综合实践题目,写过一定数量的代码之后小伙伴们自然会开始关注一些优化方法,答得越多肯定实践经验也越丰富,是很好的题目。

答题思路:

根据题目描述,这里主要探讨Vue代码层面的优化

回答范例

  • 我这里主要从Vue代码编写层面说一些优化手段,例如:代码分割、服务端渲染、组件缓存、长列表优化等

  • 最常见的路由懒加载:有效拆分App尺寸,访问时才异步加载

    const router = createRouter({
      routes: [
        // 借助webpack的import()实现异步组件
        { path: '/foo', component: () => import('./Foo.vue') }
      ]
    })
    登录后复制
  • keep-alive缓存页面:避免重复创建组件实例,且能保留缓存组件状态

    <router-view v-slot="{ Component }">
        <keep-alive>
        <component :is="Component"></component>
      </keep-alive>
    </router-view>
    登录后复制
  • 使用v-show复用DOM:避免重复创建组件

    <template>
      <div class="cell">
        <!-- 这种情况用v-show复用DOM,比v-if效果好 -->
        <div v-show="value" class="on">
          <Heavy :n="10000"/>
        </div>
        <section v-show="!value" class="off">
          <Heavy :n="10000"/>
        </section>
      </div>
    </template>
    登录后复制
  • v-for 遍历避免同时使用 v-if:实际上在Vue3中已经是个错误写法

    <template>
        <ul>
          <li
            v-for="user in activeUsers"
            <!-- 避免同时使用,vue3中会报错 -->
            <!-- v-if="user.isActive" -->
            :key="user.id">
            {{ user.name }}
          </li>
        </ul>
    </template>
    <script>
      export default {
        computed: {
          activeUsers: function () {
            return this.users.filter(user => user.isActive)
          }
        }
      }
    </script>
    登录后复制
  • v-once和v-memo:不再变化的数据使用v-once

    <!-- single element -->
    <span v-once>This will never change: {{msg}}</span>
    <!-- the element have children -->
    <div v-once>
      <h1>comment</h1>
      <p>{{msg}}</p>
    </div>
    <!-- component -->
    <my-component v-once :comment="msg"></my-component>
    <!-- `v-for` directive -->
    <ul>
      <li v-for="i in list" v-once>{{i}}</li>
    </ul>
    登录后复制

    按条件跳过更新时使用v-momo:下面这个列表只会更新选中状态变化项

    <div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
      <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
      <p>...more child nodes</p>
    </div>
    登录后复制

    https://vuejs.org/api/built-in-directives.html#v-memo

  • 长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容

    <recycle-scroller
      class="items"
      :items="items"
      :item-size="24"
    >
      <template v-slot="{ item }">
        <FetchItemView
          :item="item"
          @vote="voteItem(item)"
        />
      </template>
    </recycle-scroller>
    登录后复制

    一些开源库:

    • vue-virtual-scroller:https://github.com/Akryum/vue-virtual-scroller
    • vue-virtual-scroll-grid:https://github.com/rocwang/vue-virtual-scroll-grid
  • 事件的销毁:Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。

    export default {
      created() {
        this.timer = setInterval(this.refresh, 2000)
      },
      beforeUnmount() {
        clearInterval(this.timer)
      }
    }
    登录后复制
  • 图片懒加载

    对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。

    <img v-lazy="/static/img/1.png">
    登录后复制

    参考项目:https://github.com/hilongjw/vue-lazyload

  • 第三方插件按需引入

    element-plus这样的第三方组件库可以按需引入避免体积太大。

    import { createApp } from 'vue';
    import { Button, Select } from 'element-plus';
    
    const app = createApp()
    app.use(Button)
    app.use(Select)
    登录后复制
  • 子组件分割策略:较重的状态组件适合拆分

    <template>
      <div>
        <ChildComp/>
      </div>
    </template>
    
    <script>
    export default {
      components: {
        ChildComp: {
          methods: {
            heavy () { /* 耗时任务 */ }
          },
          render (h) {
            return h('div', this.heavy())
          }
        }
      }
    }
    </script>
    登录后复制

    但同时也不宜过度拆分组件,尤其是为了所谓组件抽象将一些不需要渲染的组件特意抽出来,组件实例消耗远大于纯dom节点。参考:https://vuejs.org/guide/best-practices/performance.html#avoid-unnecessary-component-abstractions

  • 服务端渲染/静态网站生成:SSR/SSG

    如果SPA应用有首屏渲染慢的问题,可以考虑SSR、SSG方案优化。参考:https://vuejs.org/guide/scaling-up/ssr.html


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

这题现在有些落伍,vue3已经不用一个根了。因此这题目很有说头!

体验一下

vue2直接报错,test-v2.html

new Vue({
  components: {
    comp: {
      template: `
        <div>root1</div>
        <div>root2</div>
      `
    }
  }
}).$mount('#app')
登录后复制

1.png

vue3中没有问题,test-v3.html

Vue.createApp({
  components: {
    comp: {
      template: `
        <div>root1</div>
        <div>root2</div>
      `
    }
  }
}).mount('#app')
登录后复制

2.png

回答思路

  • 给一条自己的结论
  • 解释为什么会这样
  • vue3解决方法原理

范例

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

知其所以然

  • patch方法接收单根vdom:

    https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L354-L355

    // 直接获取type等,没有考虑数组的可能性
    const { type, ref, shapeFlag } = n2
    登录后复制
  • patch方法对Fragment的处理:

    https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1091-L1092

    // a fragment can only have array children
    // since they are either generated by the compiler, or implicitly created
    // from arrays.
    mountChildren(n2.children as VNodeArrayChildren, container, ...)
    登录后复制

04-这是基本应用能力考察,稍微上点规模的项目都要拆分vuex模块便于维护。

体验

https://vuex.vuejs.org/zh/guide/modules.html

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}
const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}
const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
store.getters.c // -> moduleA里的getters
store.commit('d') // -> 能同时触发子模块中同名mutation
store.dispatch('e') // -> 能同时触发子模块中同名action
登录后复制

思路

  • 概念和必要性

  • 怎么拆

  • 使用细节

  • 优缺点

范例

  • 用过module,项目规模变大之后,单独一个store对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护

  • 可以按之前规则单独编写子模块代码,然后在主文件中通过modules选项组织起来:createStore({modules:{...}})

  • 不过使用时要注意访问子模块状态时需要加上注册时模块名:store.state.a.xxx,但同时gettersmutationsactions又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子模块加上namespace选项,此时再访问它们就要加上命名空间前缀。

  • 很显然,模块的方式可以拆分代码,但是缺点也很明显,就是使用起来比较繁琐复杂,容易出错。而且类型系统支持很差,不能给我们带来帮助。pinia显然在这方面有了很大改进,是时候切换过去了。

可能的追问

  • 用过pinia吗?都做了哪些改善?


05-怎么实现路由懒加载呢?

分析

这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效。

// 将
// import UserDetails from './views/UserDetails'
// 替换为
const UserDetails = () => import('./views/UserDetails')

const router = createRouter({
  // ...
  routes: [{ path: '/users/:id', component: UserDetails }],
})
登录后复制

参考:https://router.vuejs.org/zh/guide/advanced/lazy-loading.html

思路

  • 必要性

  • 何时用

  • 怎么用

  • 使用细节

回答范例

  • 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段。

  • 一般来说,对所有的路由都使用动态导入是个好主意。

  • component选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:

    { path: '/users/:id', component: () => import('./views/UserDetails') }

  • 结合注释() => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')可以做webpack代码分块

    vite中结合rollupOptions定义分块

  • 路由中不能使用异步组件

知其所以然

component (和 components) 配置如果接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。

https://github1s.com/vuejs/router/blob/HEAD/src/navigationGuards.ts#L292-L293


06-ref和reactive异同

这是Vue3数据响应式中非常重要的两个概念,自然的,跟我们写代码关系也很大。

体验

ref:https://vuejs.org/api/reactivity-core.html#ref

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1
登录后复制

reactive:https://vuejs.org/api/reactivity-core.html#reactive

const obj = reactive({ count: 0 })
obj.count++
登录后复制

回答思路

  • 两者概念

  • 两者使用场景

  • 两者异同

  • 使用细节

  • 原理

回答范例

  • ref接收内部值(inner value)返回响应式Ref对象,reactive返回响应式代理对象

  • 从定义上看ref通常用于处理单值的响应式,reactive用于处理对象类型的数据响应式

  • 两者均是用于构造响应式数据,但是ref主要解决原始值的响应式问题

  • ref返回的响应式数据在JS中使用需要加上.value才能访问其值,在视图中使用会自动脱ref,不需要.value;ref可以接收对象或数组等非原始值,但内部依然是reactive实现响应式;reactive内部如果接收Ref对象会自动脱ref;使用展开运算符(...)展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象之后再展开。

  • reactive内部使用Proxy代理传入对象并拦截该对象各种操作(trap),从而实现响应式。ref内部封装一个RefImpl类,并设置get value/set value,拦截用户对值的访问,从而实现响应式。

知其所以然

  • reactive实现响应式:

    https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/reactive.ts#L90-L91

  • ref实现响应式:

    https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/ref.ts#L73-L74


07-watch和watchEffect异同

我们经常性需要侦测响应式数据的变化,vue3中除了watch之外又出现了watchEffect,不少同学会混淆这两个api。

体验

watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。

Runs a function immediately while reactively tracking its dependencies and re-runs it whenever the dependencies are changed.

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

count.value++
// -> logs 1
登录后复制

watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。

Watches one or more reactive data sources and invokes a callback function when the sources change.

const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)
登录后复制

思路

  • 给出两者定义

  • 给出场景上的不同

  • 给出使用方式和细节

  • 原理阐述

范例

  • watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。

  • watchEffect(effect)是一种特殊watch,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect就是我们需要的。watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch。

  • watchEffect在使用时,传入的函数会立刻执行一次。watch默认情况下并不会执行回调函数,除非我们手动设置immediate选项。

  • 从实现上来说,watchEffect(fn)相当于watch(fn,fn,{immediate:true})

知其所以然

watchEffect定义:https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiWatch.ts#L80-L81

export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  return doWatch(effect, null, options)
}
登录后复制

watch定义如下:https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiWatch.ts#L158-L159

export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  return doWatch(source as any, cb, options)
}
登录后复制

很明显watchEffect就是一种特殊的watch实现。


08-SPA、SSR的区别是什么

我们现在编写的Vue、React和Angular应用大多数情况下都会在一个页面中,点击链接跳转页面通常是内容切换而非页面跳转,由于良好的用户体验逐渐成为主流的开发模式。但同时也会有首屏加载时间长,SEO不友好的问题,因此有了SSR,这也是为什么面试中会问到两者的区别。

思路分析

  • 两者概念

  • 两者优缺点分析

  • 使用场景差异

  • 其他选择

回答范例

  • SPA(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称 CSR。SSR(Server Side Render)即 服务端渲染。一般也称为 多页面应用(Mulpile Page Application),简称 MPA。

  • SPA应用只会首次请求html文件,后续只需要请求JSON数据即可,因此用户体验更好,节约流量,服务端压力也较小。但是首屏加载的时间会变长,而且SEO不友好。为了解决以上缺点,就有了SSR方案,由于HTML内容在服务器一次性生成出来,首屏加载快,搜索引擎也可以很方便的抓取页面信息。但同时SSR方案也会有性能,开发受限等问题。

  • 在选择上,如果我们的应用存在首屏加载优化需求,SEO需求时,就可以考虑SSR。

  • 但并不是只有这一种替代方案,比如对一些不常变化的静态网站,SSR反而浪费资源,我们可以考虑预渲染(prerender)方案。另外nuxt.js/next.js中给我们提供了SSG(Static Site Generate)静态网站生成方案也是很好的静态站点解决方案,结合一些CI手段,可以起到很好的优化效果,且能节约服务器资源。

知其所以然

内容生成上的区别:

SSR

3.png

SPA

4.png

部署上的区别

5.png


09-vue-loader是什么?它有什么作用?

分析

这是一道工具类的原理题目,相当有深度,具有不错的人才区分度。

体验

使用官方提供的SFC playground可以很好的体验vue-loader

sfc.vuejs.org

有了vue-loader加持,我们才可以以SFC的方式快速编写代码。

<template>
  <div class="example">{{ msg }}</div>
</template>

<script>
export default {
  data() {
    return {
      msg: 'Hello world!',
    }
  },
}
</script>

<style>
.example {
  color: red;
}
</style>
登录后复制

思路

  • vue-loader是什么东东
  • vue-loader是做什么用的
  • vue-loader何时生效
  • vue-loader如何工作

回答范例

  • vue-loader是用于处理单文件组件(SFC,Single-File Component)的webpack loader

  • 因为有了vue-loader,我们就可以在项目中编写SFC格式的Vue组件,我们可以把代码分割为<template>、<script>和<style>,代码会异常清晰。结合其他loader我们还可以用Pug编写<template>,用SASS编写<style>,用TS编写<script>。我们的<style>还可以单独作用当前组件。

  • webpack打包时,会以loader的方式调用vue-loader

  • vue-loader被执行时,它会对SFC中的每个语言块用单独的loader链处理。最后将这些单独的块装配成最终的组件模块。

知其所以然

1、vue-loader会调用@vue/compiler-sfc模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,返回的代码类似下面:

// source.vue被vue-loader处理之后返回的代码

// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'

script.render = render
export default script
登录后复制

2、我们想要script块中的内容被作为js处理(当然如果是<script lang="ts">被作为ts处理),这样我们想要webpack把配置中跟.js匹配的规则都应用到形如source.vue?vue&type=script的这个请求上。例如我们对所有*.js配置了babel-loader,这个规则将被克隆并应用到所在Vue SFC的

import script from 'source.vue?vue&type=script'
登录后复制

将被展开为:

import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
登录后复制

类似的,如果我们对.sass文件配置了style-loader + css-loader + sass-loader,对下面的代码:

<style scoped lang="scss">
登录后复制

vue-loader将会返回给我们下面结果:

import 'source.vue?vue&type=style&index=1&scoped&lang=scss'
登录后复制

然后webpack会展开如下:

import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
登录后复制

1)当处理展开请求时,vue-loader将被再次调用。这次,loader将会关注那些有查询串的请求,且仅针对特定块,它会选中特定块内部的内容并传递给后面匹配的loader。

2)对于<script>块,处理到这就可以了,但是<template><style>还有一些额外任务要做,比如:

  • 需要用Vue 模板编译器编译template,从而得到render函数
  • 需要对<style scoped>中的CSS做后处理(post-process),该操作在css-loader之后但在style-loader之前

实现上这些附加的loader需要被注入到已经展开的loader链上,最终的请求会像下面这样:

//