🚀 别错过!2025 Vue 面试最全问题整理来了!

322 阅读2分钟

大家好,我是前端架构师,关注微信公众号【程序员大卫】:

  • 回复 [面试] :免费领取“前端面试大全2025(Vue,React等)”
  • 回复 [架构师] :免费领取“前端精品架构师资料”
  • 回复 [书] :免费领取“前端精品电子书”
  • 回复 [软件] :免费领取“Window和Mac精品安装软件”

Vue2 中对大数据的性能优化

在 Vue2 中,如果某些大数据对象不需要响应式处理,可以使用 Object.freeze 将其冻结。Vue 内部会通过 Object.isFrozen 判断对象是否已被冻结,从而跳过该对象的响应式转换过程,以提升性能。

需要注意的是:

  • Object.freeze 只能冻结对象的第一层属性。
  • 对被冻结对象的属性进行直接赋值会抛出错误。
  • 对嵌套属性(如第二层)赋值不会报错,但由于整个对象已被 Vue 忽略响应式转换,因此这些更改不会反映在页面上,尽管实际值已被修改。

示例代码:

<template>
  <div>
    {{ obj.a }}
    {{ obj.b.c }}
  </div>
</template>

<script>
export default {
  data() {
    return {
      obj: Object.freeze({
        a: 1111,
        b: {
          c: 2222
        },
      })
    }
  },
  mounted() {
    // 报错: TypeError: Cannot assign to read only property 'a' of object '#<Object>'
    this.obj.a = 3333;

    // 虽然页面上显示的仍是原值,但实际值已发生改变
    this.obj.b.c = 4444;
    console.log(this.obj.b.c); // 4444
  }
}
</script>

Object.freeze 只能冻结第一层:

const obj = Object.freeze({
    a: 1111,
    b: {
        c: 222   
    }
});

obj.a = 8888;
console.log(obj.a); //已经冻结; 1111

obj.b.c = 9999
console.log(obj.b.c); // 没有冻结; 9999

Vue 2 中 dataprops 使用相同属性名的行为

如果在组件中 dataprops 定义了同名的属性,最终会以 props 中的值为准,而 data 中定义的同名属性会被忽略(不会生效)。

<template>
  <div>{{ num }}</div>
</template>

<script>
export default {
  props: {
    num: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      num: '这是 data 中的 num(但不会生效)'
    }
  }
}
</script>

在 Vue 中获取动态资源路径的方法

export const getAssetUrl = (src: string): string =>
  new URL(`/src/assets/${src}`, import.meta.url).href;

Vue2 和 Vue3 的区别

1. 性能优化

✅ 更快的虚拟 DOM

Vue 3 采用 基于 Block 的静态提升 和 编译优化,减少了不必要的虚拟 DOM 重新渲染,提高了性能。

✅ 更小的体积

Vue 3 使用 Tree Shaking 机制,未使用的 API 不会被打包进最终构建结果,使得体积更小。

✅ 更好的 TypeScript 支持

Vue 3 在设计时考虑到了 TypeScript,API 具有更好的类型推导,相比 Vue 2 更容易进行类型检查。

2. Vue3 引入 Composition API

可以使用 setup 进行逻辑组织,更加灵活、可复用。

3. 响应式系统升级

Vue 3 采用了 Proxy 取代 Vue 2 的 Object.defineProperty,解决了 Vue 2 的响应式缺陷:

4. 生命周期钩子变更

移除了 Vue2 的 beforeCreatecreated

5. Vue 3 新增功能

  • Teleport(跨层级传送)
  • Fragment(多个根节点)
  • Suspense(异步组件加载)

Pinia 与 Vuex 区别

  • Pinia移除了 mutations,使用起来更简洁。
  • Pinia更好的 TypeScript 支持。

当然可以,下面是对你这段内容的重组和润色,使其结构更清晰、语言更流畅:

Proxy 与 Object.defineProperty 的区别

1. 响应式对象的代理方式不同

Object.defineProperty 需要逐个遍历对象的每一个属性,对每个属性进行劫持。这在处理嵌套对象时效率较低,尤其是深层嵌套结构。

相比之下,Proxy 的响应式是按需递归:只有在读取属性且该属性为对象时,才会对其进行代理。这种“懒代理”方式能显著提升性能,避免了不必要的深层遍历。

示例:

const isObject = (obj) => typeof obj === "object" && obj !== null;
const obj = {
	a: 1,
	b: {
		c: 2,
	},
};
const reactive = (obj) => {
	return new Proxy(obj, {
		get(target, key) {
			console.log("读取", key);
			if (isObject(target[key])) {
				return reactive(target[key]);
			}
			return target[key];
		},
		set(target, key, value) {
			if (target[key] === value) return;
			console.log("设置", key);
			target[key] = value;
		},
	});
};
const proxy = reactive(obj);
proxy.a;
proxy.b.c = 40;

输出结果:

读取 a
读取 b
设置 c

2. 拦截能力差异

Vue 2 基于 Object.defineProperty 实现响应式系统,存在以下限制:

  • 无法监听新增属性
    如:this.arr[this.arr.length] = 100 不会触发视图更新,需使用 this.$set(this.arr, this.arr.length, 100)
  • 无法监听属性删除
    如:delete this.obj.key 也不会触发视图更新,需改用 this.$delete(this.obj, 'key')
  • 数组下标赋值无效
    例如:this.arr[0] = 100 不会触发更新,需使用 this.$set(this.arr, 0, 100)

虽然数据本身是变了,但 Vue 无法感知,除非手动调用 this.$forceUpdate() 强制刷新视图。

此外,Vue 2 不支持对 Map、Set 等 ES6 数据结构的响应式处理

而 Vue 3 使用 Proxy 实现响应式,具备更强的能力:

  • 能监听新增、删除属性
  • 完整支持数组下标变动
  • 支持对Map、Set 等新数据结构的响应式追踪

v-if 和 v-for 的优先级哪个高?

  • Vue 2中 v-for 的优先级更高。
  • Vue 3中 v-if 的优先级更高。

Vite 常用的配置

import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import { UserConfig, ConfigEnv, loadEnv, defineConfig } from "vite";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolver";
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
import mockDevServerPlugin from "vite-plugin-mock-dev-server";
import { resolve } from "path";
import {
  name,
  version,
  engines,
  dependencies,
  devDependencies,
} from "./package.json";

/* 平台的名称、版本、运行时所需的 `node` 版本、依赖、构建时间的类型提示 */
const __APP_INFO__ = {
  pkg: { name, version, engines, dependencies, devDependencies },
  buildTimestamp: Date.now(),
};

const pathSrc = resolve(__dirname, "src");

export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
  const env = loadEnv(mode, process.cwd());
  const isPrd = mode === "production";
  return {
    define: {
      __APP_INFO__: JSON.stringify(__APP_INFO__),
    },
    resolve: {
      alias: {
        "@": pathSrc,
      },
    },
    css: {
      // CSS 预处理器
      preprocessorOptions: {
        // 定义全局 SCSS 变量
        scss: {
          api: "modern",
          javascriptEnabled: true,
          additionalData: `
                @use "@/styles/variables.scss" as *;
              `,
        },
      },
    },
    server: {
      // 允许 IP 访问
      host: "0.0.0.0",
      // 应用端口(默认:3000)
      port: Number(env.VITE_APP_PORT),
      // 运行时是否自动打开浏览器
      open: true,
      proxy: {
        // 代理前缀为 /api 的请求
        [env.VITE_APP_BASE_API]: {
          changeOrigin: true,
          target: env.VITE_APP_BASE_API,
          rewrite: (path) =>
            path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""),
        },
      },
    },
    plugins: [
      vue(),
      // jsx, tsx 语法支持
      vueJsx(),
      // MOCK 服务
      env.VITE_MOCK_DEV_SERVER === "true" ? mockDevServerPlugin() : null,
      AutoImport({
        // 自动导入官方 vue
        imports: ["vue", "@vueuse/core", "pinia", "vue-router"],
        eslintrc: {
          // 是否自动生成 eslint 规则
          enabled: false,
          // 指定自动导入函数 eslint 规则的文件
          filepath: "./.eslintrc-auto-import.json",
          globalsPropValue: true,
        },
        // 是否在 vue 模版中自动导入
        vueTemplate: true,
        dts: "./src/types/auto-imports.d.ts",
      }),
      Components({
        resolvers: [
          IconsResolver({
            // 需配合安装: @iconify-json/ic, @iconify-json/mdi
            enabledCollections: ["ic", "mdi"],
          }),
        ],
        dts: false,
      }),
      Icons({
        // 自动安装图标库
        autoInstall: true,
      }),
      createSvgIconsPlugin({
        // 指定需要缓存的图标文件夹
        iconDirs: [resolve(pathSrc, "assets/svg")],
        // 指定 symbolId 格式
        symbolId: "svg-[dir]-[name]",
      }),
    ],
    esbuild: {
      drop: isPrd ? ["console", "debugger"] : [],
    },
    // 构建配置
    build: {
      chunkSizeWarningLimit: 2000, // 消除打包大小超过500kb警告
      rollupOptions: {
        output: {
          // 用于入口点生成的块的输出格式,[name] 表示文件名, [hash] 表示该文件的内容 hash 值
          entryFileNames: "js/[name].[hash].js",
          // 用于命名代码拆分时创建的共享块的输出命名
          chunkFileNames: "js/[name].[hash].js",
          // 用于输出静态资源的命名,[ext] 表示文件的扩展名
          assetFileNames: (assetInfo: any) => {
            const info = assetInfo.name.split(".");
            let extType = info[info.length - 1];
            if (
              /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)
            ) {
              extType = "media";
            } else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetInfo.name)) {
              extType = "img";
            } else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
              extType = "fonts";
            }
            return `${extType}/[name].[hash].[ext]`;
          },
        },
      },
    },
  };
});

1. 配置 CSS 预处理器

定义全局 SCSS 变量。

css: {
  preprocessorOptions: {
    scss:{
      api: "modern",
      javascriptEnabled: true,
      additionalData: `
        @use "@/styles/variables.scss" as *;
      `,
    }
  }
}

2. createSvgIconsPlugin 使用

  1. main.ts 中引入以下模块
import "vertual:svg-icons-register";
  1. 将 SVG 文件放入指定目录,例如:assets/svg。假设你添加了一个名为 clothes.svg 的图标文件。

  2. 在 Vue 组件中,可以这样使用该图标:

<template>
  <svg
    aria-hidden="true"
    class="xxxx"
    stroke="#f00"
    :strokeWidth="10"
    :style="{ width: 20, height: 20 }"
  >
    <use xlink:href="#svg-clothes" fill="#ff0" />
  </svg>
</template>

3.plugins

  • unplugin-auto-import 自动导入常用的 Vue API 和其他库,如 Piniavue-router 等。
  • unplugin-vue-componentsunplugin-icons/vite 配合使用,自动解析和导入 iconify 的 SVG 图标,并且使用 pnpm 安装 @iconify-json/ic@iconify-json/mdi

4. .env.development 和 .env.production 配置

.env.development 配置说明:

# 本地开发环境应用端口
VITE_APP_PORT = 3000

# 开发环境下的 API 请求前缀
VITE_APP_BASE_API = "/api"

# 开发环境下实际请求的 API 服务地址
VITE_APP_API_URL = http://172.190.xxx.xx:8083

# 是否启用 mock 服务
VITE_MOCK_DEV_SERVER = true

.env.production 配置说明:

# 生产环境接口前缀
VITE_APP_BASE_API = '/api'

5. build 构建配置

配置 chunks 和 assets 输出目录等。

Vue2 组件中 data 为什么是一个函数?

因为组件是用来复用的,且 JS 里对象是引用关系。

Vue 组件全局注册

Vue2:

import Vue from 'vue'
import MyComponent from './MyComponent.vue'

// 全局注册组件
Vue.component('my-component', MyComponent)

Vue3:

import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './components/MyComponent.vue'

const app = createApp(App)

// 全局注册组件
app.component('my-component', MyComponent)

app.mount('#app')

MVVM 的理解

MVVM 是 Model-View-ViewModel 的缩写。Model 层代表数据模型,View 代表 UI 组件,通过 ViewModel 把 Model 和 View 连接起来。

Vue 实现双向数据绑定原理是什么?

Vue 的数据双向绑定整合了 Observer,Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 的数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化->视图更新,视图交互变化(例如 input 操作)->数据 model 变更的双向绑定效果。

keep-alive 的实现原理

keep-alive 在内部维护了一个 key 数组和一个缓存对象。

nextTick 的实现原理

Vue 的 DOM 更新是异步进行的。当数据发生变化时,DOM 并不会立刻更新。Vue 会在 DOM 更新完成后,再统一执行相关回调。nextTick 的实现原理是通过判断当前环境支持的异步队列机制,优先使用微任务(如 Promise 和 MutationObserver),在不支持的情况下则回退到宏任务(如 setImmediate 或 setTimeout)来实现回调的延迟执行。

watch 与 computed 的区别是什么?

  • computed 有缓存,它依赖的值变了才会重新计算,watch 没有;
  • watch 支持异步,computed 不支持;
  • watch 监听某一个值变化,执行对应操作;computed 是监听属性依赖于其他属性。

Vue 实现双向数据绑定的原理

Vue 的双向数据绑定机制由 ObserverCompileWatcher 三个核心模块共同实现:

  • Observer:用于监听数据模型(model)的变化,一旦数据发生变化,就会触发相应的更新操作。
  • Compile:用于解析模板指令(如 v-modelv-bind 等),并将初始模板渲染到页面上,同时将指令中的数据依赖与对应的 Watcher 建立关联。
  • Watcher:作为 Observer 和 Compile 之间的桥梁,负责接收数据变化的通知,并触发视图的更新。

通过这三者的协作,Vue 实现了数据变化 → 视图更新,视图交互(如输入框操作)→ 数据更新的双向绑定效果。

如何监听 pushstate 和 replacestate 的变化呢?

重写 history 的 pushState 和 replaceStat。

var _wr = function (type) {
  var orig = history[type];
  return function () {
    var rv = orig.apply(this, arguments);
    var e = new Event(type);
    e.arguments = arguments;
    window.dispatchEvent(e);
    return rv;
  };
};
history.pushState = _wr("pushState");
history.replaceState = _wr("replaceState");

v-if 和 v-show 的区别

  • v-show: 无论初始条件是什么,始终会被渲染。相当于设置 css 的 display 属性的显示(display: block)和隐藏(display: none)。

  • v-if: 是惰性的,初始条件为 false,则不渲染。元素切换的时候事件监听器和子组件都会被销毁与重建。

Vue3 组件 v-model

v-model 默认绑定的是 modelValue,当你写:

<CustomInput v-model="searchText" />

Vue 实际上转换成:

<CustomInput
  :model-value="searchText"
  @update:model-value="val => searchText = val"
/>

子组件做的两件事

  • 绑定 modelValue<input>value

  • input 事件触发时,通过 $emit('update:modelValue', newValue) 通知父组件更新。

<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

✅ Vue 3 中支持多个 v-model

你也可以这样写:

<MyComponent v-model:title="title" v-model:content="content" />

在组件内对应:

props: ['title', 'content'],
emits: ['update:title', 'update:content']

Vue3 使用组件的两种方式

组合式(<script setup>

<script setup>
import SlotAdvance from "./components/SlotAdvance/SlotAdvance.vue";
</script>

<template>
  <SlotAdvance></SlotAdvance>
</template>

选项式 API(传统写法)

<script>
import SlotAdvance from "./components/SlotAdvance/SlotAdvance.vue";
  
export default {
  components: {
    SlotAdvance
  }
};
</script>

<template>
  <SlotAdvance></SlotAdvance>
</template>

❌ 错误写法:两种 API 混用会报错

混用会报错: 一个模块不能具有多个默认导出。ts(2528)

<script setup>
import SlotAdvance from "./components/SlotAdvance/SlotAdvance.vue";
export default {
  components: {
    SlotAdvance
  }
};
</script>