欢迎光临
我们一直在努力

Vue2 与 Vue3 核心原理对比 - 面试宝典

一、响应式系统原理

面试问题1:Vue2 和 Vue3 的响应式原理有什么区别?

详细解答:

Vue2 响应式原理(Object.defineProperty)

Vue2 使用 Object.defineProperty 实现响应式:

// Vue2 响应式实现示例
function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖收集器

Object.defineProperty(obj, key, {
get() {
console.log(`获取 ${key}: ${val}`)
if (Dep.target) {
dep.depend() // 收集依赖
}
return val
},
set(newVal) {
if (newVal === val) return
console.log(`设置 ${key}: ${newVal}`)
val = newVal
dep.notify() // 通知更新
}
})
}

Vue2 响应式的局限性:

  • 无法检测对象属性的添加和删除
  • // Vue2 中需要使用 $set
    this.$set(this.obj, 'newKey', 'value')
    // 或者
    Vue.set(this.obj, 'newKey', 'value')

  • 无法检测数组索引和长度的变化
  • // 不会触发响应式
    this.items[0] = 'newValue'
    this.items.length = 0

    // 需要使用特殊方法
    this.$set(this.items, 0, 'newValue')
    this.items.splice(0, 1)

  • 需要递归遍历对象所有属性
    • 初始化时性能开销大
    • 深层嵌套对象会有性能问题
  • Vue3 响应式原理(Proxy)

    Vue3 使用 Proxy 实现响应式:

    // Vue3 响应式实现示例
    function reactive(target) {
    return new Proxy(target, {
    get(target, key, receiver) {
    const result = Reflect.get(target, key, receiver)
    console.log(`获取 ${key}: ${result}`)
    track(target, key) // 依赖收集

    // 懒递归:只有访问到才会递归处理
    if (typeof result === 'object' && result !== null) {
    return reactive(result)
    }
    return result
    },

    set(target, key, value, receiver) {
    const oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver)

    if (oldValue !== value) {
    console.log(`设置 ${key}: ${value}`)
    trigger(target, key) // 触发更新
    }
    return result
    },

    deleteProperty(target, key) {
    const result = Reflect.deleteProperty(target, key)
    trigger(target, key) // 删除也能触发更新
    return result
    }
    })
    }

    Vue3 响应式的优势:

  • 可以检测属性的添加和删除
  • // Vue3 中直接添加属性即可
    state.newKey = 'value' // 自动响应式
    delete state.someKey // 删除也是响应式的

  • 可以检测数组索引和长度的变化
  • state.items[0] = 'newValue' // 自动响应式
    state.items.length = 0 // 自动响应式

  • 性能优化

    • 懒递归:只有访问到深层属性时才会递归处理
    • 初始化速度更快
    • 内存占用更少
  • 支持更多数据类型

  • // 支持 Map, Set, WeakMap, WeakSet
    const state = reactive(new Map())
    state.set('key', 'value') // 响应式

    性能对比
    特性Vue2Vue3
    初始化性能 需要递归遍历所有属性 懒递归,按需处理
    运行时性能 getter/setter 调用开销 Proxy 拦截开销更小
    内存占用 每个属性都需要闭包 Proxy 对象级别拦截
    新增属性 需要 $set 自动响应式
    数组操作 需要特殊处理 自动响应式

    二、组合式 API vs 选项式 API

    面试问题2:为什么 Vue3 推出 Composition API?它解决了什么问题?

    详细解答:

    Options API 的痛点(Vue2)
  • 逻辑分散问题
  • // Vue2 Options API – 相关逻辑分散在不同选项中
    export default {
    data() {
    return {
    // 用户相关
    userName: '',
    userAge: 0,

    // 产品相关
    productList: [],
    productCount: 0
    }
    },

    computed: {
    // 用户相关计算属性
    userInfo() {
    return `${this.userName}${this.userAge}`
    },

    // 产品相关计算属性
    totalProducts() {
    return this.productList.length
    }
    },

    methods: {
    // 用户相关方法
    updateUser() { /* … */ },

    // 产品相关方法
    addProduct() { /* … */ }
    },

    mounted() {
    // 用户相关初始化
    this.fetchUser()

    // 产品相关初始化
    this.fetchProducts()
    }
    }

    问题分析:

    • 一个功能的代码被强制分散到 data、methods、computed、lifecycle 中
    • 维护大型组件时需要频繁上下跳转
    • 代码复用困难(mixins 有命名冲突、来源不清等问题)
  • Mixins 的问题
  • // mixin1.js
    export default {
    data() {
    return { count: 0 }
    },
    methods: {
    increment() { this.count++ }
    }
    }

    // mixin2.js
    export default {
    data() {
    return { count: 0 } // 命名冲突!
    },
    methods: {
    increment() { /* 不同实现 */ } // 命名冲突!
    }
    }

    // 组件中使用
    export default {
    mixins: [mixin1, mixin2], // 哪个会生效?不清楚!
    }

    Mixins 的缺陷:

    • 命名冲突
    • 来源不清晰(this.xxx 从哪来的?)
    • 难以传递参数
    • 隐式依赖关系
    Composition API 的解决方案(Vue3)
  • 逻辑组织和复用
  • // composables/useUser.js
    import { ref, computed } from 'vue'

    export function useUser() {
    const userName = ref('')
    const userAge = ref(0)

    const userInfo = computed(() => `${userName.value}${userAge.value}`)

    const updateUser = () => {
    // 更新用户逻辑
    }

    const fetchUser = async () => {
    // 获取用户数据
    }

    return {
    userName,
    userAge,
    userInfo,
    updateUser,
    fetchUser
    }
    }

    // composables/useProduct.js
    import { ref, computed } from 'vue'

    export function useProduct() {
    const productList = ref([])
    const productCount = ref(0)

    const totalProducts = computed(() => productList.value.length)

    const addProduct = () => {
    // 添加产品逻辑
    }

    const fetchProducts = async () => {
    // 获取产品数据
    }

    return {
    productList,
    productCount,
    totalProducts,
    addProduct,
    fetchProducts
    }
    }

    // 组件中使用
    import { useUser } from './composables/useUser'
    import { useProduct } from './composables/useProduct'

    export default {
    setup() {
    // 用户相关逻辑都在一起
    const {
    userName,
    userAge,
    userInfo,
    updateUser,
    fetchUser
    } = useUser()

    // 产品相关逻辑都在一起
    const {
    productList,
    totalProducts,
    addProduct,
    fetchProducts
    } = useProduct()

    onMounted(() => {
    fetchUser()
    fetchProducts()
    })

    return {
    userName,
    userInfo,
    productList,
    totalProducts,
    updateUser,
    addProduct
    }
    }
    }

    优势分析:

    • ✅ 相关逻辑集中在一起
    • ✅ 更好的代码复用(组合函数)
    • ✅ 更好的类型推导(TypeScript 支持)
    • ✅ 来源清晰,没有命名冲突
    • ✅ 可以传递参数
  • 更好的 TypeScript 支持
  • // Vue3 Composition API – 完美的类型推导
    import { ref, computed } from 'vue'

    interface User {
    id: number
    name: string
    email: string
    }

    export function useUser() {
    const user = ref<User | null>(null) // 类型明确

    const userName = computed(() => user.value?.name ?? '') // 自动推导为 string

    const fetchUser = async (id: number): Promise<void> => {
    // 返回类型明确
    }

    return {
    user, // Ref<User | null>
    userName, // ComputedRef<string>
    fetchUser // (id: number) => Promise<void>
    }
    }

    // Vue2 Options API – 类型推导困难
    export default {
    data() {
    return {
    user: null // 类型是什么?不明确
    }
    },
    computed: {
    userName() {
    // this 的类型推导很复杂
    return this.user?.name ?? ''
    }
    }
    }

  • 逻辑提取和测试
  • // Composition API 的函数可以独立测试
    import { useCounter } from './useCounter'

    describe('useCounter', () => {
    it('should increment', () => {
    const { count, increment } = useCounter()
    expect(count.value).toBe(0)
    increment()
    expect(count.value).toBe(1)
    })
    })

    // Options API 需要挂载整个组件才能测试

    对比总结
    特性Options API (Vue2)Composition API (Vue3)
    逻辑组织 按选项类型组织 按功能组织
    代码复用 Mixins(有缺陷) 组合函数(清晰)
    TypeScript 支持有限 完美支持
    可测试性 需要组件实例 函数可独立测试
    学习曲线 简单直观 需要理解响应式原理
    适用场景 简单组件 复杂逻辑、大型应用

    重要提示: Vue3 同时支持 Options API 和 Composition API,可以根据场景选择使用。


    三、编译优化

    面试问题3:Vue3 在编译层面做了哪些优化?

    详细解答:

    1. 静态提升(Static Hoisting)

    Vue2 编译结果:

    // 每次渲染都会重新创建这些 VNode
    function render() {
    return h('div', null, [
    h('p', null, '静态文本'),
    h('p', null, this.dynamicText)
    ])
    }

    Vue3 编译结果:

    // 静态节点被提升到渲染函数外部
    const _hoisted_1 = h('p', null, '静态文本')

    function render() {
    return h('div', null, [
    _hoisted_1, // 复用,不重新创建
    h('p', null, this.dynamicText)
    ])
    }

    性能提升:

    • 减少 VNode 创建开销
    • 减少内存分配
    • 减少 GC 压力
    2. 补丁标记(Patch Flags)

    Vue2: 全量 diff,对比所有属性

    // Vue2 需要对比整个 VNode 树
    function patch(oldVNode, newVNode) {
    // 对比所有属性:tag、props、children 等
    if (oldVNode.tag !== newVNode.tag) { /* … */ }
    if (oldVNode.props !== newVNode.props) { /* … */ }
    if (oldVNode.children !== newVNode.children) { /* … */ }
    }

    Vue3: 标记动态内容,精准更新

    // 编译时标记动态内容
    // <div :id="id" class="static">{{ text }}</div>

    function render() {
    return h('div', {
    id: this.id,
    class: 'static'
    }, this.text, 9 /* TEXT, PROPS */)
    // 9 是 PatchFlag,表示这个节点有动态文本和动态属性
    }

    // 运行时只对比标记的部分
    function patch(n1, n2, flag) {
    if (flag & PatchFlags.TEXT) {
    // 只更新文本
    }
    if (flag & PatchFlags.PROPS) {
    // 只更新属性
    }
    // 跳过静态内容
    }

    PatchFlag 类型:

    export const enum PatchFlags {
    TEXT = 1, // 动态文本
    CLASS = 2, // 动态 class
    STYLE = 4, // 动态 style
    PROPS = 8, // 动态属性(除了 class 和 style)
    FULL_PROPS = 16, // 具有动态 key 的属性
    HYDRATE_EVENTS = 32, // 事件监听器
    STABLE_FRAGMENT = 64, // 稳定的 fragment
    KEYED_FRAGMENT = 128, // 带 key 的 fragment
    UNKEYED_FRAGMENT = 256,// 不带 key 的 fragment
    NEED_PATCH = 512, // 需要 patch
    DYNAMIC_SLOTS = 1024, // 动态插槽
    HOISTED = 1, // 静态提升
    BAIL = 2 // diff 算法应该退出优化模式
    }

    3. 块树优化(Block Tree)

    Vue2: 递归遍历整个组件树

    // Vue2 需要递归遍历查找动态节点
    function findDynamicNodes(vnode) {
    const result = []
    // 递归遍历所有子节点
    traverse(vnode, node => {
    if (isDynamic(node)) {
    result.push(node)
    }
    })
    return result
    }

    Vue3: 扁平化的动态节点数组

    // 编译时收集动态节点
    // <div>
    // <p>静态内容</p>
    // <p>{{ dynamic1 }}</p>
    // <div>
    // <span>静态内容</span>
    // <span>{{ dynamic2 }}</span>
    // </div>
    // </div>

    function render() {
    return (openBlock(), createBlock('div', null, [
    _hoisted_1, // <p>静态内容</p>
    createVNode('p', null, _ctx.dynamic1, 1 /* TEXT */),
    createVNode('div', null, [
    _hoisted_2, // <span>静态内容</span>
    createVNode('span', null, _ctx.dynamic2, 1 /* TEXT */)
    ])
    ]))
    }

    // 动态节点被收集到一个扁平数组中
    block.dynamicChildren = [
    vnode1, // {{ dynamic1 }}
    vnode2 // {{ dynamic2 }}
    ]

    // 更新时只遍历这个数组,不需要递归
    function patchBlockChildren(n1, n2) {
    for (let i = 0; i < n2.dynamicChildren.length; i++) {
    patchElement(n1.dynamicChildren[i], n2.dynamicChildren[i])
    }
    }

    性能对比:

    • Vue2:O(n) 树的递归遍历
    • Vue3:O(m) 动态节点数组遍历(m << n)
    4. 事件监听缓存

    Vue2:

    // 每次渲染都会创建新的事件处理器
    <button @click="handleClick">Click</button>

    // 编译后
    h('button', {
    onClick: this.handleClick // 每次都是新的引用
    })

    Vue3:

    // 事件处理器被缓存
    <button @click="handleClick">Click</button>

    // 编译后
    h('button', {
    onClick: _cache[0] || (_cache[0] = (args) => this.handleClick(args))
    })

    编译优化总结

    Vue3 编译优化带来的性能提升:

    优化技术性能提升适用场景
    静态提升 减少 30-50% VNode 创建 含有静态内容的模板
    Patch Flags 减少 60-80% diff 时间 有动态绑定的元素
    Block Tree 减少 70-90% 遍历开销 复杂嵌套结构
    事件缓存 减少不必要的更新 含有事件处理的组件

    实际效果:

    • 初次渲染快 55%
    • 更新快 133%
    • 内存占用减少 54%

    四、Tree-shaking 支持

    面试问题4:为什么 Vue3 支持 Tree-shaking,Vue2 不支持?

    详细解答:

    Vue2 架构问题

    全局 API 挂载在 Vue 构造函数上:

    // Vue2 源码结构
    import Vue from 'vue'

    // 所有功能都挂载在 Vue 上
    Vue.component()
    Vue.directive()
    Vue.mixin()
    Vue.use()
    Vue.nextTick()
    Vue.observable()
    Vue.set()
    Vue.delete()

    // 即使你只用了其中一个,打包工具也无法判断
    // 因为它们都在 Vue 对象上,Vue 对象被引用了,就都会被打包
    const app = new Vue({
    // 即使我只用了 Vue 构造函数
    // nextTick、mixin 等所有 API 都会被打包进来
    })

    问题分析:

    // Vue2 的全局 API 设计
    function Vue(options) {
    // …
    }

    Vue.nextTick = function() { /* … */ }
    Vue.set = function() { /* … */ }
    Vue.mixin = function() { /* … */ }

    // 打包工具分析:
    // Vue 函数被使用 -> Vue 对象被引用
    // -> Vue 对象上的所有属性都可能被访问
    // -> 无法安全地删除任何属性
    // -> 全部打包!

    Vue2 最小包大小:

    • Runtime + Compiler: ~32KB (gzipped)
    • 即使只用最基础的功能,也需要引入全部代码
    Vue3 架构改进

    基于 ES Module 的函数式 API:

    // Vue3 源码结构 – 独立的导出
    // packages/runtime-core/src/apiLifecycle.ts
    export function onMounted(hook) { /* … */ }
    export function onUpdated(hook) { /* … */ }
    export function onUnmounted(hook) { /* … */ }

    // packages/runtime-core/src/apiWatch.ts
    export function watch(source, cb, options) { /* … */ }
    export function watchEffect(effect, options) { /* … */ }

    // packages/reactivity/src/ref.ts
    export function ref(value) { /* … */ }
    export function computed(getter) { /* … */ }

    // 使用时按需导入
    import { ref, computed, onMounted } from 'vue'

    // 打包工具分析:
    // 只导入了 ref、computed、onMounted
    // -> 其他函数(watch、watchEffect、onUpdated 等)没有被引用
    // -> 可以安全删除
    // -> 只打包使用到的代码!

    Tree-shaking 示例:

    // 示例 1:只使用基础响应式
    import { ref, computed } from 'vue'

    export default {
    setup() {
    const count = ref(0)
    const double = computed(() => count.value * 2)
    return { count, double }
    }
    }

    // 打包结果:只包含 ref 和 computed 的代码
    // onMounted、watch、Transition 等未使用的功能不会被打包

    // 示例 2:使用生命周期
    import { ref, onMounted, onUnmounted } from 'vue'

    export default {
    setup() {
    const data = ref(null)

    onMounted(() => {
    // 挂载逻辑
    })

    onUnmounted(() => {
    // 清理逻辑
    })

    return { data }
    }
    }

    // 打包结果:包含 ref、onMounted、onUnmounted
    // onUpdated、watchEffect 等未使用的 API 不会被打包

    全局 API 的改进:

    // Vue2
    import Vue from 'vue'
    Vue.nextTick(() => { /* … */ })
    // 即使只用 nextTick,整个 Vue 都会被打包

    // Vue3
    import { nextTick } from 'vue'
    nextTick(() => { /* … */ })
    // 只打包 nextTick 相关的代码

    实际包体积对比

    简单应用(只使用响应式和基础渲染):

    框架包体积(gzipped)
    Vue2 ~23KB
    Vue3 ~13KB
    减少 43%

    复杂应用(使用大部分功能):

    框架包体积(gzipped)
    Vue2 ~32KB
    Vue3 ~28KB
    减少 12.5%

    极简应用(仅响应式核心):

    // Vue3 可以只导入响应式核心
    import { reactive, effect } from '@vue/reactivity'

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

    // 包体积:仅 ~4KB (gzipped)
    // Vue2 无法做到这么小

    Tree-shaking 的技术要求

    为什么 Vue3 能 Tree-shake?

  • ES Module 语法
  • // 静态分析:编译时就能确定依赖关系
    import { ref } from 'vue' // ✅ 可分析
    const ref = require('vue').ref // ❌ 运行时才知道

  • 函数式 API
  • // ✅ 独立函数,可以单独引用或删除
    export function ref() { }
    export function computed() { }

    // ❌ 对象属性,无法单独删除
    export default {
    ref() { },
    computed() { }
    }

  • 副作用标记(package.json)
  • {
    "sideEffects": false
    // 告诉打包工具:这个包没有副作用
    // 未使用的代码可以安全删除
    }

    渐进式使用的优势

    // 场景 1:只用响应式系统(如状态管理)
    import { reactive, computed } from 'vue'
    // 打包: ~8KB

    // 场景 2:添加组件渲染
    import { createApp } from 'vue'
    // 打包: ~13KB

    // 场景 3:添加路由和状态管理
    import { createApp } from 'vue'
    import { createRouter } from 'vue-router'
    import { createPinia } from 'pinia'
    // 打包: ~25KB

    // Vue2:无论场景,最小都是 23KB

    总结:

    • Vue3 通过模块化设计实现 Tree-shaking
    • 按需引入,减少最终包体积
    • 对于大型应用优势明显
    • 对于小型应用和库作者特别友好
    赞(0)
    未经允许不得转载:171主机测评 » Vue2 与 Vue3 核心原理对比 - 面试宝典
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址