Vue 深度:响应式 / Diff / Composition API

0 0

前言

做前端架构的人要不要懂 Vue 内部?我的回答是 ——不是去读 @vue/reactivity 源码,而是建立”为什么 Vue 3 比 Vue 2 更稳、更快”的工程判断。原因有三:

  1. 响应式调优需要它。为什么 refreactive 不能直接互换?为什么 computed 缓存何时失效?不懂原理就只能”用模板能跑就行”。
  2. 大项目靠 Vue 守门。ERP 财务系统 200+ 表格列 + 80+ 表单字段,用 Vue 3 的响应式 + Composition API + 类型体操,重构成 Vue 2 重写不了的项目结构
  3. 跨框架迁移需要它。简历里出现 Vue 2 / Vue 3 同时存在,理解 v2→v3 的 Proxy 重构动机,才知道”哪些 v2 写法在 v3 是反模式”。

这一篇是『前端架构修仙路』的第 9 篇。我跳过 reactivity 包源码(不展开 createReactiveObject 的每个分支),用架构图 + 时序图 + 真实案例,把 Vue 3 的 Proxy 响应式 / 依赖追踪 / VNode Diff / Composition API 讲透。下一篇深度聊 Angular 的企业级架构与 DI。

一、Vue 的核心心智:响应式数据驱动

<!-- Vue 心智模型:模板 + 数据 -->
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="count++">+</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const count = ref(0);  // 声明响应式数据
</script>

核心三要素

  1. 响应式数据:用 ref / reactive 包装,框架自动追踪依赖
  2. 模板渲染:模板编译成 render function,数据变化自动更新 DOM
  3. 事件回调:模板里 @click 直接写 JS,不需要 .addEventListener

二、Vue 2 → Vue 3 响应式重构:为什么换 Proxy

2.1 Vue 2 的 Object.defineProperty 痛点

Vue 2 用 Object.defineProperty 实现响应式:

// Vue 2 响应式原理(简化)
function defineReactive(obj, key) {
  let value = obj[key];
  Object.defineProperty(obj, key, {
    get() {
      // 追踪:把当前 watcher 加入依赖
      dep.depend();
      return value;
    },
    set(newVal) {
      // 触发:通知所有依赖更新
      dep.notify();
      value = newVal;
    },
  });
}

三大痛点

  1. 无法检测新增属性vm.newProp = 1 不触发更新(必须 Vue.set
  2. 无法检测数组下标变化arr[0] = 1 / arr.length = 0 不触发
  3. 无法检测 Map / Set:要重写一堆方法(push / pop / shift 等)

2.2 Vue 3 的 Proxy 重构

Vue 3 用 Proxy(ES2015 新特性)替代:

// Vue 3 响应式原理(简化)
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key);   // 追踪:记录哪个 effect 依赖了这个 key
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      const result = Reflect.set(target, key, value);
      trigger(target, key); // 触发:通知所有依赖更新
      return result;
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key);
      trigger(target, key);
      return result;
    },
  });
}

三大改进

  1. 新增 / 删除属性自动响应delete obj.prop 也触发)
  2. 数组下标 / length 变化自动响应
  3. Map / Set / WeakMap / WeakSet 完整支持

性能:Proxy 比 defineProperty 更快——前者是引擎原生实现,后者要遍历每个 key。

三、依赖追踪:track / trigger / effect 三件套

Vue 3 响应式的核心是 三个全局数据结构

// 全局 WeakMap:target → Map<key, Set<effect>>
const targetMap = new WeakMap<object, Map<string | symbol, Set<ReactiveEffect>>>();

function track(target, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
  // 把当前正在运行的 effect 加入依赖
  if (activeEffect) {
    dep.add(activeEffect);
  }
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  if (effects) {
    effects.forEach(effect => effect.run());  // 触发更新
  }
}

3.1 reactive vs ref:什么时候用哪个

import { reactive, ref } from 'vue';

// reactive:对象 / 数组
const state = reactive({ count: 0, user: { name: 'Alice' } });
state.count++;  // 自动响应
state.user.name = 'Bob';  // 嵌套也响应

// ref:基本类型 / 需要整体替换的值
const count = ref(0);
count.value++;  // 注意 .value

架构师规则

  • ref基本类型 + DOM 引用 + 需要整体替换的对象
  • reactive会多个属性独立修改的对象
  • 模板里 ref 自动解包(不需要 .value),reactive 不会

3.2 computed 的缓存机制

import { ref, computed } from 'vue';

const count = ref(0);
const doubled = computed(() => count.value * 2);
// doubled.value 第一次访问 → 执行计算
// doubled.value 第二次访问 → 命中缓存(count 没变)
// count.value = 1 → 缓存失效,下次访问重新计算

生产案例:某 FinSpread 表格 100 列 × 500 行,computed 缓存避免重复计算,渲染性能从 800ms 降到 50ms

3.3 watch vs watchEffect:副作用的选择

// watch:显式依赖,旧值 / 新值
watch(count, (newVal, oldVal) => {
  console.log(`count changed: ${oldVal} → ${newVal}`);
});

// watchEffect:自动追踪所有响应式依赖,初始化时立即跑
watchEffect(() => {
  console.log(`count is ${count.value}`);  // 依赖 count
});

// 停止监听(防止泄漏)
const stop = watch(...);
stop();  // 主动停止

架构师规则能 watchEffect 就 watchEffect——自动追踪更安全(不会漏掉依赖);需要旧值时才用 watch。

四、VNode Diff 算法:Vue 3 的编译期优化

4.1 双端比较算法(Vue 2)

Vue 2 用 双端 diff

  • 头尾 4 个指针同步比较
  • 时间复杂度 O(n)
  • 静态节点每次也要 diff

4.2 Vue 3 的 Block Tree(编译期提速)

Vue 3 编译时给模板加 patchFlag(编译提示),只 diff 动态部分:

// Vue 3 编译产物(简化)
function render(_ctx) {
  return openBlock(), createElementBlock("div", null, [
    createElementVNode("h1", null, "Hello"),  // patchFlag = STATIC(0),跳过 diff
    createElementVNode("p", null, _ctx.count, 1 /* TEXT */),  // patchFlag = TEXT,只 diff 文本
    createBlock("div", null, [
      forEach(_ctx.list, (item) => {
        return createElementVNode("li", { key: item.id }, item.name, 1 /* TEXT */);
      })
    ])
  ]);
}

核心收益静态节点直接跳过 diff,只对带 patchFlag 的动态节点做比较。生产数据:模板 70% 静态节点的项目,渲染速度提升 30-50%

4.3 Block Tree:动态节点边界

Vue 3 在模板编译时自动识别动态节点的边界

<template>
  <div>              <!-- 整个 div 是 block,包含两个动态子节点 -->
    <h1>{{ staticTitle }}</h1>  <!-- 动态文本 -->
    <ul>
      <li v-for="item in list">{{ item.name }}</li>  <!-- 列表是 fragment block -->
    </ul>
  </div>
</template>

编译产物会自动识别 v-if / v-for 创建的子节点,整个 block 在 diff 时扁平化——避免跨 block 的无效比较。

五、Composition API:函数式思维统一

5.1 Options API vs Composition API

// Options API:Vue 2 经典写法,声明式
export default {
  data() {
    return { count: 0, user: null };
  },
  computed: {
    doubled() { return this.count * 2; }
  },
  methods: {
    increment() { this.count++; }
  },
  mounted() { this.fetchUser(); }
};

// Composition API:Vue 3 新写法,函数式
import { ref, computed, onMounted } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const doubled = computed(() => count.value * 2);
    const increment = () => count.value++;
    
    onMounted(() => fetchUser());
    return { count, doubled, increment };
  }
};

5.2 Composition API 的真正价值

维度Options APIComposition API
状态逻辑复用mixin(隐式合并,易冲突)composable 函数(显式调用)
大组件拆分拆组件(重渲染开销大)拆 composable(无渲染开销)
TS 类型推导弱(this 难推导)强(ref / computed 类型清晰)
跨组件代码共享难(必须用 mixin 或 HOC)易(直接 import composable)

生产案例:某 ERP 财务系统 200+ 表格列,每个列的”过滤 / 排序 / 编辑”逻辑用 composable 抽出3 个核心 composable(useTable / useForm / usePermission)支撑了 800+ 页面。Options API 时代这种复用需要 mixin 或 HOC,复杂度高 10 倍

5.3 三个核心 composable 模式

// 1. useAsyncData:异步数据 + loading / error
function useAsyncData(fetcher) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);
  
  const run = async () => {
    loading.value = true;
    try {
      data.value = await fetcher();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  };
  
  onMounted(run);
  return { data, loading, error, run };
}

// 2. useForm:表单状态 + 校验
function useForm(initial, rules) {
  const values = reactive({ ...initial });
  const errors = reactive({});
  
  const validate = (field) => {
    const rule = rules[field];
    if (!rule) return true;
    if (!rule.pattern.test(values[field])) {
      errors[field] = rule.message;
      return false;
    }
    delete errors[field];
    return true;
  };
  
  const submit = async (handler) => {
    const allValid = Object.keys(rules).every(validate);
    if (!allValid) return false;
    await handler(values);
    return true;
  };
  
  return { values, errors, validate, submit };
}

// 3. usePermission:权限检查
function usePermission() {
  const permissions = inject('permissions');
  const can = (action) => permissions.value.includes(action);
  return { can };
}

六、Vapor Mode(Vue 3.5):无 VDOM 的未来

6.1 核心思路

Vapor mode(2024-2025)让 Vue 编译器绕过 VNode Diff编译时直接生成 DOM 操作代码

// Vapor 编译产物(简化,概念演示)
function render(_ctx) {
  // 不创建 VNode,直接操作 DOM
  const div = document.createElement('div');
  const p = document.createElement('p');
  const update = () => p.textContent = _ctx.count;
  _ctx.count.watch(update);  // 状态变化直接更新 p.textContent
  div.appendChild(p);
  return div;
}

6.2 收益

  • Bundle size 减少 50%(不打包 Vue runtime)
  • 首屏渲染快 1.5-3 倍(无 VNode 创建 / diff 开销)
  • 更接近 Solid.js 的细粒度响应式

生产案例:某中后台页面 Vapor mode 重构后,首屏 JS 从 280KB 降到 130KB(-54%)LCP 从 1.8s 降到 1.1s(-39%)

七、跨框架组件复用:Web Components + Vue + React

简历里出现 “8 个跨 Vue/React 通用组件”核心抽象层是 Web Components

// 用 Web Components 写一次,Vue / React / Angular 都可用
class SmartTable extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <table>
        <slot></slot>
      </table>
    `;
  }
}
customElements.define('smart-table', SmartTable);
<!-- Vue 里用 -->
<smart-table :columns="cols"><tr><td>...</td></tr></smart-table>
// React 里用(用 @lit/react 包装)
import { createComponent } from '@lit/react';
const SmartTable = createComponent({ react: SmartTableReact, ... });

架构师规则跨框架组件库的核心是 Web Components 抽象,业务代码再各自用框架 wrapper。避免在 Vue 里硬塞 React 组件库

八、踩坑提醒(资深架构师请重点看)

  1. 不要在 reactive 对象上整体替换state = newState丢失所有响应式追踪必须用 Object.assign(state, newState) 或保持引用稳定
  2. 不要滥用 watch。watch 的隐式依赖(deep / flush)容易踩坑。能 computed 就 computed,只有需要”执行副作用”才 watch
  3. 不要在 SSR 里用 window / document。Nuxt 3 / Vite SSR 默认在 Node 环境执行,直接访问 window 报错。import.meta.client 守护
  4. 不要混淆 ref / reactive 的模板语法ref 自动解包,reactive 不会——模板里 count (ref) 等价于 count.value,但 state.count (reactive) 等价于 state.count切换类型时检查一遍模板
  5. 不要在 v-for 里用 index 当 key<li v-for="(item, i) in list" :key="i"> 在列表 reorder / 增删时会复用错误 DOM 节点,状态错乱。用稳定的唯一 ID

总结

这一篇用 7 个关键事实把 Vue 3 内部机制串起来:

  • Proxy 替代 defineProperty:新增 / 删除属性、数组下标、Map / Set 全部响应式。
  • 依赖追踪三件套:track / trigger / effect 用 WeakMap + Set 实现精确依赖追踪。
  • computed 缓存:依赖不变 → 命中缓存;依赖变 → 失效重算。
  • Block Tree + patchFlag:编译期识别动态节点,只 diff 动态部分,渲染速度提升 30-50%
  • Composition API:函数式 composable 替代 mixin / HOC,大项目复用代码量减少 70%。
  • Vapor mode(Vue 3.5):无 VDOM,首屏 JS 减半,LCP 降 39%。
  • Web Components:跨 Vue / React / Angular 通用组件的核心抽象层。

下一篇:Angular 深度——企业级 DI 体系 + RxJS 异步原语 + 强约定架构,从信锐时期 Ionic + Angular 实战讲起。

5 道重点面试问题方向

Q1(答案):Vue 2 用 Object.defineProperty 实现响应式,Vue 3 为什么改用 Proxy?解决了哪些问题?

AVue 2 痛点:① 无法检测新增属性 —— vm.newProp = 1 不触发更新,必须 Vue.set;② 无法检测数组下标变化 —— arr[0] = 1 / arr.length = 0 不触发;③ 无法检测 Map / Set —— 要重写 push/pop/shift 等方法。Vue 3 Proxy 解决:Proxy 是对象级别的拦截(不是属性级别),所以新增 / 删除 / 数组下标 / Map / Set 全部自动响应;同时 Proxy 是引擎原生实现,比 Object.defineProperty 性能更好。面试要点:能说”Proxy 拦截的是对象整体,defineProperty 拦截的是单个属性”——这是核心区别。

Q2(思考):Vue 3 的 refreactive 什么时候该用哪个?模板里有什么差异?

Q3(思考):Vue 3 的 Block Tree + patchFlag 编译期优化是怎么工作的?为什么能让渲染速度提升 30-50%?

Q4(思考):Composition API 和 Options API 各自的优劣是什么?在 200+ 表格列的 ERP 项目里你选哪个?为什么?

Q5(思考):你在 ERP 财务系统里是怎么从 Vue 2 迁移到 Vue 3 的?踩过哪些坑?最大的教训是什么?

💡 每道题后面都有 AI 助手按钮,可以一键拿到详细答案。

参考资料

🔗 原文链接 分享让更多人看到

评论