vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > 前端Vue高频面试题

前端高频面试题之Vue高级篇(含详细代码)

作者:程序员小寒

Vue已经成为企业项目开发的热门框架,也是前端面试中常被问到的内容,下面这篇文章主要介绍了前端高频面试题之Vue高级篇的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

1、说一下 Vue.js 的响应式原理

1.1 Vue2 响应式原理

核心原理就是通过 Object.defineProperty 对对象属性进行劫持,重新定义对象的 gettersetter,在 getter 取值时收集依赖,在 setter 修改值时触发依赖更新,更新页面。

Vue2 对数组和对象做了两种不同方式的处理。

监听对象变化:

针对对象来说,Vue 会循环遍历对象的每一个属性,用 defineReactive 重新定义 gettersetter

function defineReactive(target,key,value){
    observer(value);
    Object.defineProperty(target,key,{ ¸v
        get(){
            // ... 收集依赖逻辑
            return value;
        },
        set(newValue){
            if (value !== newValue) {
                value = newValue;
                observer(newValue) // 把新设置的值包装成响应式
            }
            // ...触发依赖更新逻辑
        }
    })
}
function observer(data) {
    if(typeof data !== 'object'){
        return data
    }
    for(let key in data){
        defineReactive(data,key,data[key]);
    }
}

监听数组变化:

我们都知道,数组其实也是对象,同样可以用 Object.defineProperty 劫持数组的每一项,但如果数组有100万项,那就要调用 Object.defineProperty 一百万次,这样的话性能太低了。鉴于平时我们操作数组大都是采用数组提供的原生方法,于是 Vue 对数组重写原型链,在调用7个能改变自身的原生方法(pushpopshiftunshiftsplicesortreverse)时,通知页面进行刷新,具体实现过程如下:

// 先拿到数组的原型
const oldArrayProtoMethods = Array.prototype
// 用Object.create创建一个以oldArrayProtoMethods为原型的对象
const arrayMethods = Object.create(oldArrayProtoMethods)
const methods = [
    'push',
    'pop',
    'shift',
    'unshift',
    'sort',
    'reverse',
    'splice'
]
methods.forEach(method => {
    // 给arrayMethods定义7个方法
    arrayMethods[method] = function (...args){
        // 先找到数组对应的原生方法进行调用
        const result = oldArrayProtoMethods[method].apply(this, args)
        // 声明inserted,用来保存数组新增的数据
        let inserted
        // __ob__是Observer类实例的一个属性,data中的每个对象都是一个Observer类的实例
        const ob = this.__ob__
        switch(method) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
            default:
                break
        }
        // 比如有新增的数据,新增数据也要被定义为响应式
        if(inserted) ob.observeArray(inserted)
        // 通知页面更新
        ob.dep.notify()
        return result
    }
})

Object.defineProperty的缺点:

  1. 无法监听新增属性和删除属性的变化,需要通过 $set$delete 实现。
  2. 监测数组的索引性能太低,故而直接通过数组索引改值无法触发响应式。
  3. 初始化时需要一次性递归调用,性能较差。

1.2 Vue3 的响应式改进

Vue3 采用 Proxy + Reflect 配合实现响应式。能解决上述 Object.defineProperty 的所有缺陷,唯一缺点就是兼容性没有 Object.defineProperty 好。

let handler = {
  get(target, key) {
    if (typeof target[key] === "object") {
      return new Proxy(target[key], handler);
    }
    return Reflect.get(target, key);
  },
  set(target, key, value) {
    let oldValue = target[key];
    if (oldValue !== value) {
      return Reflect.set(target, key, value);
    }
    return true;
  },
};
let proxy = new Proxy(obj, handler);

2、介绍一下 Vue 中的 diff 算法?

Vue 的 diff 算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。

比较过程:

  1. 先比较是否是相同节点。
  2. 相同节点比较属性,并复用老节点。
  3. 比较儿子节点,考虑老节点和新节点儿子的情况。
  4. 优化比较:头头、尾尾、头尾、尾头。
  5. 比对查找进行复用。

Vue3 在这个比较过程的基础上增加了最长递增子序列实现diff算法。

3、Vue 的模板编译原理是什么?

Vue 中的模板编译就是把我们写的 template 转换为渲染函数(render function) 的过程,它主要经历3个步骤:

  1. 解析(Parse):将 template 模板转换成 ast 抽象语法树。
  2. 优化(Optimize):对静态节点做静态标记,减少 diff 过程中的比对。
  3. 生成(Generate):重新生成代码,将 ast 抽象语法数转化成可执行的渲染函数代码。

3.1 解析阶段

<div id="app">
  <p>{{ message }}</p>
</div>

3.2 优化阶段

3.3 代码生成阶段

vue2 解析结果:

function render() {
  with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_c('p', [_v(_s(message))])])
  }
}

vue3 解析结果:

import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", { id: "app" }, [
    _createElementVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ]))
}

vue2在线编译:template-explorer.vuejs.org/

vue3在线编译:v2.template-explorer.vuejs.org/

运行时+编译(runtime-compiler) vs 仅运行时(runtime-only):

  1. 完整版(运行时+编译):
    • 包含编译模块,可以写 template 模版。
    • 体积较大(~30kb)。
  2. 仅运行时版本
    • 需要在打包时使用 vue-loader 进行编译。
    • 体积较小(~20kb)。

平时开发项目推荐使用仅运行时(runtime-only)版本。

编译后的特点:

  1. 虚拟DOM:渲染函数生成的是虚拟DOM节点(VNode)。
  2. 响应式绑定:渲染函数中的变量会自动建立依赖关系。
  3. 性能优化:通过静态节点标记减少不必要的更新。

4、v-show 和 v-if 的原理

简单来说,v-if 内部是通过一个三元表达式来实现的,而 v-show 则是通过控制 DOM 元素的 display 属性来实现的。

v-if 源码:

function genIfConditions (
    conditions: ASTIfConditions,
    state: CodegenState,
    altGen?: Function,
    altEmpty?: string
    ): string {
    if (!conditions.length) {
        return altEmpty || '_e()'
    }
    const condition = conditions.shift()
    if (condition.exp) {   // 如果有表达式
        return `(${condition.exp})?${ // 将表达式作为条件拼接成元素
        genTernaryExp(condition.block)
        }:${
        genIfConditions(conditions, state, altGen, altEmpty)
        }`
    } else {
        return `${genTernaryExp(condition.block)}` // 没有表达式直接生成元素 像v-else
    }

    // v-if with v-once should generate code like (a)?_m(0):_m(1)
    function genTernaryExp (el) {
        return altGen
        ? altGen(el, state)
        : el.once
            ? genOnce(el, state)
            : genElement(el, state)
    }
}

v-show 源码:

{
    bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
    const originalDisplay = el.__vOriginalDisplay =
        el.style.display === 'none' ? '' : el.style.display // 获取原始显示值
        el.style.display = value ? originalDisplay : 'none' // 根据属性控制显示或者隐藏
    }  
} 

5、v-if 和 v-for 哪个优先级更高?为什么?

<li v-for="item in arr" v-if="item.visible">
  {{ item}}
</li>

以上代码在 vue3 的编译结果如下:

import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createCommentVNode as _createCommentVNode } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_ctx.item.visible)
    ? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(_ctx.arr, (item) => {
        return (_openBlock(), _createElementBlock("li", null, _toDisplayString(item), 1 /* TEXT */))
      }), 256 /* UNKEYED_FRAGMENT */))
    : _createCommentVNode("v-if", true)
}

可以看出 vue3 在编译时会先判断 v-if,然后再走 v-for 的循环,所以在 v-if 中自然就无法访问 v-for 作用域名中定义的变量别名。

这样的写法在 vue3 中会抛出一个警告⚠️,[Vue warn]: Property "item" was accessed during render but is not defined on instance,导致渲染失败。

以上代码在 vue2 还不能直接编译,因为 vue2 的组件需要一个根节点,所以我们在外层加一个 div

<div>
  <li v-for="item in arr" v-if="item.visible">
    {{ item}}
  </li>
</div>

其编译结果如下:

function render() {
  with(this) {
    return _c('div', _l((arr), function (item) {
      return (item.visible) ? _c('li', [_v("\n    " + _s(item) + "\n  ")]) :
        _e()
    }), 0)
  }
}

很明显是先循环 arr,然后每一项再用 item.visible 去判断的,也印证了在 vue2 中, v-for 的优先级高于 v-if

所以不管是 vue2 还是 vue3,都不推荐同时使用 v-ifv-for,更好的方案是采用计算属性,或者在外层再包裹一个容器元素,将 v-if 作用在容器元素上。

6、nextTick 的原理是什么?

6.1 Vue2 的 nextTick:

这里体现了优雅降级的思想。

6.2 Vue3 的 nextTick:

7、Vue.set 方法是如何实现的?

Vue2的实现:在 Vue 2 中,Vue.set 的实现主要位于 src/core/observer/index.js 中:

export function set (target: Array | Object, key: any, val: any): any {
    // 1.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val)
        return val
    }
    // 2.如果是对象本身的属性,则直接添加即可
    if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
    }
    const ob = (target: any).__ob__
    // 3.如果是响应式的也不需要将其定义成响应式属性
    if (!ob) {
        target[key] = val
        return val
    }
    // 4.将属性定义成响应式的
    defineReactive(ob.value, key, val)
    // 5.通知视图更新
    ob.dep.notify()
    return val
}

Vue3 中 set 方法已经被移除,因为 proxy 天然弥补 vue2 响应式的缺陷。

8、Vue.use 是干什么的?原理是什么?

Vue.use 是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等。

Vue.use 源码:

Vue.use = function (plugin: Function | Object) {
    // 插件不能重复的加载
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
        return this
    }
    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)  // install方法的第一个参数是Vue的构造函数,其他参数是Vue.use中除了第一个参数的其他参数
    if (typeof plugin.install === 'function') { // 调用插件的install方法
        plugin.install.apply(plugin, args)  Vue.install = function(Vue,args){}
    } else if (typeof plugin === 'function') { // 插件本身是一个函数,直接让函数执行
        plugin.apply(null, args) 
    }
    installedPlugins.push(plugin) // 缓存插件
    return this
}

9、介绍下 Vue 中的 mixin,Vue3 为何不再推荐使用它?

mixin 是 Vue 2 中一种复用组件逻辑的方式,允许将可复用的配置(data、methods、computed、lifecycle hooks 等)抽离成一个对象,然后通过 mixins: [] 合并到组件中。支持全局注入和局部注入。

mixin 的优点:

mixin 中有很多缺陷:

vue3 不再推荐使用它的理由如下:

问题说明
1. 隐式依赖 & 数据来源不明确组件行为来自多个 mixin,难以追踪 data、methods 是从哪里来的。
2. 命名冲突多个 mixin 可能定义同名 data、methods,合并规则复杂(同名 methods 后者覆盖前者,data 合并为对象,同名 Key 后者覆盖前者)。
3. 调试和维护困难父组件无法知道子组件内部有哪些 mixin 注入的属性,排查 bug 和调试困难。
4. 不利于 Tree-shaking打包时难以移除未使用的 mixin 代码。
5. 与 Composition API 理念冲突Mixin 是“横切关注点”,而 Composition API 强调显式、可组合的逻辑。

Vue 3 推荐替代方案:Composition API + 可复用函数(Composables)。

特性MixinComposables
数据来源明确隐式显式(import)
是否有命名冲突问题
逻辑封装全局污染按需引入
Tree-shaking 支持
TypeScript 支持

对于全局混入(Global Mixin),Vue3 虽然提供了 app.mixin(),但不推荐,推荐使用:

  1. app.config.globalProperties
  2. app.provide 在顶层提供数据,组件通过 inject 方法消费数据。

10、介绍下 Vue.js 中的函数式组件、异步组件和递归组件

10.1 函数式组件(Functional Components)

函数式组件是一种轻量级、无状态的组件形式。它们很像纯函数:接收 props,返回 虚拟 DOM(vnode)。函数式组件在渲染过程中不会创建组件实例 (也就是说,没有 this),也不会触发常规的组件生命周期钩子。没有响应式系统、生命周期和实例的开销,函数式组件自然在渲染上更加高效和快速。

总而言之,函数式组件有无状态无this无生命周期性能更高等特点。

使用场景:适合简单、静态的 UI 元素,如列表项或包装组件。

在 Vue 2 中,通过 functional: true 声明;在 Vue 3 中,函数式组件更简单,直接返回渲染函数。

Vue2 示例:

<template functional>
  <div>{{ props.msg }}</div>
</template>

或者 js 形式:

export default {
  functional: true,
  props: ['msg'],
  render(h, { props }) {
    return h('div', props.msg);
  }
};

Vue3 示例:

<script setup>
import { h } from 'vue';

const FunctionalComp = (props) => h('div', props.msg);
</script>

<template>
  <FunctionalComp msg="Hello Functional" />
</template>

10.2 异步组件(Async Components)

异步组件是一种懒加载(Lazy Loading)机制,用于按需加载组件代码,优化初始加载时间和性能。Vue 会动态导入组件,只有在使用时才下载和渲染,常用于路由或大型组件。

特点:

Vue2 示例:

<script>
export default {
  components: {
    AsyncComp: () => import('./AsyncComp.vue')
  }
};
</script>

<template>
  <AsyncComp />
</template>

Vue3 示例:

<script setup>
import { defineAsyncComponent } from 'vue';
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'));
</script>
<template>
  <Suspense>
    <template #default>
      <AsyncComp />
    </template>
    <template #fallback>加载中...</template>
  </Suspense>
</template>

10.3 递归组件(Recursive Components)

递归组件是指组件内部调用自身,用于处理树形或嵌套数据结构,如菜单、树视图或评论回复。Vue 支持组件自引用,但需注意避免无限循环(通过条件终止递归)。

特点:

Vue 2 示例:

<template>
  <ul>
    <li v-for="item in tree" :key="item.id">
      {{ item.name }}
      <Tree v-if="item.children" :tree="item.children" />
    </li>
  </ul>
</template>

<script>
export default {
  name: 'Tree',  // 必须有 name
  props: ['tree']
};
</script>

Vue 3 示例:

<script setup>
import { defineAsyncComponent } from 'vue';  // 可选:异步加载避免循环

const Tree = defineAsyncComponent(() => import('./Tree.vue'));  // 自引用
defineProps(['tree']);
</script>

<template>
  <ul>
    <li v-for="item in tree" :key="item.id">
      {{ item.name }}
      <Tree v-if="item.children" :tree="item.children" />
    </li>
  </ul>
</template>

11、Vue.js 中的 vue-loader 是什么?

Vue-loader 是一个专为 Vue.js 设计的 Webpack loader(加载器),其主要作用是将 Vue 的单文件组件(Single-File Components,简称 SFC,即 .vue 文件)转换为可执行的 JavaScript 模块。 它允许开发者以一种结构化的方式编写组件,将模板(template)、脚本(script)和样式(style)封装在同一个文件中,便于管理和维护。

核心功能:

12、Vue.extend 方法的作用?

Vue.extend方法可以作为基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

Vue2 示例:

var dialog = Vue.extend({
  template: "<div>{{hello}} {{world}}</div>",
  data: function () {
    return {
      hello: "hello",
      world: "world",
    };
  },
});
// 创建 dialog 实例,并手动挂载到一个元素上。
new dialog().$mount("#app");

注意:在 Vue.extend 中的 data 必须是一个函数,要不然会报错。

Vue3 示例:

Vue3 中不在使用 Vue.extend 方法,而是采用render方法进行手动渲染。

<!-- Modal.vue -->
<template>
  <div class="modal">这是一个弹窗</div>
</template>

<script>
export default {
  name: 'Modal',
}
</script>
<template>
  <div id="box"></div>
</template>

<script setup>
import { h, render, createApp, onMounted } from 'vue'
import Modal from './Modal.vue'

onMounted(() => {
  render(h(Modal), document.getElementById('box'));
})
</script>

13、keep-alive 的原理

<keep-alive> 是 Vue.js 的内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例,避免重复渲染和状态丢失,提高性能。它是一个抽象组件(abstract: true),不会渲染到 DOM,也不会出现在组件树中,而是通过插槽(slots)和自定义 render 函数实现缓存逻辑。

核心实现机制:

  1. 抽象组件与 Render 函数:
  1. 缓存存储:
  1. LRU 缓存算法:
  1. 过滤规则(include/exclude):
  1. 生命周期钩子:

注意事项:

  • 只缓存一个直接子组件(插槽内容),不支持多个。
  • Vue 3 中原理类似,但优化了 VNode 处理和 Composition API 支持。
  • 潜在问题:缓存过多导致内存占用;需手动清理资源(如在 deactivated 中停止监听)。

14、Vue.js 中使用了哪些设计模式?

1. 观察者模式 (Observer Pattern)

2. 发布-订阅模式 (Publish-Subscribe Pattern)

3. 代理模式 (Proxy Pattern)

描述:Vue 3 的响应式系统直接使用 ES6 Proxy 作为代理层,拦截对象操作,实现透明的响应式。 应用:reactive() 返回 Proxy 对象,代理目标对象的访问和修改。 优势:比 Vue 2 的 defineProperty 更强大,支持数组和 Map/Set 等类型。

4. 策略模式 (Strategy Pattern)

5. 单例模式(Singleton Pattern)

6. 工厂模式(Factory Pattern)

15、Vue.js 应用中常见的内存泄漏来源有哪些?

  1. 未清理的事件监听器、定时器、动画
<script setup>
import { onMounted, onUnmounted } from 'vue';

let timer = null;
let controller = null;
let raf = null;

onMounted(() => {
  // 定时器
  timer = setInterval(() => {}, 1000);
  // 动画
  raf = requestAnimationFrame(() => {});
  // 事件监听
  window.addEventListener('resize', handleResize);
});

onUnmounted(() => {
  clearInterval(timer);
  cancelAnimationFrame(this.raf);
  window.removeEventListener('resize', handleResize);
});
</script>
  1. 未移除的第三方库实例
<script setup>
import { onMounted, onUnmounted } from 'vue';

let chart = null;

onMounted(() => {
  chart = echarts.init(this.$refs.chart);
});

onUnmounted(() => {
  chart?.dispose();
});
</script>
  1. 事件总线(Event Bus)未解绑

vue2 用可以用 new Vue 全局创建一个事件总线实例,或者在组件中直接使用 this.$onthis.$emitthis.$off

vue3 则需要借助第三库,比如 mitt 来实现事件总线。

<script setup>
import { onMounted, onUnmounted } from 'vue';
import mitt from 'mitt';

// 创建事件总线实例
const emitter = mitt();

onMounted(() => {
  emitter.on('update', this.handler);
});

onUnmounted(() => {
  emitter.off('update', this.handler);
});
</script>

顺便提一下, vue3 为啥去掉 $on、$emit、$off 这些 API,主要有以下原因:

  1. 设计理念的调整

Vue 3 更加注重组件间通信的明确性和可维护性。$on 这类事件 API 本质上是一种 "发布 - 订阅" 模式,容易导致组件间关系模糊(多个组件可能监听同一个事件,难以追踪事件来源和流向)。Vue 3 推荐使用更明确的通信方式,如:

  1. 与 Composition API 的适配

Vue 3 主推的 Composition API 强调逻辑的封装和复用,而 $on 基于选项式 API 的实例方法,与 Composition API 的函数式思维不太契合。移除后,开发者可以更自然地使用响应式变量或第三方事件库来实现类似功能。

  1. 减少潜在问题

4. 未清理的 Watcher

Vue 本身不会泄漏内存,泄漏几乎都来自开发者未清理的副作用。养成“创建即清理”的习惯,使用 beforeDestroy 或者 onUnmounted 集中清理,在使用 keep-alive 的组件中,视情况在 deactivated 钩子中清理资源。

16、Vue.js 中的性能优化手段有哪些?

16.1 数据相关

16.2 组件相关

16.3 渲染相关

结语

以上是整理的 Vue 高级的高频面试题,如有错误或者可以优化的地方欢迎评论区指正,后续还会更新 Vuex 和 Vue-router 相关面试题。

到此这篇关于前端高频面试题之Vue高级篇的文章就介绍到这了,更多相关前端Vue高频面试题内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文