Vue 高级特性与核心原理

困难 🔴Vue 生态
12 个标签
预计阅读时间:115 分钟
Vue高级特性虚拟 DOMDiff 算法响应式原理Composition APITeleportSuspenseKeepAlive自定义指令渲染函数性能优化

Vue 高级特性与核心原理

Vue.js 作为现代前端框架,拥有许多强大的高级特性。深入理解这些特性对于掌握 Vue 至关重要,也是面试中的高频考点。本文将从虚拟 DOM、响应式系统、编译优化到内置组件、自定义指令、渲染函数、插件开发等多个维度,系统地拆解 Vue 的底层实现与工程实践。

阅读本文前,建议你已经熟悉 Vue 的基础用法(模板语法、组件、生命周期、Vue Router、Vuex/Pinia)。本文的目标不是重复基础,而是回答"为什么"和"怎么实现"这两个层面的问题。我们会尽量做到:先讲概念,再讲为什么重要,接着深入原理,然后给出可运行的代码示例,配合真实案例、性能数据与对比表格,最后总结常见坑与最佳实践。

目录概览

虚拟 DOM 与 Diff 算法(含 Vue 3 的最长递增子序列 LIS、PatchFlags、静态提升)
响应式系统深度解析(effect/scheduler、computed、ref vs reactive、shallow/readonly、nextTick 与异步更新队列)
内置组件(Teleport、Suspense、KeepAlive、Transition/TransitionGroup)
异步组件与懒加载
自定义指令与指令生命周期
渲染函数 h/JSX 与函数式组件
自定义 v-model 与组件双向绑定
插件开发与生态扩展
组件通信高级模式
混入(Mixins)与组合式 API
性能优化与真实案例
常见坑与最佳实践

---

一、虚拟 DOM 与 Diff 算法

1.1 虚拟 DOM 概念

虚拟 DOM(Virtual DOM)是 Vue 的核心机制之一,它是一个用 JavaScript 对象来描述真实 DOM 树结构的抽象层。你可以把它类比为"施工图纸":真实 DOM 是盖好的房子,改动房子的成本很高(浏览器的重排重绘代价昂贵),而改图纸(JavaScript 对象)几乎没有成本。当数据变化时,Vue 不会直接推倒重建房子,而是先在内存里画出新的图纸,与旧图纸做对比(Diff),算出"只需要改哪几面墙",最后把这批最小改动一次性施工到真实 DOM 上。

为什么重要:

直接操作 DOM 有两个问题。第一,DOM API 本身较慢,尤其是触发重排(reflow)的操作,如读取 offsetWidth、修改布局相关样式等。第二,手动操作 DOM 时开发者很难保证"最小更新",容易出现整块重渲染。虚拟 DOM 把"计算差异"这件事交给框架,用少量的 JavaScript 计算换取大量 DOM 操作的节省,同时带来了跨平台能力(同一份虚拟 DOM 可以渲染到浏览器、SSR 字符串、原生移动端如 Weex/NativeScript)。

虚拟 DOM 的优势总结:

1.跨平台能力:虚拟 DOM 不依赖于浏览器,可以在 Node.js(SSR)、移动端、Canvas 等环境运行
2.性能优化:通过 Diff 算法减少不必要的 DOM 操作
3.可预测性:数据驱动视图,状态变化可追踪,便于调试
4.声明式编程:开发者只描述"想要什么状态",不用关心"怎么一步步改 DOM"

需要澄清一个常见误区:虚拟 DOM 并不总是比手写命令式 DOM 操作快。它的价值在于"在可维护性可接受的前提下,提供一个足够快的更新下限"。一个经过精心手写优化的原生 DOM 更新可能比虚拟 DOM 更快,但代价是代码难以维护、容易出 bug。

javascriptCode
// 虚拟 DOM 节点结构(VNode)
const vnode = {
  tag: 'div',
  props: {
    id: 'app',
    class: 'container'
  },
  children: [
    {
      tag: 'h1',
      props: {},
      children: [{ text: 'Hello Vue' }]
    },
    {
      tag: 'p',
      props: { class: 'description' },
      children: [{ text: 'Virtual DOM Example' }]
    }
  ]
};

// 渲染函数:把 VNode 转成真实 DOM
function render(vnode) {
  // 文本节点
  if (vnode.text !== undefined) {
    return document.createTextNode(vnode.text);
  }

  const el = document.createElement(vnode.tag);

  // 设置属性
  if (vnode.props) {
    Object.keys(vnode.props).forEach(key => {
      el.setAttribute(key, vnode.props[key]);
    });
  }

  // 处理子节点
  if (vnode.children) {
    vnode.children.forEach(child => {
      if (child.text !== undefined) {
        el.appendChild(document.createTextNode(child.text));
      } else {
        el.appendChild(render(child));
      }
    });
  }

  return el;
}

1.2 真实 VNode 的字段远比示例复杂

Vue 3 内部的 VNode 结构包含大量字段,用于支撑优化。理解这些字段有助于读懂源码:

javascriptCode
// Vue 3 VNode 关键字段(简化)
const realVNode = {
  type: 'div',          // 元素类型 / 组件对象 / Fragment / Text / Comment
  props: { id: 'app' }, // 属性、事件、指令等
  key: 'a',             // Diff 时的唯一标识
  ref: null,            // 模板 ref
  children: [],         // 子节点
  shapeFlag: 9,         // 位运算标记:元素/组件/文本子节点/数组子节点等
  patchFlag: 1,         // 编译期生成的动态标记,运行时按需 patch
  dynamicProps: ['id'], // 动态属性名列表
  dynamicChildren: [],  // 收集的动态子节点(Block Tree 的核心)
  el: null,             // 对应的真实 DOM 元素
  component: null,      // 组件实例
  appContext: null      // 应用上下文
};

其中 shapeFlag 和 patchFlag 都用位运算存储多种状态,这是 Vue 3 性能优化的关键设计——用一个整数的不同二进制位表达多个布尔标记,判断时用按位与,极快且省内存。

javascriptCode
// shapeFlag 位运算枚举(源码简化)
const ShapeFlags = {
  ELEMENT: 1,                    // 00000001 普通元素
  FUNCTIONAL_COMPONENT: 1 << 1,  // 00000010 函数式组件
  STATEFUL_COMPONENT: 1 << 2,    // 00000100 有状态组件
  TEXT_CHILDREN: 1 << 3,         // 00001000 文本子节点
  ARRAY_CHILDREN: 1 << 4,        // 00010000 数组子节点
  SLOTS_CHILDREN: 1 << 5,        // 00100000 插槽子节点
  TELEPORT: 1 << 6,
  SUSPENSE: 1 << 7,
  COMPONENT_KEPT_ALIVE: 1 << 8,
  COMPONENT_SHOULD_KEEP_ALIVE: 1 << 9
};

// 判断一个 vnode 是否是有状态组件
function isStatefulComponent(vnode) {
  return (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) > 0;
}

// 组合标记:既是元素又有数组子节点
const flag = ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN; // 00010001

1.3 Diff 算法原理

Vue 的 Diff 算法采用了同层比较策略,时间复杂度为 O(n)。传统的树 Diff 算法复杂度是 O(n^3),对于前端场景不可接受。Vue 做了两个"启发式"假设来降低复杂度:第一,只比较同一层级的节点(跨层级的移动被当作删除+新增处理),第二,通过 key 来追踪节点身份,尽可能复用现有 DOM 节点。

Diff 算法的核心步骤:

1.同层比较:只比较同一层级的节点,不做跨层级移动
2.Key 优化:通过 key 快速定位可复用节点
3.双端比较:Vue 2 使用双端指针(头头、尾尾、头尾、尾头)比较
4.最长递增子序列:Vue 3 使用 LIS 算法最小化节点移动次数
javascriptCode
// 简化的 Diff 算法
function diff(oldVnode, newVnode) {
  // 如果节点类型不同,直接替换
  if (oldVnode.tag !== newVnode.tag) {
    return { type: 'REPLACE', node: render(newVnode) };
  }

  const patches = [];

  // 比较属性
  const propsPatches = diffProps(oldVnode.props, newVnode.props);
  if (propsPatches.length > 0) {
    patches.push({ type: 'PROPS', patches: propsPatches });
  }

  // 比较子节点
  const childrenPatches = diffChildren(oldVnode.children, newVnode.children);
  if (childrenPatches.length > 0) {
    patches.push({ type: 'CHILDREN', patches: childrenPatches });
  }

  return patches;
}

// 属性比较
function diffProps(oldProps, newProps) {
  const patches = [];
  const allProps = { ...oldProps, ...newProps };

  Object.keys(allProps).forEach(key => {
    if (oldProps[key] !== newProps[key]) {
      patches.push({
        key,
        oldValue: oldProps[key],
        newValue: newProps[key]
      });
    }
  });

  return patches;
}

// 子节点比较(简化版)
function diffChildren(oldChildren, newChildren) {
  const patches = [];
  const maxLength = Math.max(oldChildren.length, newChildren.length);

  for (let i = 0; i < maxLength; i++) {
    const oldChild = oldChildren[i];
    const newChild = newChildren[i];

    if (!oldChild) {
      patches.push({ type: 'ADD', index: i, node: newChild });
    } else if (!newChild) {
      patches.push({ type: 'REMOVE', index: i });
    } else {
      const childPatch = diff(oldChild, newChild);
      if (childPatch) {
        patches.push({ type: 'UPDATE', index: i, patch: childPatch });
      }
    }
  }

  return patches;
}

1.4 Vue 2 的双端 Diff 算法

Vue 2 的核心 Diff 逻辑在 updateChildren 函数中,使用四个指针进行双端比较。它每一轮会尝试四种匹配,命中任意一种就移动对应指针,避免了大量的节点移动。

javascriptCode
// Vue 2 双端 Diff 核心逻辑(简化还原)
function updateChildren(parentElm, oldCh, newCh) {
  let oldStartIdx = 0;
  let newStartIdx = 0;
  let oldEndIdx = oldCh.length - 1;
  let newEndIdx = newCh.length - 1;
  let oldStartVnode = oldCh[0];
  let oldEndVnode = oldCh[oldEndIdx];
  let newStartVnode = newCh[0];
  let newEndVnode = newCh[newEndIdx];

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (sameVnode(oldStartVnode, newStartVnode)) {
      // 1. 头头比较
      patchVnode(oldStartVnode, newStartVnode);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 2. 尾尾比较
      patchVnode(oldEndVnode, newEndVnode);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // 3. 头尾比较:旧头移动到旧尾之后
      patchVnode(oldStartVnode, newEndVnode);
      parentElm.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // 4. 尾头比较:旧尾移动到旧头之前
      patchVnode(oldEndVnode, newStartVnode);
      parentElm.insertBefore(oldEndVnode.el, oldStartVnode.el);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      // 5. 四种都没命中:用 key 建立映射查找
      const key = newStartVnode.key;
      const idxInOld = findIdxInOld(oldCh, key, oldStartIdx, oldEndIdx);
      if (idxInOld === undefined) {
        // 全新节点,创建并插入
        parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.el);
      } else {
        const vnodeToMove = oldCh[idxInOld];
        patchVnode(vnodeToMove, newStartVnode);
        oldCh[idxInOld] = undefined;
        parentElm.insertBefore(vnodeToMove.el, oldStartVnode.el);
      }
      newStartVnode = newCh[++newStartIdx];
    }
  }

  // 处理剩余节点
  if (oldStartIdx > oldEndIdx) {
    // 新增剩余的新节点
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      parentElm.appendChild(createElm(newCh[i]));
    }
  } else if (newStartIdx > newEndIdx) {
    // 删除剩余的旧节点
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      if (oldCh[i]) parentElm.removeChild(oldCh[i].el);
    }
  }
}

// 判断是否是同一个节点:key 与 tag 都相同
function sameVnode(a, b) {
  return a.key === b.key && a.tag === b.tag;
}

1.5 Vue 3 的快速 Diff 与最长递增子序列(LIS)

Vue 3 的 Diff 借鉴了 ivi 和 inferno 的思路,称为"快速 Diff"。它先做头部预处理和尾部预处理(把两端相同的节点快速 patch 掉),然后对中间乱序的部分建立"新节点在旧节点中的索引映射",最后通过求解最长递增子序列,找出"不需要移动"的节点集合,从而把 DOM 移动次数降到最低。

为什么用 LIS:

在中间乱序区,我们已经知道每个新节点应该复用哪个旧 DOM。剩下的问题是"如何用最少的移动,把这些 DOM 排成新顺序"。如果一批节点在旧序列中的相对顺序已经是递增的,它们就不需要移动。最长递增子序列正是"最多有多少个节点可以保持不动",其余节点才需要移动,这样移动次数最小。

javascriptCode
// 求最长递增子序列的下标(Vue 3 源码同款算法:贪心 + 二分 + 回溯)
function getSequence(arr) {
  const p = arr.slice();      // 记录前驱下标,用于回溯
  const result = [0];         // 存储 LIS 的下标
  let i, j, u, v, c;
  const len = arr.length;

  for (i = 0; i < len; i++) {
    const arrI = arr[i];
    if (arrI !== 0) {
      j = result[result.length - 1];
      if (arr[j] < arrI) {
        // 当前值比结果序列最后一个大,直接追加
        p[i] = j;
        result.push(i);
        continue;
      }
      // 二分查找:找到第一个大于等于 arrI 的位置替换
      u = 0;
      v = result.length - 1;
      while (u < v) {
        c = (u + v) >> 1;
        if (arr[result[c]] < arrI) {
          u = c + 1;
        } else {
          v = c;
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1];
        }
        result[u] = i;
      }
    }
  }

  // 回溯得到正确的下标序列
  u = result.length;
  v = result[u - 1];
  while (u-- > 0) {
    result[u] = v;
    v = p[v];
  }
  return result;
}

// 示例
console.log(getSequence([2, 3, 1, 5, 6, 8, 7, 9, 4]));
// 输出的是最长递增子序列对应的下标

下面是 Vue 3 patchKeyedChildren 中处理乱序区的核心思路(简化):

javascriptCode
// Vue 3 处理乱序中间区(简化还原)
function patchUnkeyedMiddle(oldCh, newCh, s1, s2, e1, e2, container) {
  // 1. 为新节点建立 key -> index 映射
  const keyToNewIndexMap = new Map();
  for (let i = s2; i <= e2; i++) {
    keyToNewIndexMap.set(newCh[i].key, i);
  }

  const toBePatched = e2 - s2 + 1;
  // newIndexToOldIndexMap[i] = 0 表示新节点没有对应旧节点(需新建)
  const newIndexToOldIndexMap = new Array(toBePatched).fill(0);
  let moved = false;
  let maxNewIndexSoFar = 0;

  // 2. 遍历旧节点,找到它在新序列中的位置
  for (let i = s1; i <= e1; i++) {
    const oldVnode = oldCh[i];
    const newIndex = keyToNewIndexMap.get(oldVnode.key);
    if (newIndex === undefined) {
      // 旧节点在新序列中不存在,卸载
      unmount(oldVnode);
    } else {
      // 记录映射,+1 是为了区分"未匹配(0)"和"下标为0"
      newIndexToOldIndexMap[newIndex - s2] = i + 1;
      if (newIndex >= maxNewIndexSoFar) {
        maxNewIndexSoFar = newIndex;
      } else {
        // 出现逆序,说明需要移动
        moved = true;
      }
      patch(oldVnode, newCh[newIndex], container);
    }
  }

  // 3. 求 LIS,得到不需要移动的节点下标集合
  const increasingSeq = moved ? getSequence(newIndexToOldIndexMap) : [];
  let j = increasingSeq.length - 1;

  // 4. 从后往前遍历,插入或移动节点
  for (let i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i;
    const nextChild = newCh[nextIndex];
    const anchor = nextIndex + 1 < newCh.length ? newCh[nextIndex + 1].el : null;

    if (newIndexToOldIndexMap[i] === 0) {
      // 全新节点
      patch(null, nextChild, container, anchor);
    } else if (moved) {
      if (j < 0 || i !== increasingSeq[j]) {
        // 不在 LIS 中,需要移动
        container.insertBefore(nextChild.el, anchor);
      } else {
        // 在 LIS 中,保持不动
        j--;
      }
    }
  }
}

1.6 Vue 3 的编译时优化

Vue 3 引入了编译时优化,在编译阶段就标记出静态节点和动态节点,运行时可以跳过静态节点的比较。这是 Vue 3 相比 Vue 2 性能提升的最大来源之一。主要优化包括:

1.静态节点提升(Static Hoisting):静态节点提升到渲染函数外部,只创建一次
2.补丁标志(Patch Flags):标记动态内容类型,运行时按位判断只 patch 变化的部分
3.缓存事件处理函数(cacheHandlers):避免每次渲染重新创建内联函数导致子组件无谓更新
4.树结构拍平(Block Tree / dynamicChildren):把动态节点收集到扁平数组,Diff 时直接遍历动态节点数组,跳过静态节点
javascriptCode
// Vue 2 编译结果:所有节点每次都参与创建与比较
function render() {
  return h('div', { id: 'app' }, [
    h('h1', {}, this.title),                     // 动态内容
    h('p', { class: 'static' }, 'Static text')   // 静态内容也要比较
  ]);
}

// Vue 3 编译结果(带静态提升 + PatchFlag)
const _hoisted_1 = h('p', { class: 'static' }, 'Static text'); // 提升到外部,只创建一次

function render() {
  return h('div', { id: 'app' }, [
    // 1 是 PatchFlag.TEXT,表示只有文本是动态的
    h('h1', {}, this.title, 1 /* TEXT */),
    _hoisted_1 // 静态节点直接复用,不参与 Diff
  ]);
}

PatchFlags 是一组位标记,编译器根据模板分析生成,运行时据此只更新变化的部分:

javascriptCode
// PatchFlags 枚举(源码简化)
const PatchFlags = {
  TEXT: 1,               // 动态文本内容
  CLASS: 1 << 1,         // 动态 class
  STYLE: 1 << 2,         // 动态 style
  PROPS: 1 << 3,         // 动态属性(非 class/style)
  FULL_PROPS: 1 << 4,    // 有动态 key,需要全量 diff props
  HYDRATE_EVENTS: 1 << 5,
  STABLE_FRAGMENT: 1 << 6,
  KEYED_FRAGMENT: 1 << 7,
  UNKEYED_FRAGMENT: 1 << 8,
  NEED_PATCH: 1 << 9,
  DYNAMIC_SLOTS: 1 << 10,
  HOISTED: -1,           // 静态提升节点
  BAIL: -2               // 退出优化模式
};

// 运行时按 patchFlag 精准更新
function patchElement(oldVnode, newVnode) {
  const el = (newVnode.el = oldVnode.el);
  const flag = newVnode.patchFlag;

  if (flag & PatchFlags.TEXT) {
    if (oldVnode.children !== newVnode.children) {
      el.textContent = newVnode.children;
    }
  }
  if (flag & PatchFlags.CLASS) {
    if (oldVnode.props.class !== newVnode.props.class) {
      el.className = newVnode.props.class;
    }
  }
  if (flag & PatchFlags.STYLE) {
    patchStyle(el, oldVnode.props.style, newVnode.props.style);
  }
  // ... 其余按位判断
}

Block Tree 是"树结构拍平"的载体。带有动态内容的节点会作为一个 Block,它的 dynamicChildren 收集了子树中所有动态节点。Diff 时不再递归遍历整棵树,而是直接遍历这个扁平数组:

javascriptCode
// Block 的概念:openBlock 开启收集,createBlock 结束收集
function render() {
  return (openBlock(), createBlock('div', null, [
    createVNode('span', null, '静态'),
    createVNode('span', null, msg, 1 /* TEXT */) // 会被收集进 dynamicChildren
  ]));
}
// Diff 时只对比 dynamicChildren,静态的 span 完全跳过

1.7 真实案例:为什么不能用 index 作为 key

一个高频的线上 bug:用数组下标 index 作为 v-for 的 key,在列表中间插入或删除元素、或列表内含有表单输入时,会导致状态错乱。

vueCode
<!-- 错误示范:用 index 作为 key -->
<template>
  <ul>
    <li v-for="(item, index) in list" :key="index">
      <input v-model="item.value" />
      {{ item.name }}
    </li>
  </ul>
</template>

假设列表有 3 项,用户在第一个 input 里输入了内容,然后在头部插入一个新项。由于 key 是 index,Vue 认为"第 0 个节点还是第 0 个节点,只是内容变了",于是复用了原来第 0 个 DOM(包括其中 input 的 DOM 状态),只更新文本。结果就是用户输入的内容"串位"到了新插入的项上。正确做法是使用稳定唯一的业务 id:

vueCode
<template>
  <ul>
    <li v-for="item in list" :key="item.id">
      <input v-model="item.value" />
      {{ item.name }}
    </li>
  </ul>
</template>

---

二、响应式系统深度解析

2.1 概念:什么是响应式

响应式(Reactivity)是指"当数据变化时,依赖这些数据的地方(视图、计算属性、侦听器)自动更新"。类比 Excel 表格:单元格 C1 写公式 =A1+B1,当你改动 A1,C1 会自动重算。响应式系统要解决两个核心问题:一是"谁依赖了这个数据"(依赖收集,track),二是"数据变了要通知谁"(派发更新,trigger)。

2.2 Vue 2 响应式局限性

Vue 2 使用 Object.defineProperty 劫持每个属性的 getter/setter 来实现响应式,存在以下局限性:

1.无法检测对象属性的添加或删除(因为 defineProperty 只能劫持已存在的属性)
2.无法检测数组索引和长度的直接变化
3.初始化时需要递归遍历所有属性一次性做劫持,数据量大时开销大
javascriptCode
// Vue 2 响应式原理(简化)
function defineReactive(obj, key, val) {
  const dep = new Dep(); // 每个属性一个依赖收集器
  observe(val);          // 递归处理嵌套对象

  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend(); // 依赖收集
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      observe(newVal); // 新值也要变成响应式
      dep.notify();    // 派发更新
    }
  });
}

function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return;
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}
javascriptCode
// Vue 2 响应式问题示例
const vm = new Vue({
  data: {
    obj: { a: 1 },
    arr: [1, 2, 3]
  }
});

// 以下操作不会触发视图更新
vm.obj.b = 2;      // 新增属性
delete vm.obj.a;   // 删除属性
vm.arr[0] = 100;   // 修改数组索引
vm.arr.length = 2; // 修改数组长度

// 解决方案
Vue.set(vm.obj, 'b', 2);    // 添加响应式属性
Vue.delete(vm.obj, 'a');    // 删除响应式属性
vm.arr.splice(0, 1, 100);   // 通过重写的数组方法响应式修改

Vue 2 对数组的处理是"重写数组的 7 个变更方法"(push、pop、shift、unshift、splice、sort、reverse),拦截这些方法调用来触发更新,这也是为什么直接改索引不生效、但用 splice 生效的原因。

javascriptCode
// Vue 2 数组方法拦截(简化)
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];

methodsToPatch.forEach(method => {
  const original = arrayProto[method];
  Object.defineProperty(arrayMethods, method, {
    value: function (...args) {
      const result = original.apply(this, args);
      const ob = this.__ob__;
      // 对新增元素做响应式处理
      let inserted;
      if (method === 'push' || method === 'unshift') inserted = args;
      else if (method === 'splice') inserted = args.slice(2);
      if (inserted) ob.observeArray(inserted);
      ob.dep.notify(); // 触发更新
      return result;
    }
  });
});

2.3 Vue 3 Proxy 的优势

Vue 3 使用 Proxy 实现响应式,解决了 Vue 2 的所有问题:

1.可以检测属性的添加和删除
2.可以检测数组索引和长度的变化
3.懒代理(lazy),只有访问到嵌套对象时才递归代理,初始化更快
4.支持 Map、Set、WeakMap、WeakSet 等集合类型
javascriptCode
// Vue 3 Proxy 响应式(简化)
function createReactive(target, isReadonly = false) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集
      if (!isReadonly) {
        track(target, key);
      }
      const result = Reflect.get(target, key, receiver);

      // 懒代理:只有访问时才代理嵌套对象
      if (typeof result === 'object' && result !== null) {
        return createReactive(result, isReadonly);
      }

      return result;
    },
    set(target, key, value, receiver) {
      if (isReadonly) {
        console.warn(\`Cannot set \${String(key)} on readonly object\`);
        return true;
      }

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

      // 派发更新(只有真正变化才触发)
      if (oldValue !== value) {
        trigger(target, key);
      }

      return result;
    },
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (hadKey && result) {
        trigger(target, key);
      }
      return result;
    },
    has(target, key) {
      track(target, key);
      return Reflect.has(target, key);
    },
    ownKeys(target) {
      track(target, 'iterate');
      return Reflect.ownKeys(target);
    }
  });
}

// 使用示例
const state = createReactive({
  obj: { a: 1 },
  arr: [1, 2, 3]
});

// 以下操作都会触发视图更新
state.obj.b = 2;      // 新增属性
delete state.obj.a;   // 删除属性
state.arr[0] = 100;   // 修改数组索引
state.arr.length = 2; // 修改数组长度

2.4 effect 与依赖收集的完整实现

Vue 3 响应式的核心是 effect(副作用函数)。渲染、计算属性、侦听器本质上都是 effect。effect 运行时会读取响应式数据,触发 getter,从而把自己作为依赖被收集;数据变化时触发 setter,通知所有依赖它的 effect 重新运行。

javascriptCode
// effect 与 track/trigger 的完整实现(教学版)
let activeEffect = null;
const effectStack = [];

// 依赖存储结构:WeakMap<target, Map<key, Set<effect>>>
const targetMap = new WeakMap();

function effect(fn, options = {}) {
  const effectFn = () => {
    try {
      activeEffect = effectFn;
      effectStack.push(effectFn);
      cleanup(effectFn); // 每次运行前清理旧依赖,避免分支切换残留
      return fn();
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1] || null;
    }
  };
  effectFn.deps = [];         // 记录该 effect 被哪些依赖集合收集
  effectFn.options = options; // scheduler 等配置

  if (!options.lazy) {
    effectFn();
  }
  return effectFn;
}

function cleanup(effectFn) {
  effectFn.deps.forEach(dep => dep.delete(effectFn));
  effectFn.deps.length = 0;
}

// 依赖收集
function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  dep.add(activeEffect);
  activeEffect.deps.push(dep); // 反向记录,便于 cleanup
}

// 派发更新
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (!dep) return;

  // 复制一份避免遍历时增删导致死循环
  const effectsToRun = new Set(dep);
  effectsToRun.forEach(effectFn => {
    // 避免自身触发自身导致无限递归
    if (effectFn === activeEffect) return;
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn); // 交给调度器
    } else {
      effectFn();
    }
  });
}

2.5 scheduler 与异步更新队列

如果每次数据变化都同步重新运行 effect,一次操作里连改 100 次数据就会渲染 100 次。Vue 通过 scheduler 把 effect 的执行"调度"到微任务队列,做去重与批处理,一个 tick 内只渲染一次。这就是"异步更新队列"的本质。

javascriptCode
// 异步更新队列(简化还原 Vue 的调度)
const queue = new Set();
let isFlushing = false;
const resolvedPromise = Promise.resolve();
let currentFlushPromise = null;

function queueJob(job) {
  queue.add(job); // Set 天然去重
  if (!isFlushing) {
    isFlushing = true;
    currentFlushPromise = resolvedPromise.then(flushJobs);
  }
}

function flushJobs() {
  try {
    queue.forEach(job => job());
  } finally {
    isFlushing = false;
    queue.clear();
    currentFlushPromise = null;
  }
}

// nextTick:在下一次 DOM 更新后执行回调
function nextTick(fn) {
  const p = currentFlushPromise || resolvedPromise;
  return fn ? p.then(fn) : p;
}

// 配合 effect 使用
const state = createReactive({ count: 0 });
effect(() => {
  console.log('render count:', state.count);
}, {
  scheduler: queueJob // 变化时不立即执行,加入队列
});

state.count++;
state.count++;
state.count++;
// 由于批处理,render 只会在微任务里执行一次,输出最终值

在组件中使用 nextTick 的典型场景:修改数据后需要读取更新后的 DOM。

javascriptCode
import { ref, nextTick } from 'vue';

export default {
  setup() {
    const list = ref([]);
    const scrollRef = ref(null);

    async function addItem(item) {
      list.value.push(item);
      // 此刻 DOM 尚未更新,直接读高度是旧值
      await nextTick();
      // DOM 已更新,可以安全地滚动到底部
      scrollRef.value.scrollTop = scrollRef.value.scrollHeight;
    }

    return { list, scrollRef, addItem };
  }
};

2.6 computed 计算属性的原理

计算属性本质是一个带缓存的 effect。它内部用一个 dirty 标记实现"惰性求值 + 缓存":只有依赖变化时才把 dirty 置为 true,下次访问 value 才重新计算,否则直接返回缓存值。

javascriptCode
// computed 原理实现
function computed(getter) {
  let value;      // 缓存值
  let dirty = true; // 是否需要重新计算

  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      // 依赖变化时不立即计算,只标记为脏
      if (!dirty) {
        dirty = true;
        // 通知依赖了该 computed 的外层 effect
        trigger(obj, 'value');
      }
    }
  });

  const obj = {
    get value() {
      if (dirty) {
        value = effectFn(); // 重新计算并缓存
        dirty = false;
      }
      // 让读取 computed.value 的外层 effect 也收集依赖
      track(obj, 'value');
      return value;
    }
  };

  return obj;
}

// 使用
const state = createReactive({ a: 1, b: 2 });
const sum = computed(() => {
  console.log('computing...');
  return state.a + state.b;
});

console.log(sum.value); // computing... 3
console.log(sum.value); // 3(直接读缓存,不再计算)
state.a = 10;
console.log(sum.value); // computing... 12(依赖变了,重新计算)

2.7 ref vs reactive 的区别与选择

ref 和 reactive 都能创建响应式数据,但适用场景不同。核心区别在于:reactive 只能包裹对象/数组/集合类型,且不能整体替换(会丢失响应性);ref 可以包裹任意类型(包括原始值),通过 .value 访问,可以整体替换。

javascriptCode
import { ref, reactive, toRefs, isRef } from 'vue';

// ref:包裹原始值,本质是一个含 value 访问器的对象
const count = ref(0);
count.value++; // 必须通过 .value

// ref 内部原理(简化)
function myRef(value) {
  const wrapper = {
    get value() {
      track(wrapper, 'value');
      return value;
    },
    set value(newVal) {
      if (newVal !== value) {
        value = newVal;
        trigger(wrapper, 'value');
      }
    }
  };
  return wrapper;
}

// reactive:包裹对象
const state = reactive({ count: 0, user: { name: 'Vue' } });
state.count++; // 直接访问

// 坑1:reactive 整体替换会丢失响应性
let obj = reactive({ a: 1 });
obj = reactive({ a: 2 }); // 原来的引用失去响应,模板还指向旧对象

// 坑2:解构 reactive 会丢失响应性
const { count: c } = state; // c 只是普通值,不再响应
// 正确做法:用 toRefs 保持响应性
const { count: c2 } = toRefs(state); // c2 是 ref,保持响应

选型建议:组合式函数(composable)返回值优先用 ref(便于解构与整体替换);一组强关联的状态用 reactive 更简洁。团队内保持统一约定最重要。

2.8 shallow 与 readonly 系列 API

Vue 提供了一组"浅层"和"只读"的响应式 API,用于性能优化和数据保护。

javascriptCode
import {
  shallowRef,
  shallowReactive,
  readonly,
  shallowReadonly,
  markRaw,
  toRaw
} from 'vue';

// shallowRef:只有 .value 的替换是响应式,内部属性变化不触发
const state = shallowRef({ count: 0 });
state.value.count++;        // 不触发更新
state.value = { count: 1 }; // 触发更新(整体替换)

// 典型用途:存储大型不可变数据(如 ECharts 实例、大数组)避免深度代理开销
const chartInstance = shallowRef(null);

// shallowReactive:只有第一层属性是响应式
const obj = shallowReactive({ a: 1, nested: { b: 2 } });
obj.a = 10;         // 触发更新
obj.nested.b = 20;  // 不触发更新

// readonly:深度只读,任何修改都会警告
const config = readonly({ apiUrl: '/api', timeout: 3000 });
config.timeout = 5000; // 开发环境警告,修改无效

// markRaw:标记对象永远不被代理(第三方库实例常用)
const rawInstance = markRaw(new SomeThirdPartyClass());
const s = reactive({ instance: rawInstance }); // instance 不会被代理

// toRaw:拿到代理背后的原始对象
const original = toRaw(state.value);

性能案例:一个渲染 10000 行的大表格,如果整个数据用 reactive 深度代理,初始化和更新都很慢。改用 shallowRef 存储数据、手动触发替换,可显著降低响应式开销。

javascriptCode
import { shallowRef, triggerRef } from 'vue';

const bigList = shallowRef(loadTenThousandRows());

function updateRow(index, patch) {
  // 直接改内部数据(不会自动触发)
  Object.assign(bigList.value[index], patch);
  // 手动触发一次更新
  triggerRef(bigList);
}

---

三、内置组件

3.1 Teleport 传送门

Teleport 允许把组件的一部分模板"传送"到 DOM 树的其他位置渲染,同时保持组件的逻辑归属不变。类比"寄快递":内容还是你写的、逻辑还是你的组件管的,但实际展示位置被送到了另一个地方(如 body 下)。

为什么重要:

模态框、通知、下拉菜单这类组件,逻辑上属于某个深层子组件,但视觉上需要脱离父级的 overflow: hidden、z-index 层叠上下文、transform 等约束。传统做法要手动 appendChild 到 body,破坏了组件封装。Teleport 优雅地解决了这个问题。

vueCode
<template>
  <button @click="open = true">打开弹窗</button>

  <!-- 内容传送到 body 下,避免被父级样式裁剪 -->
  <Teleport to="body">
    <div v-if="open" class="modal-mask">
      <div class="modal">
        <h3>{{ title }}</h3>
        <slot />
        <button @click="open = false">关闭</button>
      </div>
    </div>
  </Teleport>
</template>

<script setup>
import { ref } from 'vue';
const open = ref(false);
const title = 'Teleport 弹窗';
</script>

<style scoped>
.modal-mask {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

Teleport 支持动态目标与禁用(disabled 时内容留在原地,常用于响应式布局,桌面端传送、移动端原地):

vueCode
<template>
  <Teleport :to="target" :disabled="isMobile">
    <div class="tooltip">提示内容</div>
  </Teleport>
</template>

<script setup>
import { ref, computed } from 'vue';
const isMobile = computed(() => window.innerWidth < 768);
const target = ref('#tooltip-container');
</script>

3.2 Suspense 异步依赖协调

Suspense 是一个(当前仍为实验性)内置组件,用于协调异步依赖。它允许在等待多个异步组件或 async setup 解析完成时,先展示一个 fallback(加载态),全部就绪后再一次性展示内容,避免"闪烁式"的分块加载。

vueCode
<template>
  <Suspense>
    <!-- 默认插槽:异步内容 -->
    <template #default>
      <UserProfile :id="userId" />
      <UserPosts :id="userId" />
    </template>

    <!-- fallback 插槽:加载中占位 -->
    <template #fallback>
      <div class="loading">加载中...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { ref } from 'vue';
const userId = ref(1);
</script>

配合 async setup 的子组件:

vueCode
<!-- UserProfile.vue -->
<script setup>
const props = defineProps({ id: Number });

// 顶层 await:让该组件成为异步组件,Suspense 会等待它
const res = await fetch(\`/api/users/\${props.id}\`);
const user = await res.json();
</script>

<template>
  <div class="profile">
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>
</template>

配合错误捕获:

vueCode
<script setup>
import { ref, onErrorCaptured } from 'vue';

const error = ref(null);
onErrorCaptured((err) => {
  error.value = err;
  return false; // 阻止继续向上传播
});
</script>

<template>
  <div v-if="error" class="error">加载失败:{{ error.message }}</div>
  <Suspense v-else>
    <template #default><AsyncContent /></template>
    <template #fallback><Spinner /></template>
  </Suspense>
</template>

3.3 KeepAlive 组件缓存

KeepAlive 用于缓存组件实例,避免组件在切换时被销毁重建,从而保留其状态(如表单输入、滚动位置、请求结果)。类比"给房间贴上暂离牌"而不是拆掉重盖,回来时一切照旧。

vueCode
<template>
  <!-- 缓存所有被包裹的动态组件 -->
  <KeepAlive>
    <component :is="currentTab" />
  </KeepAlive>

  <!-- 只缓存指定组件 -->
  <KeepAlive :include="['TabA', 'TabB']">
    <component :is="currentTab" />
  </KeepAlive>

  <!-- 排除指定组件 -->
  <KeepAlive :exclude="['TabC']">
    <component :is="currentTab" />
  </KeepAlive>

  <!-- 限制最大缓存数量,超出按 LRU 淘汰 -->
  <KeepAlive :max="10">
    <component :is="currentTab" />
  </KeepAlive>
</template>

被 KeepAlive 缓存的组件会多出两个生命周期钩子:activated(进入激活)与 deactivated(离开缓存)。注意:缓存组件切换回来时不会再触发 mounted,需要"重新拉数据"的逻辑要放在 activated 里。

vueCode
<script setup>
import { onActivated, onDeactivated, ref } from 'vue';

const list = ref([]);

onActivated(() => {
  // 每次被激活(切回来)时执行,适合刷新数据
  console.log('组件被激活,刷新最新数据');
  fetchLatest();
});

onDeactivated(() => {
  // 被切走但未销毁时执行,适合暂停定时器、保存草稿
  console.log('组件被缓存,暂停轮询');
});

function fetchLatest() { /* ... */ }
</script>

结合路由缓存的常见写法(Vue Router 4):

vueCode
<template>
  <router-view v-slot="{ Component }">
    <KeepAlive :include="cachedViews">
      <component :is="Component" :key="$route.fullPath" />
    </KeepAlive>
  </router-view>
</template>

<script setup>
import { ref } from 'vue';
// 通常配合 tab 页签管理动态维护该列表
const cachedViews = ref(['ListPage', 'DetailPage']);
</script>

3.4 Transition 与 TransitionGroup 动画

Transition 为单个元素/组件的进入和离开提供过渡动画;TransitionGroup 为列表的增删和排序提供动画(含 FLIP 位移动画)。它会在合适的时机自动添加/移除一组 CSS 类名。

vueCode
<template>
  <button @click="show = !show">切换</button>
  <Transition name="fade">
    <p v-if="show">Hello Transition</p>
  </Transition>
</template>

<script setup>
import { ref } from 'vue';
const show = ref(true);
</script>

<style>
/* 进入动画的起点、终点 */
.fade-enter-from { opacity: 0; transform: translateY(10px); }
.fade-enter-active { transition: all 0.3s ease; }
.fade-enter-to { opacity: 1; transform: translateY(0); }

/* 离开动画 */
.fade-leave-from { opacity: 1; }
.fade-leave-active { transition: all 0.3s ease; }
.fade-leave-to { opacity: 0; transform: translateY(-10px); }
</style>

Transition 的 JavaScript 钩子,适合接入 GSAP 等动画库:

vueCode
<template>
  <Transition
    @before-enter="onBeforeEnter"
    @enter="onEnter"
    @leave="onLeave"
    :css="false"
  >
    <div v-if="show" class="box" />
  </Transition>
</template>

<script setup>
import { ref } from 'vue';
const show = ref(true);

function onBeforeEnter(el) {
  el.style.opacity = 0;
}
function onEnter(el, done) {
  // 手动控制动画,结束时调用 done
  let opacity = 0;
  const timer = setInterval(() => {
    opacity += 0.1;
    el.style.opacity = opacity;
    if (opacity >= 1) {
      clearInterval(timer);
      done();
    }
  }, 20);
}
function onLeave(el, done) {
  setTimeout(done, 300);
}
</script>

TransitionGroup 实现列表动画,move-class 是 FLIP 位移动画的关键:

vueCode
<template>
  <button @click="shuffle">打乱</button>
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in list" :key="item.id" class="list-item">
      {{ item.name }}
    </li>
  </TransitionGroup>
</template>

<script setup>
import { ref } from 'vue';
const list = ref([
  { id: 1, name: 'A' },
  { id: 2, name: 'B' },
  { id: 3, name: 'C' }
]);
function shuffle() {
  list.value = list.value.sort(() => Math.random() - 0.5);
}
</script>

<style>
.list-item { transition: all 0.4s ease; }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); }
.list-leave-active { position: absolute; } /* 让离开元素脱离文档流,避免跳动 */
/* move-class:元素位置改变时的平滑位移(FLIP) */
.list-move { transition: transform 0.4s ease; }
</style>

---

四、异步组件与懒加载

4.1 异步组件

异步组件可以按需加载组件,减少初始包体积。它返回一个 Promise,Vue 在需要渲染时才真正加载对应代码块。

javascriptCode
// Vue 2 异步组件
Vue.component('async-component', function (resolve, reject) {
  // 特殊 require() 告诉 webpack 自动将构建代码分割成单独的包
  require(['./my-component.vue'], resolve);
});

// Vue 3 异步组件
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
);

// 带加载状态、错误处理、重试的异步组件
const AsyncComponentWithLoading = defineAsyncComponent({
  loader: () => import('./components/MyComponent.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,     // 延迟 200ms 才显示 loading,避免闪烁
  timeout: 3000,  // 超时后显示 errorComponent
  // 加载失败时的重试逻辑
  onError(error, retry, fail, attempts) {
    if (error.message.includes('fetch') && attempts <= 3) {
      retry(); // 网络错误,重试
    } else {
      fail();
    }
  }
});

4.2 路由懒加载

结合 Vue Router 实现路由级别的代码分割,是首屏优化最有效的手段之一。

javascriptCode
// 路由懒加载
const routes = [
  {
    path: '/home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    // webpackChunkName 注释可命名 chunk,便于分析与合并
    component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue')
  },
  {
    path: '/user/:id',
    name: 'User',
    component: () => import(/* webpackChunkName: "user" */ '@/views/User.vue')
  }
];

4.3 组件级懒加载与 Intersection Observer

除了路由,还可以对"首屏之下"的重组件做可视区懒加载,进入视口才加载。

vueCode
<template>
  <div ref="placeholder">
    <component :is="LazyComp" v-if="LazyComp" />
    <div v-else class="skeleton">加载中...</div>
  </div>
</template>

<script setup>
import { ref, onMounted, shallowRef } from 'vue';

const placeholder = ref(null);
const LazyComp = shallowRef(null);

onMounted(() => {
  const observer = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting) {
      // 进入视口才加载重组件
      import('./HeavyChart.vue').then((mod) => {
        LazyComp.value = mod.default;
      });
      observer.disconnect();
    }
  });
  observer.observe(placeholder.value);
});
</script>

---

五、自定义指令

5.1 概念与生命周期

自定义指令(Custom Directive)用于封装对底层 DOM 的直接操作,弥补模板无法覆盖的低层需求,如自动聚焦、权限控制、懒加载图片、点击外部关闭、拖拽等。可以把它理解成"针对单个 DOM 元素的可复用行为插件"。

Vue 3 指令的生命周期钩子(对齐组件生命周期):created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted。

javascriptCode
// 全局自定义指令:自动聚焦
app.directive('focus', {
  mounted(el) {
    el.focus();
  }
});

// 完整钩子说明
const myDirective = {
  created(el, binding, vnode) {
    // 元素属性/事件绑定前
  },
  beforeMount(el, binding) {
    // 元素插入 DOM 前
  },
  mounted(el, binding) {
    // 元素插入 DOM 后(最常用)
    // binding.value 指令的值,binding.arg 参数,binding.modifiers 修饰符
  },
  beforeUpdate(el, binding) {},
  updated(el, binding) {
    // 组件更新后
  },
  beforeUnmount(el, binding) {},
  unmounted(el, binding) {
    // 元素卸载后,适合清理事件监听
  }
};

5.2 实战:v-permission 权限指令

javascriptCode
// 根据用户权限控制元素显示/移除
import { useUserStore } from '@/stores/user';

const permissionDirective = {
  mounted(el, binding) {
    const store = useUserStore();
    const required = binding.value; // 例如 'user:delete'
    if (!store.permissions.includes(required)) {
      // 无权限:直接从 DOM 移除(比 v-if 更彻底,避免残留)
      el.parentNode && el.parentNode.removeChild(el);
    }
  }
};

// 使用:<button v-permission="'user:delete'">删除</button>

5.3 实战:v-click-outside 点击外部关闭

javascriptCode
const clickOutside = {
  mounted(el, binding) {
    el.__clickOutsideHandler__ = (event) => {
      // 点击的不是元素本身也不是其子元素时触发回调
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event);
      }
    };
    document.addEventListener('click', el.__clickOutsideHandler__);
  },
  unmounted(el) {
    // 清理,避免内存泄漏
    document.removeEventListener('click', el.__clickOutsideHandler__);
    delete el.__clickOutsideHandler__;
  }
};

// 使用:<div v-click-outside="closeDropdown">...</div>

5.4 实战:v-lazy 图片懒加载指令

javascriptCode
const lazyImage = {
  mounted(el, binding) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          el.src = binding.value; // 真正的图片地址
          el.classList.add('loaded');
          observer.unobserve(el);
        }
      });
    }, { rootMargin: '50px' });
    observer.observe(el);
    el.__lazyObserver__ = observer;
  },
  unmounted(el) {
    el.__lazyObserver__ && el.__lazyObserver__.disconnect();
  }
};

// 使用:<img v-lazy="imageUrl" src="placeholder.png" />

5.5 实战:v-longpress 长按指令(含修饰符与参数)

javascriptCode
const longpress = {
  mounted(el, binding) {
    // binding.arg 参数:如 v-longpress:800 表示 800ms
    const duration = Number(binding.arg) || 500;
    let timer = null;

    const start = (e) => {
      if (e.type === 'click' && e.button !== 0) return;
      timer = setTimeout(() => binding.value(e), duration);
    };
    const cancel = () => {
      clearTimeout(timer);
      timer = null;
    };

    el.__lp__ = { start, cancel };
    el.addEventListener('mousedown', start);
    el.addEventListener('touchstart', start);
    ['click', 'mouseout', 'touchend', 'touchcancel'].forEach((evt) =>
      el.addEventListener(evt, cancel)
    );
  },
  unmounted(el) {
    const { start, cancel } = el.__lp__ || {};
    if (start) {
      el.removeEventListener('mousedown', start);
      el.removeEventListener('touchstart', start);
    }
    // 省略其余解绑
  }
};

// 使用:<button v-longpress:1000="onLongPress">长按1秒</button>

---

六、渲染函数 h、JSX 与函数式组件

6.1 为什么需要渲染函数

模板语法足够声明式且性能好(可被编译优化),但在需要高度动态、以编程方式生成结构的场景(如根据 level 动态生成 h1~h6、复杂表格列渲染、组件库底层)时,渲染函数提供了完整的 JavaScript 表达能力。

javascriptCode
import { h } from 'vue';

// h(类型, 属性, 子节点)
export default {
  props: { level: { type: Number, default: 1 } },
  setup(props, { slots }) {
    return () =>
      h(
        'h' + props.level, // 动态标签名
        { class: 'heading' },
        slots.default ? slots.default() : '默认标题'
      );
  }
};

h 函数的多种调用形式:

javascriptCode
// 无属性
h('div', 'hello');
// 带属性
h('div', { id: 'app', class: ['a', 'b'] }, 'hello');
// 带事件(on + 大写事件名)
h('button', { onClick: () => console.log('clicked') }, '点我');
// 子节点为数组
h('ul', [
  h('li', '第一项'),
  h('li', '第二项')
]);
// 渲染组件
import MyComponent from './MyComponent.vue';
h(MyComponent, { msg: 'hi', onCustomEvent: handler });
// 带插槽的组件(第三个参数为返回 vnode 的函数对象)
h(MyComponent, null, {
  default: () => h('span', '默认插槽'),
  header: (props) => h('h2', props.title)
});

6.2 JSX 写法

配合 @vue/babel-plugin-jsx,可以在 Vue 中使用 JSX,更接近 React 的书写体验,适合逻辑密集的渲染。

jsxCode
import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    const count = ref(0);
    const list = ref(['a', 'b', 'c']);

    return () => (
      <div class="container">
        <p>count: {count.value}</p>
        <button onClick={() => count.value++}>加一</button>
        <ul>
          {list.value.map((item, i) => (
            <li key={i}>{item}</li>
          ))}
        </ul>
        {count.value > 3 ? <strong>超过3了</strong> : null}
      </div>
    );
  }
});

6.3 函数式组件

函数式组件是无状态、无实例的组件,只接收 props 并返回 vnode,渲染开销更小。Vue 3 中直接用一个普通函数即可定义。

javascriptCode
// 函数式组件:一个纯函数
import { h } from 'vue';

const FunctionalButton = (props, { slots, emit }) => {
  return h(
    'button',
    {
      class: ['btn', \`btn-\${props.type}\`],
      onClick: () => emit('click')
    },
    slots.default ? slots.default() : props.text
  );
};

// 声明 props 与 emits(可选)
FunctionalButton.props = {
  type: { type: String, default: 'primary' },
  text: String
};
FunctionalButton.emits = ['click'];

export default FunctionalButton;

6.4 实战:动态渲染的表格列

javascriptCode
import { h, defineComponent } from 'vue';

export default defineComponent({
  props: {
    columns: Array, // [{ key, title, render?: (row) => vnode }]
    data: Array
  },
  setup(props) {
    return () =>
      h('table', { class: 'data-table' }, [
        h('thead', [
          h('tr', props.columns.map((col) =>
            h('th', { key: col.key }, col.title)
          ))
        ]),
        h('tbody', props.data.map((row) =>
          h('tr', { key: row.id }, props.columns.map((col) =>
            h('td', { key: col.key },
              // 支持自定义 render 函数,否则渲染纯文本
              col.render ? col.render(row) : row[col.key]
            )
          ))
        ))
      ]);
  }
});

---

七、自定义 v-model 与组件双向绑定

7.1 v-model 的本质

v-model 是"属性绑定 + 事件监听"的语法糖。在 Vue 3 中,组件上的 v-model 默认对应 modelValue 属性和 update:modelValue 事件。理解这一点就能自由定制。

vueCode
<!-- 父组件 -->
<CustomInput v-model="text" />
<!-- 等价于 -->
<CustomInput
  :modelValue="text"
  @update:modelValue="text = $event"
/>
vueCode
<!-- 子组件 CustomInput.vue -->
<script setup>
defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
</script>

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

7.2 具名 v-model 与多个 v-model

Vue 3 支持在同一组件上使用多个具名 v-model。

vueCode
<!-- 父组件:同时双向绑定 firstName 和 lastName -->
<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
vueCode
<!-- 子组件 UserName.vue -->
<script setup>
defineProps(['firstName', 'lastName']);
const emit = defineEmits(['update:firstName', 'update:lastName']);
</script>

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

7.3 自定义 v-model 修饰符

自定义修饰符通过 modelModifiers 传入,可实现如 .capitalize、.trim 等自定义处理。

vueCode
<!-- 使用:<MyInput v-model.capitalize="text" /> -->
<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
});
const emit = defineEmits(['update:modelValue']);

function onInput(e) {
  let value = e.target.value;
  // 检测到 capitalize 修饰符则首字母大写
  if (props.modelModifiers.capitalize && value) {
    value = value.charAt(0).toUpperCase() + value.slice(1);
  }
  emit('update:modelValue', value);
}
</script>

<template>
  <input :value="modelValue" @input="onInput" />
</template>

7.4 defineModel 新写法(Vue 3.4+)

Vue 3.4 起提供 defineModel 宏,大幅简化自定义 v-model。

vueCode
<script setup>
// 一行搞定:既是 prop 又能触发 update 事件
const model = defineModel();
// 具名 + 默认值 + 类型
const count = defineModel('count', { type: Number, default: 0 });
</script>

<template>
  <input v-model="model" />
  <button @click="count++">count: {{ count }}</button>
</template>

---

八、插件开发与生态扩展

8.1 插件概念

插件(Plugin)是为 Vue 应用添加全局功能的标准方式,如注册全局组件、指令、混入,提供全局方法或属性,注入 provide 等。一个插件是一个带 install 方法的对象,或一个函数。

javascriptCode
// 定义一个 Toast 插件
import ToastComponent from './Toast.vue';
import { createApp, h, ref } from 'vue';

const ToastPlugin = {
  install(app, options = {}) {
    // 1. 注册全局组件
    app.component('Toast', ToastComponent);

    // 2. 提供全局方法
    const message = ref('');
    const visible = ref(false);

    function show(text, duration = 2000) {
      message.value = text;
      visible.value = true;
      setTimeout(() => (visible.value = false), duration);
    }

    // 3. 挂到全局属性,模板中可用 $toast
    app.config.globalProperties.$toast = show;

    // 4. 通过 provide 供 inject 使用
    app.provide('toast', { show, message, visible });
  }
};

export default ToastPlugin;

// main.js 使用
// import { createApp } from 'vue';
// import App from './App.vue';
// createApp(App).use(ToastPlugin, { position: 'top' }).mount('#app');

8.2 在组合式 API 中使用插件能力

javascriptCode
import { inject } from 'vue';

export function useToast() {
  const toast = inject('toast');
  if (!toast) {
    throw new Error('useToast 必须在安装了 ToastPlugin 后使用');
  }
  return toast;
}

// 组件中
// const toast = useToast();
// toast.show('保存成功');

8.3 实战:一个简易的国际化插件

javascriptCode
import { reactive } from 'vue';

function createI18n(options) {
  const state = reactive({
    locale: options.locale || 'zh',
    messages: options.messages || {}
  });

  function t(key) {
    const msg = state.messages[state.locale] || {};
    // 支持 a.b.c 形式的嵌套 key
    return key.split('.').reduce((obj, k) => (obj ? obj[k] : undefined), msg) || key;
  }

  return {
    install(app) {
      app.config.globalProperties.$t = t;
      app.provide('i18n', { state, t, setLocale: (l) => (state.locale = l) });
    }
  };
}

// 使用
const i18n = createI18n({
  locale: 'zh',
  messages: {
    zh: { hello: '你好', nav: { home: '首页' } },
    en: { hello: 'Hello', nav: { home: 'Home' } }
  }
});
// app.use(i18n);
// 模板中:{{ $t('nav.home') }}

---

九、组件通信高级模式

9.1 跨层级通信

除了 props 和 emit,Vue 还提供了多种跨层级通信方式:

1.provide/inject:祖先组件向后代组件注入数据
2.attrs:透传属性和事件(Vue 3 中 listeners 合并进 attrs)
3.expose/ref:直接访问组件实例暴露的内容
4.状态管理(Pinia/Vuex):全局共享状态
javascriptCode
// Vue 3 setup 语法下的 provide/inject
// 祖先组件
import { provide, ref, readonly } from 'vue';

const theme = ref('light');
function updateTheme(newTheme) {
  theme.value = newTheme;
}
// 提供只读值 + 修改方法,防止后代直接篡改(单向数据流)
provide('theme', readonly(theme));
provide('updateTheme', updateTheme);
javascriptCode
// 后代组件
import { inject } from 'vue';

const theme = inject('theme', 'light'); // 第二个参数为默认值
const updateTheme = inject('updateTheme');

function toggleTheme() {
  updateTheme(theme.value === 'light' ? 'dark' : 'light');
}

选项式 API 的 provide/inject 写法:

vueCode
<!-- provide/inject 示例(选项式) -->
<!-- 祖先组件 -->
<script>
export default {
  provide() {
    return {
      theme: this.theme,
      updateTheme: this.updateTheme
    };
  },
  data() {
    return { theme: 'light' };
  },
  methods: {
    updateTheme(newTheme) {
      this.theme = newTheme;
    }
  }
};
</script>

<!-- 后代组件 -->
<script>
export default {
  inject: ['theme', 'updateTheme'],
  methods: {
    toggleTheme() {
      this.updateTheme(this.theme === 'light' ? 'dark' : 'light');
    }
  }
};
</script>

9.2 作用域插槽高级模式

作用域插槽允许父组件访问子组件的数据,实现"逻辑在子、渲染在父"的灵活组合,是组件库设计的核心模式(无渲染组件 Renderless Component)。

vueCode
<!-- 子组件:DataTable -->
<template>
  <table>
    <thead>
      <tr>
        <th v-for="column in columns" :key="column.key">
          {{ column.label }}
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row in data" :key="row.id">
        <td v-for="column in columns" :key="column.key">
          <slot :name="column.key" :row="row" :value="row[column.key]">
            {{ row[column.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  props: {
    columns: Array,
    data: Array
  }
};
</script>

<!-- 父组件:使用作用域插槽 -->
<template>
  <DataTable :columns="columns" :data="users">
    <template #name="{ row, value }">
      <router-link :to="'/user/' + row.id">{{ value }}</router-link>
    </template>

    <template #email="{ value }">
      <a :href="'mailto:' + value">{{ value }}</a>
    </template>

    <template #actions="{ row }">
      <button @click="editUser(row)">编辑</button>
      <button @click="deleteUser(row)">删除</button>
    </template>
  </DataTable>
</template>

9.3 无渲染组件(Renderless Component)

无渲染组件只封装逻辑、不渲染具体 UI,通过作用域插槽把数据和方法交给使用者渲染。

vueCode
<!-- MouseTracker.vue:只提供鼠标坐标逻辑 -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const x = ref(0);
const y = ref(0);
function update(e) {
  x.value = e.pageX;
  y.value = e.pageY;
}
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
</script>

<template>
  <!-- 不渲染任何具体 UI,把数据通过插槽交出去 -->
  <slot :x="x" :y="y" />
</template>

<!-- 使用者自由决定如何渲染 -->
<!--
<MouseTracker v-slot="{ x, y }">
  鼠标位置:{{ x }}, {{ y }}
</MouseTracker>
-->

9.4 attrs 透传与包装组件

vueCode
<!-- 二次封装 el-input,把未声明的属性/事件透传下去 -->
<script setup>
// inheritAttrs: false 时手动控制透传目标
defineOptions({ inheritAttrs: false });
</script>

<template>
  <div class="my-input-wrapper">
    <label>{{ label }}</label>
    <!-- v-bind="$attrs" 把父级传入的所有未声明属性/事件透传到内部 input -->
    <input v-bind="$attrs" class="my-input" />
  </div>
</template>

---

十、混入(Mixins)与组合式 API

10.1 Mixins 的优缺点

Mixins 是 Vue 2 的代码复用方式,但存在明显缺点:

1.来源不清晰:模板里用到的属性无法一眼看出来自哪个 mixin
2.命名冲突:多个 mixin 可能定义同名属性/方法,合并规则复杂
3.隐式依赖:mixin 之间、mixin 与组件之间可能存在隐式耦合
javascriptCode
// Mixin 示例
const formMixin = {
  data() {
    return {
      formData: {},
      errors: {}
    };
  },
  methods: {
    validateField(field, rules) {
      // 验证逻辑
    },
    submitForm() {
      // 提交逻辑
    }
  }
};

export default {
  mixins: [formMixin],
  data() {
    return {
      formData: {
        // 与 mixin 同名,组件的 data 优先,容易踩坑
        username: '',
        password: ''
      }
    };
  }
};

10.2 Composition API 的优势

Composition API(组合式 API)解决了 Mixins 的所有问题:

1.来源清晰:通过 import 明确来源
2.无命名冲突:使用解构自由重命名
3.类型友好:对 TypeScript 支持更好
4.逻辑聚合:相关逻辑写在一起,而不是被 options 结构强行拆散
javascriptCode
// Composable 示例:useForm.js
import { ref, reactive } from 'vue';

export function useForm(initialData = {}) {
  const formData = reactive({ ...initialData });
  const errors = ref({});
  const isSubmitting = ref(false);

  function validateField(field, rules) {
    // 验证逻辑
    const value = formData[field];
    for (const rule of rules) {
      if (rule.required && !value) {
        errors.value[field] = rule.message || \`\${field} 不能为空\`;
        return false;
      }
    }
    delete errors.value[field];
    return true;
  }

  async function submitForm(submitFn) {
    isSubmitting.value = true;
    try {
      await submitFn(formData);
    } catch (e) {
      errors.value = e.errors || {};
    } finally {
      isSubmitting.value = false;
    }
  }

  return {
    formData,
    errors,
    isSubmitting,
    validateField,
    submitForm
  };
}
javascriptCode
// 组件中使用(setup 语法)
import { useForm } from './useForm';

const { formData, errors, isSubmitting, submitForm } = useForm({
  username: '',
  password: ''
});

async function onSubmit() {
  await submitForm(async (data) => {
    await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(data)
    });
  });
}

10.3 更多可复用 Composable 示例

javascriptCode
// useFetch.js:封装数据请求
import { ref, watchEffect, toValue } from 'vue';

export function useFetch(urlRef) {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(false);

  watchEffect(async () => {
    const url = toValue(urlRef); // 支持传入 ref、getter 或普通值
    if (!url) return;
    loading.value = true;
    error.value = null;
    try {
      const res = await fetch(url);
      data.value = await res.json();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  });

  return { data, error, loading };
}
javascriptCode
// useLocalStorage.js:响应式 localStorage
import { ref, watch } from 'vue';

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key);
  const state = ref(stored ? JSON.parse(stored) : defaultValue);

  watch(
    state,
    (val) => {
      localStorage.setItem(key, JSON.stringify(val));
    },
    { deep: true }
  );

  return state;
}
javascriptCode
// useDebounce.js:防抖组合函数
import { ref, watch } from 'vue';

export function useDebounce(valueRef, delay = 300) {
  const debounced = ref(valueRef.value);
  let timer = null;

  watch(valueRef, (val) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      debounced.value = val;
    }, delay);
  });

  return debounced;
}

---

十一、性能优化与真实案例

11.1 长列表虚拟滚动

渲染上万条数据时,一次性创建上万个 DOM 会导致严重卡顿。虚拟滚动只渲染可视区域内的少量 DOM,滚动时动态替换内容。

vueCode
<template>
  <div ref="container" class="viewport" @scroll="onScroll">
    <!-- 占位撑起总高度,形成滚动条 -->
    <div class="phantom" :style="{ height: totalHeight + 'px' }"></div>
    <!-- 只渲染可视区节点,用 transform 定位 -->
    <div class="content" :style="{ transform: 'translateY(' + offsetY + 'px)' }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        class="row"
        :style="{ height: itemHeight + 'px' }"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue';

const props = defineProps({ items: Array });
const itemHeight = 40;
const container = ref(null);
const scrollTop = ref(0);
const viewportHeight = ref(0);

const totalHeight = computed(() => props.items.length * itemHeight);
const startIndex = computed(() => Math.floor(scrollTop.value / itemHeight));
const visibleCount = computed(() => Math.ceil(viewportHeight.value / itemHeight) + 2);
const visibleItems = computed(() =>
  props.items.slice(startIndex.value, startIndex.value + visibleCount.value)
);
const offsetY = computed(() => startIndex.value * itemHeight);

function onScroll(e) {
  scrollTop.value = e.target.scrollTop;
}
onMounted(() => {
  viewportHeight.value = container.value.clientHeight;
});
</script>

<style scoped>
.viewport { height: 400px; overflow-y: auto; position: relative; }
.content { position: absolute; top: 0; left: 0; right: 0; }
</style>

真实数据:某后台管理系统的日志页面,原本一次渲染 5000 行需要约 1200ms 且滚动明显掉帧,接入虚拟滚动后首屏渲染降到约 30ms,滚动稳定在 60fps。

11.2 v-once 与 v-memo

vueCode
<template>
  <!-- v-once:只渲染一次,之后视为静态,永不更新 -->
  <header v-once>
    <h1>{{ siteTitle }}</h1>
  </header>

  <!-- v-memo:仅当依赖数组变化时才更新该子树,用于超大列表精细控制 -->
  <div
    v-for="item in list"
    :key="item.id"
    v-memo="[item.id, item.selected]"
  >
    <!-- 只有 item.id 或 item.selected 变化时才重渲染此项 -->
    <ExpensiveItem :item="item" />
  </div>
</template>

11.3 合理拆分组件与稳定 props

大组件的任意状态变化都会导致整个组件重新渲染。将频繁变化的部分拆成独立子组件,能把重渲染范围限制在局部。同时避免在模板中传入每次都新建的对象/内联函数作为 props,否则子组件会被无谓更新(Vue 3 的 cacheHandlers 可缓存内联事件,但对象字面量仍会变化)。

vueCode
<!-- 不推荐:每次渲染都创建新对象/新函数 -->
<Child :config="{ a: 1 }" :onClick="() => doSomething()" />

<!-- 推荐:提到 setup 中稳定引用 -->
<script setup>
const config = { a: 1 };
function handleClick() { doSomething(); }
</script>
<Child :config="config" :onClick="handleClick" />

11.4 Vue 2 vs Vue 3 性能与体积对比

下表为官方基准与社区实测的量级参考(具体数字随版本与场景浮动,仅用于说明趋势):

| 对比维度 | Vue 2 | Vue 3 | 提升幅度 |

| --- | --- | --- | --- |

| 运行时体积(gzip 压缩后) | 约 22 至 23 KB | 约 13 至 16 KB(Tree-shaking 后可更小) | 体积减少约 40% |

| 初始渲染速度 | 基准 | 约快 40% 至 55% | 明显提升 |

| 更新渲染速度 | 基准 | 约快 130% 至 200% | 大幅提升 |

| 内存占用 | 基准 | 约减少 50% | 更省内存 |

| 响应式实现 | Object.defineProperty | Proxy | 支持更多数据结构 |

| 响应式初始化开销 | 递归全量劫持 | 懒代理按需劫持 | 大对象更快 |

| Tree-shaking 支持 | 弱(全局 API 挂在 Vue 上) | 强(按需引入) | 未用特性可摇掉 |

| TypeScript 支持 | 一般(需额外装饰器) | 原生优秀 | 类型推导更好 |

11.5 编译优化带来的更新性能

同一个含大量静态内容的页面,Vue 3 的静态提升 + Block Tree + PatchFlag 让更新只触达动态节点。假设一个页面有 1000 个节点、其中仅 20 个动态,Vue 2 更新时需遍历比较约 1000 个节点,而 Vue 3 只需遍历 dynamicChildren 中的 20 个动态节点,理论上 Diff 工作量减少约 98%。

| 场景 | Vue 2 参与 Diff 节点数 | Vue 3 参与 Diff 节点数 | 说明 |

| --- | --- | --- | --- |

| 1000 节点仅 20 动态 | 约 1000 | 约 20 | Block Tree 跳过静态节点 |

| 纯静态大段落 | 每次都比较 | 0(静态提升复用) | 静态节点只创建一次 |

| 动态列表带 key | 双端 Diff | 快速 Diff + LIS | 移动次数最小化 |

---

十二、常见坑与排查清单

| 场景 | 坑 | 正确做法 |

| --- | --- | --- |

| v-for 的 key | 用 index 做 key 导致状态错乱 | 用稳定唯一业务 id |

| reactive 解构 | 解构后失去响应性 | 用 toRefs 或直接访问 |

| reactive 整体替换 | 重新赋值丢失响应 | 用 ref 包裹或逐字段更新 |

| watch 深层对象 | 默认不深度监听 | 加 deep: true 或监听 getter |

| 修改后立即读 DOM | 读到旧 DOM | await nextTick 后再读 |

| KeepAlive 刷新数据 | 放在 mounted 不生效 | 放在 activated |

| 自定义指令绑定事件 | unmounted 未解绑 | 在 unmounted 清理监听 |

| 大对象响应式 | 深度代理开销大 | 用 shallowRef/markRaw |

| props 传内联对象 | 子组件被无谓更新 | 提到 setup 稳定引用 |

| 组件上 v-model | 误以为绑 value | Vue 3 默认绑 modelValue |

12.1 watch 深度监听与惰性执行

javascriptCode
import { watch, ref, reactive } from 'vue';

const state = reactive({ user: { name: 'Vue', age: 3 } });

// 坑:直接监听 reactive 的某个深层属性不生效
// watch(state.user.name, ...) // 错误:传入的是一个普通字符串

// 正确:用 getter 函数
watch(
  () => state.user.name,
  (newVal, oldVal) => console.log('name 变化', oldVal, '->', newVal)
);

// 深度监听整个对象
watch(
  () => state.user,
  (val) => console.log('user 变化', val),
  { deep: true, immediate: true } // immediate 立即执行一次
);

12.2 内存泄漏排查

组件里注册的全局监听、定时器、第三方实例,必须在卸载时清理,否则组件销毁后仍占用内存并可能报错。

javascriptCode
import { onMounted, onUnmounted } from 'vue';

onMounted(() => {
  const timer = setInterval(poll, 1000);
  const onResize = () => layout();
  window.addEventListener('resize', onResize);

  onUnmounted(() => {
    clearInterval(timer);
    window.removeEventListener('resize', onResize);
  });
});

---

十三、最佳实践

1.虚拟 DOM 优化:合理使用 key,避免使用 index 作为 key;大列表用虚拟滚动
2.响应式优化:Vue 3 优先使用 ref/reactive;大对象或第三方实例用 shallowRef/markRaw 避免深度代理
3.组件通信:优先 props/emit,跨层级用 provide/inject(配合 readonly 保持单向数据流),全局状态用 Pinia
4.代码复用:Vue 3 优先使用 Composables,避免 Mixins;composable 用 use 前缀命名
5.性能优化:使用异步组件与路由懒加载减少初始包体积;v-once/v-memo 减少无谓更新
6.内置组件:模态框用 Teleport,组件缓存用 KeepAlive(配合 activated 刷新),异步协调用 Suspense
7.渲染函数:高度动态的结构用 h/JSX,纯展示无状态用函数式组件
8.双向绑定:优先使用 defineModel(3.4+)简化 v-model
9.资源清理:自定义指令、监听、定时器务必在卸载钩子中清理,防止内存泄漏
10.编译优化:尽量让模板可被静态分析(避免过度动态化),充分利用静态提升与 PatchFlag

---

十四、总结

Vue 的高级特性并非孤立的知识点,而是围绕"声明式、响应式、高性能"这三个目标层层递进的完整体系。虚拟 DOM 提供了跨平台与最小更新的基础,Diff 算法(Vue 2 双端、Vue 3 快速 Diff + LIS)保证了更新效率,编译时优化(静态提升、PatchFlag、Block Tree)把运行时开销进一步压到最低;响应式系统(Proxy、effect、scheduler、computed 缓存)让数据驱动视图既自动又高效;内置组件、自定义指令、渲染函数、插件机制则从工程层面提供了强大的扩展能力。

| 主题 | 核心要点 | 关键 API/机制 | Vue 3 改进 |

| --- | --- | --- | --- |

| 虚拟 DOM | JS 对象描述 DOM,最小更新 | VNode、shapeFlag | patchFlag、Block Tree |

| Diff 算法 | 同层比较、key 复用 | 双端 Diff | 快速 Diff + LIS |

| 编译优化 | 静态与动态分离 | 静态提升、缓存事件 | PatchFlag、dynamicChildren |

| 响应式 | 依赖收集与派发更新 | track/trigger、effect | Proxy 替代 defineProperty |

| 计算属性 | 惰性求值 + 缓存 | dirty 标记 | 更精细的依赖追踪 |

| 异步更新 | 批处理去重 | scheduler、nextTick | 微任务队列 |

| ref/reactive | 原始值 vs 对象 | .value、toRefs | shallow/readonly 系列 |

| Teleport | 内容传送 | to、disabled | 全新内置组件 |

| Suspense | 异步依赖协调 | default/fallback | 全新内置组件 |

| KeepAlive | 组件状态缓存 | include/exclude/max | activated/deactivated |

| 过渡动画 | 进入离开与列表动画 | Transition、TransitionGroup | 更完善的 JS 钩子 |

| 异步组件 | 按需加载减体积 | defineAsyncComponent | 重试、超时、错误组件 |

| 自定义指令 | 封装 DOM 操作 | mounted/unmounted | 生命周期对齐组件 |

| 渲染函数 | 编程式生成结构 | h、JSX | 函数式组件更轻量 |

| 自定义 v-model | 组件双向绑定 | modelValue、defineModel | 多 v-model、修饰符 |

| 插件 | 全局能力扩展 | install、provide | 组合式配合 inject |

| 代码复用 | 逻辑聚合与共享 | Composables | 取代 Mixins |

掌握这些原理,不仅能在面试中从容应对底层追问,更能在实际项目中做出正确的架构与性能取舍:什么时候该用 shallowRef、为什么列表要用稳定 key、大页面如何靠编译优化和组件拆分保持流畅、复杂交互如何用无渲染组件和作用域插槽解耦。真正理解"为什么这样设计",才能把 Vue 用到极致。