前端面试准备指南三之Vue篇


一、Vue基础概念

1.1 MVVM模式

MVVMModel-View-ViewModel 缩写,也就是把 MVC 中的 Controller 演变成 ViewModelModel 层代表数据模型,View 代表 UI 组件,ViewModelViewModel 层的桥梁,数据会绑定到 viewModel 层并自动将数据渲染到页面中,视图变化的时候会通知 viewModel 层更新数据。

组成部分 说明
Model 数据模型层,存放业务数据
View 视图层,负责UI展示
ViewModel 视图模型层,作为View和Model的桥梁

核心特点

  • 数据驱动视图:数据变化自动更新视图
  • 双向绑定:视图变化自动同步到数据
  • 低耦合:各层职责清晰,易于维护

1.2 单向数据流

父级 prop 的更新会向下流动到子组件,但反向不行。

优点

  • 防止子组件意外修改父组件状态
  • 数据流向清晰,易于追踪

二、Vue核心机制

2.1 生命周期

每个 Vue 实例在创建时都会经过一系列的初始化过程,Vue 的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件

Vue实例从创建到销毁的完整过程,包含多个钩子函数:

  • create阶段Vue 实例被创建
    • beforeCreate:创建前,此时 datamethods 中的数据都还没有初始化
    • created:创建完毕,data 中有值,未挂载
  • mount阶段Vue 实例被挂载到真实 DOM 节点
    • beforeMount:可以发起服务端请求,获取数据
    • mounted:此时可以操作 DOM,也可以操作 data 中的数据
  • update阶段:当 Vue 实例里面的 data 数据变化时,触发组件的重新渲染
    • beforeUpdate:组件重新渲染前,此时可以操作 data 中的数据
    • updated:组件重新渲染完成
  • destroy/unmount阶段Vue 实例被销毁
    • Vue2:beforeDestroy / destroyed
    • Vue3:beforeUnmount / unmounted
    • 作用:实例被销毁/卸载前和销毁/卸载后,可以执行清理工作(如清除定时器、取消订阅等)

2.2 父子组件生命周期顺序

1. 挂载阶段
该过程主要涉及 beforeCreatecreatedbeforeMountmounted 4 个钩子函数。执行顺序为:
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
一定得等子组件挂载完毕后,父组件才能挂载完毕,所以父组件的 mounted 在最后。

2. 更新阶段
该过程主要涉及 beforeUpdateupdated 2 个钩子函数。注意,当父子组件有数据传递时,才有这个更新阶段执行顺序的比较。执行顺序为:
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated

3. 销毁阶段
该过程主要涉及 beforeDestroydestroyed 2 个钩子函数。执行顺序为:
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

4. 总结
Vue 父子组件生命周期钩子的执行顺序遵循:从外到内创建,从内到外销毁。

2.3 new Vue初始化流程

2.3.1 Vue2初始化流程

initProxy → initLifecycle → initEvents → initRender → initInjections → initState → initProvide → vm.$mount(vm.options.el)

关键步骤

步骤 说明
initProxy 开发环境代理,拦截未定义属性访问并给出警告提示
initLifecycle 建立父子组件关系,在当前组件实例上添加一些属性和生命周期标识。如 $parent, $refs, $children, _isMounted
initEvents 初始化事件监听系统,处理父组件通过 $emit 触发的自定义事件监听
initRender 声明 $slotscreateElement() 等渲染相关的方法
initInjections 注入数据,初始化 inject,一般用于组件更深层次之间的通信
initState (重要)数据响应式:初始化状态。很多选项初始化的汇总:datamethodspropscomputedwatch
initProvide 提供数据,初始化 provide,一般用于组件更深层次之间的通信
vm.$mount(vm.options.el) 挂载实例到DOM

2.3.2 Vue3初始化流程

createApp → createComponent → setup() → initProps → initSlots → setupRenderEffect → mount → mounted

关键步骤

步骤 说明
createApp 创建应用实例,包含 app 对象及其配置
createComponent 创建根组件实例
setup() Composition API 入口,执行 setup 选项或 <script setup>
initProps 初始化 props,将父组件传递的 props 转换为响应式
initSlots 初始化插槽,处理 <slot> 和作用域插槽
setupRenderEffect 设置渲染副作用,建立响应式系统和组件更新机制
mount 将虚拟 DOM 挂载到真实 DOM
mounted 挂载完成,触发 onMounted 钩子

为什么先inject后provide(初始化顺序)?

  1. 注入(inject)先于状态(state)初始化

    • initInjectionsinitState 之前执行,这样注入的数据可以在 datacomputedmethods 等中被使用
    • 例如:可以在 data() 中使用 this.injectedValue 来初始化响应式数据
  2. 提供(provide)在状态初始化之后

    • initProvideinitState 之后执行,因为 provide 通常需要访问组件实例上的响应式数据
    • 例如:provide: { foo: () => this.count },这里的 this.count 需要在 initState 之后才能访问
  3. 父子组件的初始化顺序

    • 父组件先执行 initInjections 注入祖辈提供的数据
    • 然后父组件执行 initState 初始化自己的状态
    • 接着子组件开始初始化,执行自己的 initInjectionsinitState
    • 子组件完成后,父组件才执行 initProvide
    • 这样设计的目的是确保子组件的 inject 能获取到父组件 provide 的数据,同时避免循环依赖

三、Vue响应式原理

3.1 Vue2双向绑定原理

当一个 Vue 实例创建时,Vue 会遍历 data 选项的属性,用 Object.defineProperty 将它们转为 getter/setter 并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器 Observer ,用来监听所有属性。如果属性发生变化了,就需要告诉订阅者 Watcher 看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器 Dep 来专门收集这些订阅者,然后在监听器 Observer 和订阅者 Watcher 之间进行统一管理的。

使用 Object.defineProperty 实现数据劫持:

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            // 依赖收集
            Dep.target && dep.addSub(Dep.target);
            return val;
        },
        set(newVal) {
            if (newVal !== val) {
                val = newVal;
                // 通知更新
                dep.notify();
            }
        },
    });
}

核心三要素

  • Observer:监听数据变化
  • Dep:依赖收集器
  • Watcher:订阅者,接收更新通知

3.2 Vue3响应式原理

Vue 3 使用 Proxy 代替 Object.defineProperty 来实现响应式系统:

const proxy = new Proxy(obj, {
    get(target, key) {
        // 依赖收集
        track(target, key);
        return target[key];
    },
    set(target, key, value) {
        target[key] = value;
        // 触发更新
        trigger(target, key);
        return true;
    },
});

3.3 Proxy vs Object.defineProperty

Vue2 使用 Object.defineProperty 实现响应式,Vue3 使用 Proxy 重写了响应式系统。两者的核心差异如下:

3.3.1 核心特性对比

特性 Object.defineProperty Proxy
监听范围 单个属性,需要遍历对象 整个对象,无需遍历
数组支持 不支持数组API(pushpop等) 原生支持数组变化
新增属性 不支持,需手动调用 Vue.set 自动支持新增属性
删除属性 不支持,需手动调用 Vue.delete 自动支持删除属性
性能 较差(递归遍历+逐个劫持) 更好(代理一次即可)
兼容性 IE9+ IE不支持
返回值 无返回值,直接修改原对象 返回代理对象,不修改原对象
嵌套对象 需要递归劫持,性能开销大 惰性代理,访问时才劫持

总结:

  1. Object.defineProperty 的作用是劫持一个对象的属性,劫持属性的 gettersetter 方法,在对象的属性发生变化时进行特定的操作。而 Proxy 劫持的是整个对象。
  2. Proxy 会返回一个代理对象,我们只需要操作新对象即可,而 Object.defineProperty 只能遍历对象属性直接修改。
  3. Object.defineProperty 不支持数组,更准确的说是不支持数组的各种 API,因为如果仅仅考虑 arr[i] = value 这种情况,是可以劫持的,但是这种劫持意义不大。而 Proxy 可以支持数组的各种 API。

3.3.2 实现原理差异

Object.defineProperty 的局限性

// Vue2响应式实现
function defineReactive(obj, key, val) {
    // 递归处理嵌套对象
    if (typeof val === "object") {
        observe(val);
    }

    Object.defineProperty(obj, key, {
        get() {
            // 依赖收集
            Dep.target && dep.addSub(Dep.target);
            return val;
        },
        set(newVal) {
            if (newVal !== val) {
                // 新值是对象时需要重新劫持
                if (typeof newVal === "object") {
                    observe(newVal);
                }
                val = newVal;
                dep.notify();
            }
        },
    });
}

问题

  1. 初始化性能差:需要递归遍历所有属性
  2. 新增属性无法监听obj.newKey = value 无法触发响应
  3. 数组API无法监听arr.push()arr.pop() 等操作不会触发 setter
  4. 嵌套对象重复劫持:每次赋值新对象都需要重新递归

Proxy 的优势

// Vue3响应式实现
function reactive(target) {
    const handler = {
        get(target, key, receiver) {
            // 依赖收集
            track(target, key);

            // 惰性代理:访问时才处理嵌套对象
            const result = Reflect.get(target, key, receiver);
            if (isObject(result)) {
                return reactive(result);
            }
            return result;
        },
        set(target, key, value, receiver) {
            const oldValue = target[key];
            const result = Reflect.set(target, key, value, receiver);

            // 仅在值真正改变时触发更新
            if (hasChanged(value, oldValue)) {
                trigger(target, key, value, oldValue);
            }
            return result;
        },
        deleteProperty(target, key) {
            const hadKey = hasOwn(target, key);
            const result = Reflect.deleteProperty(target, key);
            if (hadKey) {
                trigger(target, key, undefined);
            }
            return result;
        },
    };

    return new Proxy(target, handler);
}

优势

  1. 性能更好:无需递归遍历,惰性代理按需处理
  2. 自动监听新增/删除:直接拦截对象操作
  3. 原生支持数组:所有数组API都能被拦截
  4. 返回代理对象:不修改原对象,更安全

3.3.3 数组处理特殊方案

由于 Object.defineProperty 无法监听数组API,Vue2采用了重写数组方法的方案:

// Vue2重写数组方法
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);

// 重写7个会改变数组的方法
["push", "pop", "shift", "unshift", "splice", "sort", "reverse"].forEach(method => {
    arrayMethods[method] = function (...args) {
        const result = arrayProto[method].apply(this, args);
        // 手动触发更新
        dep.notify();
        return result;
    };
});

局限性

  • 仅支持7种方法,直接修改数组索引(arr[0] = value)仍无法监听
  • 需要额外维护数组方法的重写逻辑

3.3.4 为什么Vue3 选择Proxy

原因 说明
性能提升 惰性代理避免了初始化时的递归开销
功能完善 原生支持新增/删除属性、数组操作
代码简洁 无需特殊处理数组,逻辑更统一
未来兼容 Proxy是ES6标准,代表未来发展方向

3.3.5 兼容性权衡

场景 推荐方案
现代浏览器 使用Proxy(Vue3默认)
需要IE支持 使用Object.defineProperty(Vue2)
混合场景 提供polyfill或降级方案

总结Proxy在功能和性能上全面优于Object.defineProperty,是响应式系统的更优选择。Vue3选择Proxy是技术演进的必然结果。

3.4 v-model原理

v-model是语法糖,用于在表单元素和组件上创建双向数据绑定。其本质是属性绑定和事件监听的组合。

3.4.1 核心原理

版本 绑定方式 组件实现 编译后原理
Vue2 :value + @input props: ['value'] + this.$emit('input', val) :value="msg" @input="msg = $event.target.value"
Vue3 :modelValue + @update:modelValue defineProps(['modelValue']) + emit('update:modelValue', val) :modelValue="msg" @update:modelValue="msg = $event"

3.4.2 Vue2中的v-model

表单元素使用

<!-- 输入框 -->
<input v-model="message" />
<!-- 等价于 -->
<input :value="message" @input="message = $event.target.value" />

<!-- 复选框 -->
<input type="checkbox" v-model="checked" />
<!-- 等价于 -->
<input type="checkbox" :checked="checked" @change="checked = $event.target.checked" />

自定义组件实现







3.4.3 Vue3中的v-model

表单元素使用

<!-- 输入框 -->
<input v-model="message" />
<!-- 等价于 -->
<input :modelValue="message" @update:modelValue="message = $event" />

自定义组件实现







3.4.4 Vue3多v-model支持

Vue3允许在同一个组件上使用多个v-model:

<!-- 父组件 -->
<ChildComponent v-model:title="title" v-model:content="content" />

<!-- 等价于 -->
<ChildComponent
    :title="title"
    @update:title="title = $event"
    :content="content"
    @update:content="content = $event"
/>

子组件实现