阅读视图

发现新文章,点击刷新页面。

吃透 JS 事件委托:从原理到实战,解锁高性能事件处理方案

事件委托(Event Delegation)是 JavaScript 中最核心的事件处理技巧之一,也是前端面试的高频考点。它基于事件冒泡机制,能大幅减少事件绑定数量、解决动态元素事件失效问题,同时降低内存占用、提升页面性能。本文将从原理拆解、实战场景、性能优化到避坑指南,全方位带你吃透事件委托。

一、为什么需要事件委托?先看痛点

在未使用事件委托的场景中,我们通常会给每个元素单独绑定事件,比如一个列表的所有项:

// 传统方式:给每个li绑定点击事件
const items = document.querySelectorAll('.list-item');
items.forEach(item => {
  item.addEventListener('click', () => {
    console.log('点击了列表项:', item.textContent);
  });
});

这种写法会暴露三个核心问题:

  1. 性能损耗:如果列表有 1000 个项,就会创建 1000 个事件处理函数,占用大量内存;
  2. 动态元素失效:新增的列表项(如通过 JS 动态添加)不会自动绑定事件,需要重新执行绑定逻辑;
  3. 代码冗余:重复的事件绑定逻辑,增加维护成本。

而事件委托能一次性解决这些问题 —— 只给父元素绑定一次事件,就能处理所有子元素的事件触发。

二、事件委托的核心原理:事件流

要理解事件委托,必须先掌握 DOM 事件流的三个阶段:

  1. 捕获阶段:事件从 window 向下传播到目标元素(从外到内);
  2. 目标阶段:事件到达目标元素本身;
  3. 冒泡阶段:事件从目标元素向上传播回 window(从内到外)。

事件委托的核心逻辑是:利用事件冒泡,将子元素的事件绑定到父元素(甚至根元素)上,通过判断事件源(target)来区分具体触发的子元素

举个直观的例子:点击列表中的<li>,事件会先触发<li>的 click 事件,然后冒泡到<ul><div>,直到documentwindow。我们只需要在<ul>上绑定一次事件,就能捕获所有<li>的点击行为。

三、基础实战:实现一个列表的事件委托

1. 核心实现代码

<ul id="list" class="item-list">
  <li class="list-item" data-id="1">列表项1</li>
  <li class="list-item" data-id="2">列表项2</li>
  <li class="list-item" data-id="3">列表项3</li>
</ul>
<button id="addItem">新增列表项</button>

<script>
// 父元素绑定事件(只绑定一次)
const list = document.getElementById('list');
list.addEventListener('click', (e) => {
  // 核心:判断触发事件的目标元素
  const target = e.target;
  // 确认点击的是列表项(避免点击ul空白处触发)
  if (target.classList.contains('list-item')) {
    const id = target.dataset.id;
    console.log(`点击了列表项${id}:`, target.textContent);
  }
});

// 动态新增列表项(无需重新绑定事件)
const addItem = document.getElementById('addItem');
let index = 4;
addItem.addEventListener('click', () => {
  const li = document.createElement('li');
  li.className = 'list-item';
  li.dataset.id = index;
  li.textContent = `列表项${index}`;
  list.appendChild(li);
  index++;
});
</script>

2. 关键知识点解析

  • e.target:触发事件的原始元素(比如点击的<li>);
  • e.currentTarget:绑定事件的元素(这里是<ul>);
  • 类名 / 属性判断:通过classListdataset等方式精准匹配目标元素,避免非目标元素触发逻辑;
  • 动态元素兼容:新增的<li>无需重新绑定事件,因为事件委托在父元素上,天然支持动态元素。

四、进阶场景:精细化事件委托

实际开发中,事件委托的场景往往更复杂,比如多层嵌套、多类型事件、需要阻止冒泡等,以下是高频进阶用法:

1. 多层嵌套元素的委托

当目标元素嵌套在其他元素中(比如<li>里有<span><button>),需要通过closest找到最外层的目标元素:

<ul id="list">
  <li class="list-item" data-id="1">
    <span>列表项1</span>
    <button class="delete-btn">删除</button>
  </li>
</ul>

<script>
const list = document.getElementById('list');
list.addEventListener('click', (e) => {
  // 找到最近的list-item(解决点击子元素触发的问题)
  const item = e.target.closest('.list-item');
  if (item) {
    // 区分点击的是列表项还是删除按钮
    if (e.target.classList.contains('delete-btn')) {
      console.log(`删除列表项${item.dataset.id}`);
      item.remove();
    } else {
      console.log(`点击列表项${item.dataset.id}`);
    }
  }
});
</script>

closest方法会从当前元素向上查找,返回匹配选择器的第一个祖先元素(包括自身),是处理嵌套元素的最佳方案。

2. 多类型事件的统一委托

可以在父元素上绑定多个事件类型,或通过一个处理函数区分不同事件:

// 一个处理函数处理多个事件类型
list.addEventListener('click', handleItemEvent);
list.addEventListener('mouseenter', handleItemEvent);
list.addEventListener('mouseleave', handleItemEvent);

function handleItemEvent(e) {
  const item = e.target.closest('.list-item');
  if (!item) return;

  switch(e.type) {
    case 'click':
      console.log('点击:', item.dataset.id);
      break;
    case 'mouseenter':
      item.style.backgroundColor = '#f5f5f5';
      break;
    case 'mouseleave':
      item.style.backgroundColor = '';
      break;
  }
}

3. 委托到 document/body(全局委托)

对于全局范围内的动态元素(如弹窗、动态按钮),可以将事件委托到documentbody

// 全局委托:处理所有动态生成的按钮
document.addEventListener('click', (e) => {
  if (e.target.classList.contains('dynamic-btn')) {
    console.log('点击了动态按钮:', e.target.textContent);
  }
});

// 动态创建按钮
setTimeout(() => {
  const btn = document.createElement('button');
  btn.className = 'dynamic-btn';
  btn.textContent = '动态按钮';
  document.body.appendChild(btn);
}, 1000);

⚠️ 注意:全局委托虽方便,但不要滥用 ——document上的事件会监听整个页面的点击,过多的全局委托会增加事件处理的耗时,建议优先委托到最近的父元素。

五、性能优化:让事件委托更高效

事件委托本身是高性能方案,但不当使用仍会产生性能问题,以下是优化技巧:

1. 选择最近的父元素

尽量避免直接委托到document/body,而是选择离目标元素最近的固定父元素。比如列表的事件委托到<ul>,而非document,减少事件传播的层级和处理函数的触发次数。

2. 节流 / 防抖处理高频事件

如果委托的是scrollresizemousemove等高频事件,必须结合节流 / 防抖:

// 节流函数
function throttle(fn, delay = 100) {
  let timer = null;
  return (...args) => {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}

// 委托scroll事件(节流处理)
document.addEventListener('scroll', throttle((e) => {
  // 处理滚动逻辑
  console.log('滚动了');
}, 200));

3. 及时移除无用的委托事件

如果委托的父元素被销毁(比如弹窗关闭),要及时移除事件监听,避免内存泄漏:

const modal = document.getElementById('modal');
const handleModalClick = (e) => {
  // 弹窗内的事件逻辑
};

// 绑定事件
modal.addEventListener('click', handleModalClick);

// 弹窗关闭时移除事件
function closeModal() {
  modal.removeEventListener('click', handleModalClick);
  modal.remove();
}

六、避坑指南:事件委托的常见问题

1. 事件被阻止冒泡

如果子元素的事件处理函数中调用了e.stopPropagation(),会导致事件无法冒泡到父元素,委托失效:

// 错误示例:子元素阻止冒泡,委托失效
document.querySelector('.list-item').addEventListener('click', (e) => {
  e.stopPropagation(); // 阻止冒泡
  console.log('子元素点击');
});

// 父元素的委托事件不会触发
list.addEventListener('click', (e) => {
  console.log('委托事件'); // 不会执行
});

✅ 解决方案:避免在子元素中随意阻止冒泡,若必须阻止,需确保不影响委托逻辑。

2. 目标元素是不可冒泡的事件

部分事件不支持冒泡(如focusblurmouseentermouseleave),直接委托会失效:

// 错误示例:mouseenter不冒泡,委托失效
list.addEventListener('mouseenter', (e) => {
  console.log('鼠标进入列表项'); // 不会触发
});

✅ 解决方案:使用事件捕获模式(第三个参数设为true):

// 捕获模式处理不冒泡的事件
list.addEventListener('mouseenter', (e) => {
  const item = e.target.closest('.list-item');
  if (item) {
    console.log('鼠标进入列表项');
  }
}, true); // 开启捕获模式

3. 动态修改元素的类名 / 属性

如果目标元素的类名、dataset等用于判断的属性被动态修改,可能导致委托逻辑失效:

// 动态修改类名后,委托无法匹配
const item = document.querySelector('.list-item');
item.classList.remove('list-item'); // 移除类名
// 此时点击该元素,委托逻辑不会触发

✅ 解决方案:尽量使用稳定的标识(如固定的data-*属性),而非易变的类名。

七、框架中的事件委托(Vue/React)

现代前端框架虽封装了事件处理,但底层仍基于事件委托,且有专属的使用方式:

1. Vue3 中的事件委托

Vue 的v-on@)指令默认会利用事件委托(绑定到组件根元素),也可手动实现精细化委托:

<template>
  <ul @click="handleListClick">
    <li v-for="item in list" :key="item.id" :data-id="item.id">
      {{ item.name }}
      <button class="delete-btn">删除</button>
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue';
const list = ref([{ id: 1, name: '列表项1' }, { id: 2, name: '列表项2' }]);

const handleListClick = (e) => {
  const item = e.target.closest('[data-id]');
  if (item) {
    const id = item.dataset.id;
    if (e.target.classList.contains('delete-btn')) {
      list.value = list.value.filter(item => item.id !== Number(id));
    } else {
      console.log(`点击列表项${id}`);
    }
  }
};
</script>

2. React 中的事件委托

React 的合成事件系统本身就是基于事件委托(所有事件绑定到document),无需手动实现,但可通过e.target判断目标元素:

import { useState } from 'react';

function List() {
  const [list, setList] = useState([{ id: 1, name: '列表项1' }]);

  const handleListClick = (e) => {
    const item = e.target.closest('[data-id]');
    if (item) {
      const id = item.dataset.id;
      console.log(`点击列表项${id}`);
    }
  };

  return (
    <ul onClick={handleListClick}>
      {list.map(item => (
        <li key={item.id} data-id={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

八、总结

事件委托是前端开发中 “四两拨千斤” 的技巧,核心是利用事件冒泡,将多个子元素的事件绑定到父元素,通过目标元素判断执行逻辑。它的优势在于:

  • 减少事件绑定数量,降低内存占用;
  • 天然支持动态元素,无需重复绑定;
  • 简化代码逻辑,提升可维护性。

使用时需注意:

  1. 优先委托到最近的父元素,避免全局委托;
  2. 处理嵌套元素用closest,处理不冒泡事件用捕获模式;
  3. 高频事件结合节流 / 防抖,及时移除无用事件;
  4. 避免随意阻止冒泡,防止委托失效。

掌握事件委托,不仅能写出更高效的代码,更能深入理解 DOM 事件流的本质 —— 这也是从 “初级前端” 到 “中高级前端” 的必经之路。

Vue3 中 Lottie 动画库的使用指南

Lottie 是 Airbnb 开源的一款跨平台动画渲染库,能够将 AE(After Effects)制作的动画导出为 JSON 格式,并在 Web、iOS、Android 等平台无缝渲染,完美还原设计师的动画效果。在 Vue3 项目中集成 Lottie,既能提升页面交互体验,又能避免传统 GIF / 视频动画的性能问题和体积冗余。本文将详细讲解 Vue3 中 Lottie 的安装、基础使用、高级配置及实战技巧。

一、核心优势

在开始集成前,先了解 Lottie 适配 Vue3 项目的核心价值:

  1. 轻量化:JSON 动画文件体积远小于 GIF / 视频,且支持按需加载;
  2. 可交互:可通过代码控制动画播放、暂停、跳转、循环等,支持自定义交互逻辑;
  3. 矢量渲染:动画基于矢量,适配不同分辨率设备无模糊;
  4. Vue3 友好:支持组合式 API(Setup),可封装为通用组件,复用性强。

二、环境准备与安装

1. 依赖安装

Vue3 项目中推荐使用 lottie-web(官方 Web 端实现),通过 npm/yarn/pnpm 安装:

# npm
npm install lottie-web --save

# yarn
yarn add lottie-web

# pnpm
pnpm add lottie-web

2. 动画资源准备

Lottie 依赖 AE 导出的 JSON 动画文件,获取方式:

  • 设计师使用 AE 制作动画,通过 Bodymovin 插件导出 JSON 文件;
  • 从 Lottie 官方素材库获取免费动画:LottieFiles

将下载的 JSON 动画文件放入 Vue3 项目的 public 或 src/assets 目录(推荐 public,避免打包路径问题)。

三、基础使用:封装通用 Lottie 组件

为了在项目中复用,我们先封装一个通用的 Lottie 组件(支持 Vue3 组合式 API)。

1. 创建 Lottie 通用组件

在 src/components 目录下新建 LottieAnimation.vue

<template>
  <!-- 动画容器,需指定宽高 -->
  <div ref="lottieContainer" class="lottie-container" :style="{ width, height }"></div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import lottie from 'lottie-web';

// 定义 Props
const props = defineProps({
  // 动画 JSON 文件路径
  animationData: {
    type: Object,
    required: false,
    default: null
  },
  path: {
    type: String,
    required: false,
    default: ''
  },
  // 动画宽高
  width: {
    type: String,
    default: '300px'
  },
  height: {
    type: String,
    default: '300px'
  },
  // 是否自动播放
  autoplay: {
    type: Boolean,
    default: true
  },
  // 是否循环播放
  loop: {
    type: Boolean,
    default: true
  },
  // 动画速度(1 为正常速度)
  speed: {
    type: Number,
    default: 1
  },
  // 渲染方式(svg/canvas/html),优先 svg(矢量)
  renderer: {
    type: String,
    default: 'svg',
    validator: (val) => ['svg', 'canvas', 'html'].includes(val)
  }
});

// 定义 Emits:暴露动画状态事件
const emit = defineEmits(['complete', 'loopComplete', 'enterFrame']);

// 动画容器 Ref
const lottieContainer = ref(null);
// Lottie 实例
let lottieInstance = null;

// 初始化动画
const initLottie = () => {
  if (!lottieContainer.value) return;

  // 销毁旧实例(避免重复渲染)
  if (lottieInstance) {
    lottieInstance.destroy();
  }

  // 创建 Lottie 实例
  lottieInstance = lottie.loadAnimation({
    container: lottieContainer.value, // 动画容器
    animationData: props.animationData, // 动画 JSON 数据(本地导入)
    path: props.path, // 动画 JSON 文件路径(远程/ public 目录)
    renderer: props.renderer, // 渲染方式
    loop: props.loop, // 循环播放
    autoplay: props.autoplay, // 自动播放
    name: 'lottie-animation' // 动画名称(可选)
  });

  // 设置动画速度
  lottieInstance.setSpeed(props.speed);

  // 监听动画事件
  lottieInstance.addEventListener('complete', () => {
    emit('complete'); // 动画播放完成
  });
  lottieInstance.addEventListener('loopComplete', () => {
    emit('loopComplete'); // 动画循环完成
  });
  lottieInstance.addEventListener('enterFrame', (e) => {
    emit('enterFrame', e); // 动画每一帧
  });
};

// 监听 Props 变化,重新初始化
watch(
  [() => props.path, () => props.animationData, () => props.loop, () => props.speed],
  () => {
    initLottie();
  },
  { immediate: true }
);

// 组件卸载时销毁实例
onUnmounted(() => {
  if (lottieInstance) {
    lottieInstance.destroy();
    lottieInstance = null;
  }
});
</script>

<style scoped>
.lottie-container {
  display: inline-block;
  overflow: hidden;
}
</style>

2. 基础使用示例

在页面组件中引入并使用封装好的 LottieAnimation 组件,支持两种加载方式:

方式 1:加载 public 目录下的 JSON 文件

将动画文件 animation.json 放入 public/lottie/ 目录,使用 path 传入路径:

<template>
  <div class="demo-page">
    <h2>Lottie 基础使用示例</h2>
    <LottieAnimation
      path="/lottie/animation.json"
      width="200px"
      height="200px"
      :loop="false"
      :speed="1.2"
      @complete="handleAnimationComplete"
    />
  </div>
</template>

<script setup>
import LottieAnimation from '@/components/LottieAnimation.vue';

// 动画播放完成回调
const handleAnimationComplete = () => {
  console.log('动画播放完成!');
};
</script>

方式 2:本地导入 JSON 文件(需配置 loader)

如果将动画文件放在 src/assets 目录,需先导入 JSON 文件(Vue3 + Vite 无需额外配置,Webpack 需确保支持 JSON 导入):

<template>
  <div class="demo-page">
    <LottieAnimation
      :animation-data="animationData"
      width="200px"
      height="200px"
      :autoplay="false"
      ref="lottieRef"
    />
    <button @click="playAnimation">播放动画</button>
    <button @click="pauseAnimation">暂停动画</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import LottieAnimation from '@/components/LottieAnimation.vue';
// 导入本地 JSON 动画文件
import animationData from '@/assets/lottie/animation.json';

const animationData = ref(animationData);
const lottieRef = ref(null);

// 播放动画
const playAnimation = () => {
  lottieRef.value.lottieInstance.play();
};

// 暂停动画
const pauseAnimation = () => {
  lottieRef.value.lottieInstance.pause();
};
</script>

四、高级配置与交互控制

Lottie 提供了丰富的 API 用于控制动画,以下是常用的交互场景:

1. 手动控制播放 / 暂停 / 停止

通过获取 Lottie 实例,调用内置方法:

// 播放动画
lottieInstance.play();

// 暂停动画
lottieInstance.pause();

// 停止动画(重置到第一帧)
lottieInstance.stop();

// 跳转到指定帧(frameNum 为帧编号)
lottieInstance.goToAndStop(frameNum, true);

// 跳转到指定帧并播放
lottieInstance.goToAndPlay(frameNum, true);

2. 动态修改动画速度

javascript

运行

// 设置速度(0.5 为慢放,2 为快放)
lottieInstance.setSpeed(0.5);

// 获取当前速度
const currentSpeed = lottieInstance.playSpeed;

3. 控制循环模式

// 设置循环次数(0 为无限循环,1 为播放 1 次)
lottieInstance.loop = 0;

// 单独设置循环(立即生效)
lottieInstance.setLoop(true); // 无限循环
lottieInstance.setLoop(3); // 循环 3 次

4. 监听动画进度

通过 enterFrame 事件监听动画进度,实现进度条联动:

<template>
  <LottieAnimation
    path="/lottie/animation.json"
    @enterFrame="handleEnterFrame"
  />
  <input
    type="range"
    min="0"
    max="100"
    v-model="progress"
    @input="handleProgressChange"
  />
</template>

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

const progress = ref(0);
let totalFrames = 0;

// 监听每一帧,更新进度
const handleEnterFrame = (e) => {
  totalFrames = e.totalFrames;
  progress.value = Math.floor((e.currentFrame / totalFrames) * 100);
};

// 拖动进度条,跳转动画
const handleProgressChange = () => {
  const targetFrame = (progress.value / 100) * totalFrames;
  lottieInstance.goToAndPlay(targetFrame, true);
};
</script>

五、性能优化与注意事项

1. 性能优化

  • 懒加载:非首屏动画使用 v-if 或动态导入,按需初始化;
  • 销毁实例:组件卸载时务必调用 destroy() 销毁实例,避免内存泄漏;
  • 选择渲染方式:优先使用 svg 渲染(矢量、轻量),复杂动画可使用 canvas
  • 压缩 JSON 文件:使用 LottieFiles 在线工具压缩动画 JSON,减少体积。

2. 常见问题解决

  • 动画不显示:检查容器宽高是否设置、JSON 文件路径是否正确(path 以 / 开头表示 public 根目录);
  • 动画卡顿:减少动画层数和复杂路径,避免同时播放多个大型动画;
  • 跨域问题:远程加载 JSON 文件需确保服务端开启 CORS;
  • Vue3 打包路径问题animationData 导入本地 JSON 时,Vite 需确保 assetsInclude 包含 .json(默认已支持)。

六、总结

Lottie 是 Vue3 项目中实现高品质动画的最佳选择之一,通过封装通用组件可快速集成到项目中,结合其丰富的 API 能实现灵活的交互控制。本文从基础安装、组件封装、高级交互到性能优化,覆盖了 Lottie 在 Vue3 中的核心使用场景。合理使用 Lottie 可显著提升页面交互体验,同时兼顾性能与兼容性。

如果需要更复杂的场景(如动画分段播放、结合 Vuex/Pinia 控制动画状态),可基于本文的通用组件扩展,结合业务需求定制化开发。

前端 Token 无感刷新全解析:Vue3 与 React 实现方案

在前后端分离架构中,Token 是主流的身份认证方式。但 Token 存在有效期限制,若在用户操作过程中 Token 过期,会导致请求失败,影响用户体验。「无感刷新」技术应运而生——它能在 Token 过期前或过期瞬间,自动刷新 Token 并继续完成原请求,全程对用户透明。

本文将先梳理 Token 无感刷新的核心原理,再分别基于 Vue3(Composition API + Pinia)和 React(Hooks + Axios)给出完整实现方案,同时解析常见问题与优化思路,帮助开发者快速落地。

一、核心原理:为什么需要无感刷新?怎么实现?

1. 基础概念:Access Token 与 Refresh Token

无感刷新依赖「双 Token 机制」,后端需返回两种 Token:

  • Access Token(访问 Token) :有效期短(如 2 小时),用于接口请求的身份认证,放在请求头(如 Authorization: Bearer {token});
  • Refresh Token(刷新 Token) :有效期长(如 7 天),仅用于 Access Token 过期时请求新的 Access Token,安全性要求更高(建议存储在 HttpOnly Cookie 中,避免 XSS 攻击)。

2. 无感刷新核心流程

  1. 前端发起接口请求,携带 Access Token;
  2. 拦截响应:若返回 401 状态码(Access Token 过期),则触发刷新逻辑;
  3. 用 Refresh Token 调用后端「刷新 Token 接口」,获取新的 Access Token;
  4. 更新本地存储的 Access Token;
  5. 重新发起之前失败的请求(携带新 Token);
  6. 若 Refresh Token 也过期(刷新接口返回 401),则跳转至登录页,要求用户重新登录。

关键优化点:避免重复刷新——当多个请求同时因 Token 过期失败时,需保证只发起一次 Refresh Token 请求,其他请求排队等待新 Token 生成后再重试。

二、前置准备:Axios 拦截器封装(通用基础)

无论是 Vue 还是 React,都可基于 Axios 的「请求拦截器」和「响应拦截器」实现 Token 统一处理。先封装一个基础 Axios 实例:

// utils/request.js
import axios from 'axios';

// 创建 Axios 实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量中的接口基础地址
  timeout: 5000 // 请求超时时间
});

// 1. 请求拦截器:添加 Access Token
service.interceptors.request.use(
  (config) => {
    const accessToken = localStorage.getItem('accessToken'); // 简化存储,实际建议 Vue 用 Pinia/React 用状态管理
    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// 2. 响应拦截器:处理 Token 过期逻辑(核心,后续框架差异化实现)
// 此处先留空,后续在 Vue/React 中补充具体逻辑
service.interceptors.response.use(
  (response) => response.data, // 直接返回响应体
  (error) => handleResponseError(error, service) // 错误处理,传入 service 用于重试请求
);

export default service;

三、Vue3 实现方案(Composition API + Pinia)

Vue3 中推荐用 Pinia 管理全局状态(存储 Token),结合 Composition API 封装刷新逻辑,保证代码复用性。

1. 步骤 1:Pinia 状态管理(存储 Token)

创建 Pinia Store 管理 Access Token 和 Refresh Token,提供刷新 Token 的方法:

// stores/authStore.js
import { defineStore } from 'pinia';
import axios from 'axios';

export const useAuthStore = defineStore('auth', {
  state: () => ({
    accessToken: localStorage.getItem('accessToken') || '',
    refreshToken: localStorage.getItem('refreshToken') || '' // 实际建议存 HttpOnly Cookie
  }),
  actions: {
    // 更新 Token
    updateTokens(newAccessToken, newRefreshToken) {
      this.accessToken = newAccessToken;
      this.refreshToken = newRefreshToken;
      localStorage.setItem('accessToken', newAccessToken);
      localStorage.setItem('refreshToken', newRefreshToken); // 仅演示,生产环境用 HttpOnly Cookie
    },
    // 刷新 Token 核心方法
    async refreshAccessToken() {
      try {
        const res = await axios.post('/api/refresh-token', {
          refreshToken: this.refreshToken
        });
        const { accessToken, refreshToken } = res.data;
        this.updateTokens(accessToken, refreshToken);
        return accessToken; // 返回新 Token,用于重试请求
      } catch (error) {
        // 刷新 Token 失败(如 Refresh Token 过期),清除状态并跳转登录
        this.clearTokens();
        window.location.href = '/login';
        return Promise.reject(error);
      }
    },
    // 清除 Token
    clearTokens() {
      this.accessToken = '';
      this.refreshToken = '';
      localStorage.removeItem('accessToken');
      localStorage.removeItem('refreshToken');
    }
  }
});

2. 步骤 2:实现响应拦截器的错误处理

完善之前的响应拦截器,添加 Token 过期处理逻辑,核心是「避免重复刷新」:

// utils/request.js(Vue3 版本补充)
import { useAuthStore } from '@/stores/authStore';

// 用于存储刷新 Token 的请求(避免重复刷新)
let refreshPromise = null;

// 响应错误处理函数
async function handleResponseError(error, service) {
  const authStore = useAuthStore();
  const originalRequest = error.config; // 原始请求配置

  // 1. 不是 401 错误,直接 reject
  if (error.response?.status !== 401) {
    return Promise.reject(error);
  }

  // 2. 是 401 错误,但已经重试过一次,避免死循环
  if (originalRequest._retry) {
    return Promise.reject(error);
  }

  try {
    // 3. 标记当前请求已重试,避免重复
    originalRequest._retry = true;

    // 4. 若没有正在进行的刷新请求,发起刷新;否则等待已有请求完成
    if (!refreshPromise) {
      refreshPromise = authStore.refreshAccessToken();
    }

    // 5. 等待刷新完成,获取新 Token
    const newAccessToken = await refreshPromise;

    // 6. 刷新完成后,重置 refreshPromise
    refreshPromise = null;

    // 7. 更新原始请求的 Authorization 头,重新发起请求
    originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
    return service(originalRequest);
  } catch (refreshError) {
    // 刷新失败,重置 refreshPromise
    refreshPromise = null;
    return Promise.reject(refreshError);
  }
}

// 响应拦截器(补充完整)
service.interceptors.response.use(
  (response) => response.data,
  (error) => handleResponseError(error, service)
);

3. 步骤 3:组件中使用

封装好后,组件中直接使用 request 发起请求即可,无需关注 Token 刷新逻辑:

// components/Example.vue
<script setup>
import request from '@/utils/request';
import { ref, onMounted } from 'vue';

const data = ref(null);

onMounted(async () => {
  try {
    // 发起请求,Token 过期时会自动无感刷新
    const res = await request.get('/api/user-info');
    data.value = res.data;
  } catch (error) {
    console.error('请求失败:', error);
  }
});
</script>

<template>
  <div>{{ data ? data.name : '加载中...' }}</div>
</template>

四、React 实现方案(Hooks + Context)

React 中推荐用「Context + Hooks」管理全局 Token 状态,结合 Axios 拦截器实现无感刷新,逻辑与 Vue3 类似,但状态管理方式不同。

1. 步骤 1:创建 Auth Context(管理 Token 状态)

用 Context 提供 Token 相关的状态和方法,供全局组件使用:

// context/AuthContext.js
import { createContext, useContext, useState, useEffect } from 'react';
import axios from 'axios';

// 创建 Context
const AuthContext = createContext();

//  Provider 组件:提供 Token 状态和方法
export function AuthProvider({ children }) {
  const [accessToken, setAccessToken] = useState(localStorage.getItem('accessToken') || '');
  const [refreshToken, setRefreshToken] = useState(localStorage.getItem('refreshToken') || '');

  // 更新 Token
  const updateTokens = (newAccessToken, newRefreshToken) => {
    setAccessToken(newAccessToken);
    setRefreshToken(newRefreshToken);
    localStorage.setItem('accessToken', newAccessToken);
    localStorage.setItem('refreshToken', newRefreshToken); // 演示用,生产环境用 HttpOnly Cookie
  };

  // 刷新 Token
  const refreshAccessToken = async () => {
    try {
      const res = await axios.post('/api/refresh-token', { refreshToken });
      const { accessToken: newAccessToken, refreshToken: newRefreshToken } = res.data;
      updateTokens(newAccessToken, newRefreshToken);
      return newAccessToken;
    } catch (error) {
      // 刷新失败,清除状态并跳转登录
      clearTokens();
      window.location.href = '/login';
      return Promise.reject(error);
    }
  };

  // 清除 Token
  const clearTokens = () => {
    setAccessToken('');
    setRefreshToken('');
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
  };

  // 提供给子组件的内容
  const value = {
    accessToken,
    refreshToken,
    updateTokens,
    refreshAccessToken,
    clearTokens
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// 自定义 Hook:方便组件获取 Auth 状态
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

2. 步骤 2:在入口文件中包裹 AuthProvider

确保全局组件都能访问到 Auth Context:

// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { AuthProvider } from './context/AuthContext';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <AuthProvider>
    <App />
  </AuthProvider>
);

3. 步骤 3:完善 Axios 响应拦截器

逻辑与 Vue3 一致,核心是避免重复刷新,通过 useAuth Hook 获取刷新 Token 方法:

// utils/request.js(React 版本补充)
import { useAuth } from '../context/AuthContext';

// 注意:React 中不能在 Axios 拦截器中直接使用 useAuth(Hook 只能在组件/自定义 Hook 中使用)
// 解决方案:用一个函数封装,在组件初始化时调用,注入 auth 实例
export function initRequestInterceptors() {
  const { refreshAccessToken } = useAuth();
  let refreshPromise = null;

  // 响应错误处理函数
  async function handleResponseError(error, service) {
    const originalRequest = error.config;

    // 1. 非 401 错误,直接 reject
    if (error.response?.status !== 401) {
      return Promise.reject(error);
    }

    // 2. 已重试过,避免死循环
    if (originalRequest._retry) {
      return Promise.reject(error);
    }

    try {
      originalRequest._retry = true;

      // 3. 避免重复刷新
      if (!refreshPromise) {
        refreshPromise = refreshAccessToken();
      }

      // 4. 等待新 Token
      const newAccessToken = await refreshPromise;
      refreshPromise = null;

      // 5. 重试原始请求
      originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
      return service(originalRequest);
    } catch (refreshError) {
      refreshPromise = null;
      return Promise.reject(refreshError);
    }
  }

  // 重新设置响应拦截器(注入 auth 实例后)
  service.interceptors.response.use(
    (response) => response.data,
    (error) => handleResponseError(error, service)
  );
}

export default service;

4. 步骤 4:在组件中初始化拦截器并使用

在根组件(如 App.js)中初始化拦截器,确保 useAuth 能正常使用:

// App.js
import { useEffect } from 'react';
import { initRequestInterceptors } from './utils/request';
import request from './utils/request';
import { useState } from 'react';

function App() {
  const [userInfo, setUserInfo] = useState(null);

  // 初始化 Axios 拦截器(注入 Auth 上下文)
  useEffect(() => {
    initRequestInterceptors();
  }, []);

  // 发起请求(Token 过期自动刷新)
  const fetchUserInfo = async () => {
    try {
      const res = await request.get('/api/user-info');
      setUserInfo(res.data);
    } catch (error) {
      console.error('请求失败:', error);
    }
  };

  useEffect(() => {
    fetchUserInfo();
  }, []);

  return (
    <div className="App">
      {userInfo ? <h1>欢迎,{userInfo.name}</h1> : <p>加载中...</p>}
    </div>
  );
}

export default App;

五、关键优化与安全注意事项

1. 避免重复刷新的核心逻辑

用「refreshPromise」变量存储正在进行的刷新 Token 请求,当多个请求同时失败时,都等待同一个 refreshPromise 完成,避免发起多个刷新请求,这是无感刷新的核心优化点。

2. 安全优化:Refresh Token 的存储方式

  • 不建议将 Refresh Token 存储在 localStorage/sessionStorage 中,容易遭受 XSS 攻击;

  • 推荐存储在「HttpOnly Cookie」中,由浏览器自动携带,无法通过 JavaScript 访问,有效防御 XSS 攻击;

  • 若后端支持,可给 Refresh Token 增加「设备绑定」「IP 限制」等额外安全措施。

3. 主动刷新:提前预防 Token 过期

被动刷新(等待 401 后再刷新)可能存在延迟,可增加「主动刷新」逻辑:

  • 记录 Access Token 的生成时间和过期时间;
  • 在请求拦截器中判断 Token 剩余有效期(如小于 5 分钟),主动发起刷新请求;
  • 避免在用户无操作时刷新,可结合「用户活动监听」(如 click、keydown 事件)触发主动刷新。

4. 异常处理:刷新失败的兜底方案

当 Refresh Token 过期或无效时,必须跳转至登录页,并清除本地残留的 Token 状态,避免死循环请求。同时,可给用户提示「登录已过期,请重新登录」,提升体验。

六、Vue3 与 React 实现方案对比

对比维度 Vue3 实现 React 实现
状态管理 Pinia(官方推荐,API 简洁,支持 TypeScript) Context + Hooks(原生支持,无需额外依赖)
拦截器初始化 可直接在 Pinia 中获取状态,无需额外注入 需在组件中初始化拦截器,注入 Auth Context
核心逻辑 基于 Composition API,逻辑封装更灵活 基于自定义 Hooks,符合函数式编程思想
学习成本 Pinia 学习成本低,适合 Vue 生态开发者 Context + Hooks 需理解 React 状态传递机制

本质差异:状态管理方式不同,但无感刷新的核心逻辑(双 Token、拦截器、避免重复刷新)完全一致,开发者可根据自身技术栈选择对应方案。

七、总结

前端 Token 无感刷新的核心是「双 Token 机制 + Axios 拦截器」,关键在于解决「重复刷新」和「安全存储」问题。Vue3 和 React 的实现方案虽在状态管理上有差异,但核心逻辑相通:

  1. 用请求拦截器统一添加 Access Token;
  2. 用响应拦截器捕获 401 错误,触发刷新逻辑;
  3. 通过一个全局变量控制刷新请求的唯一性,避免重复请求;
  4. 刷新成功后重试原始请求,失败则跳转登录。

实际项目中,需结合后端接口设计(如刷新 Token 的接口地址、参数格式)和安全需求(如 Refresh Token 存储方式)调整实现细节。合理的无感刷新方案能大幅提升用户体验,避免因 Token 过期导致的操作中断。

Vue 与 React 数据体系深度对比

在前端框架生态中,Vue 和 React 无疑是两大主流选择。两者的核心差异不仅体现在语法风格上,更根植于数据管理的设计理念——前者追求“渐进式”与“易用性”,后者强调“函数式”与“可预测性”。本文将从数据核心设计、状态管理、数据绑定、性能优化等关键维度,结合实际代码案例,深度解析 Vue(以 Vue3 为主)与 React 的数据体系差异,帮助开发者根据项目需求做出更合适的技术选型。

一、核心设计理念:响应式 vs 单向数据流

Vue 和 React 对“数据如何驱动视图”的核心认知不同,直接决定了两者数据体系的底层逻辑。

1. Vue:响应式数据驱动(自动追踪依赖)

Vue 的核心设计之一是响应式系统。其核心思想是:当数据发生变化时,视图会自动更新,开发者无需手动处理数据与视图的同步逻辑。Vue3 采用 ES6 Proxy 实现响应式,相比 Vue2 的 Object.defineProperty,解决了数组索引监听、对象新增属性等痛点。

Vue 的响应式流程可概括为:

  • 初始化时,通过 Proxy 代理数据对象,拦截数据的读取(get)和修改(set)操作;
  • 读取数据时(如渲染视图),收集依赖(即当前使用该数据的组件/DOM);
  • 数据修改时(如赋值操作),触发依赖更新,自动重新渲染相关视图。

代码示例(Vue3 响应式数据):

<script setup>
import { ref, reactive } from 'vue'

// 基本类型响应式数据
const count = ref(0)
// 引用类型响应式数据
const user = reactive({ name: '张三', age: 20 })

// 直接修改数据,视图自动更新
const increment = () => {
  count.value++ // ref 需通过 .value 访问/修改
  user.age++    // reactive 可直接修改属性
}
</script>

<template>
  <div>计数:{{ count }}</div>
  <div>姓名:{{ user.name }}, 年龄:{{ user.age }}</div>
  <button @click="increment">增加</button>
</template>

从代码可以看出,Vue 对开发者的“侵入性”较低,数据修改逻辑直观,更接近原生 JavaScript 写法,降低了学习成本。

2. React:单向数据流(手动触发更新)

React 的核心设计是单向数据流函数式组件。其核心思想是:数据通过 props 从父组件传递到子组件,子组件不能直接修改父组件传递的数据;当数据需要更新时,必须通过“修改状态 + 重新渲染”的方式触发视图更新,全程数据流可追踪、可预测。

React 的数据更新流程可概括为:

  • 通过 useState/useReducer 定义状态(state);
  • 视图由状态和 props 计算得出(纯函数渲染);
  • 数据更新时,必须调用 setState 或 dispatch 方法(不可直接修改 state);
  • 状态更新后,组件会重新执行渲染函数,生成新的虚拟 DOM,通过 Diff 算法对比新旧虚拟 DOM,最终只更新变化的 DOM 节点。

代码示例(React 函数式组件状态):

import { useState } from 'react';

function App() {
  // 定义状态:count 和 user(不可直接修改)
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: '张三', age: 20 });

  const increment = () => {
    // 1. 基本类型:通过 setCount 传递新值
    setCount(count + 1);
    // 2. 引用类型:必须创建新对象(不可直接修改 user.age)
    setUser({
      ...user, // 浅拷贝原有属性
      age: user.age + 1
    });
  };

  return (
    <div>
      <div>计数:{count}</div>
      <div>姓名:{user.name}, 年龄:{user.age}</div>
      <button onClick={increment}>增加</button>
    </div>
  );
}

React 强制要求“状态不可变”(Immutability),直接修改 state 不会触发视图更新。这种设计虽然增加了一定的代码量,但保证了数据流的清晰可追踪,尤其在复杂项目中,能有效减少因数据突变导致的 Bug。

二、状态管理:内置简化 vs 生态完善

当项目规模扩大时,组件间的数据共享和状态管理成为核心需求。Vue 和 React 在状态管理上的思路差异明显:Vue 倾向于内置简化方案,React 则依赖生态插件。

1. Vue:内置 API + Pinia 轻量方案

Vue 为不同规模的项目提供了渐进式的状态管理方案:

  • 小型项目:无需额外插件,通过 provide/inject API 实现跨组件数据共享。provide 在父组件提供数据,inject 在子组件(无论层级深浅)注入数据,适用于简单的跨层级通信。
  • 中大型项目:官方推荐 Pinia(替代 Vuex)。Pinia 是 Vue 团队开发的状态管理库,设计简洁,支持 TypeScript,无需嵌套模块(Vuex 的 modules),直接通过定义“存储(Store)”管理状态,且与 Vue3 的 Composition API 无缝衔接。

Pinia 代码示例:

// stores/counter.js
import { defineStore } from 'pinia'

// 定义并导出 Store
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }), // 状态
  actions: { // 修改状态的方法(支持异步)
    increment() {
      this.count++
    },
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count++
    }
  },
  getters: { // 计算属性
    doubleCount: (state) => state.count * 2
  }
})

// 组件中使用
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>

<template>
  <div>计数:{{ counterStore.count }}</div>
  <div>双倍计数:{{ counterStore.doubleCount }}</div>
  <button @click="counterStore.increment">增加</button>
  <button @click="counterStore.incrementAsync">异步增加</button>
</template>

Pinia 的优势在于“轻量”和“易用”,去掉了 Vuex 中繁琐的概念(如 mutations),异步操作直接在 actions 中处理,符合开发者的直觉。

2. React:useContext + useReducer 基础方案 + Redux 生态

React 本身没有内置的状态管理库,而是通过“基础 API + 生态插件”的方式满足不同规模的需求:

  • 小型项目:使用 useContext + useReducer 组合实现跨组件状态管理。useContext 用于传递数据(类似 Vue 的 provide/inject),useReducer 用于管理复杂状态逻辑(类似 Vuex 的 mutations/actions)。
  • 中大型项目:使用 Redux 生态(如 Redux Toolkit、Zustand、Jotai 等)。其中,Redux Toolkit 是官方推荐的 Redux 简化方案,解决了原生 Redux 代码繁琐、模板化严重的问题;Zustand 和 Jotai 则是更轻量的替代方案,API 更简洁,学习成本更低。

useContext + useReducer 代码示例:

import { createContext, useContext, useReducer } from 'react';

// 1. 创建上下文
const CounterContext = createContext();

// 2. 定义 reducer(处理状态更新逻辑)
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'INCREMENT_ASYNC':
      return { ...state, count: state.count + 1 };
    default:
      return state;
  }
}

// 3. 父组件:提供状态和方法
function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  const increment = () => {
    dispatch({ type: 'INCREMENT' });
  };

  const incrementAsync = async () => {
    await new Promise(resolve => setTimeout(resolve, 1000));
    dispatch({ type: 'INCREMENT_ASYNC' });
  };

  return (
    <CounterContext.Provider value={{ state, increment, incrementAsync }}>
      {children}
    </CounterContext.Provider>
  );
}

// 4. 子组件:注入并使用状态
function Child() {
  const { state, increment, incrementAsync } = useContext(CounterContext);
  return (
    <div>
      <div>计数:{state.count}</div>
      <button onClick={increment}>增加</button>
      <button onClick={incrementAsync}>异步增加</button>
    </div>
  );
}

// 5. 根组件:包裹 Provider
function App() {
  return (
    <CounterProvider>
      <Child />
    </CounterProvider>
  );
}

Redux Toolkit 则进一步简化了 Redux 的使用,通过 createSlice 自动生成 actions 和 reducers,无需手动编写模板代码。React 状态管理生态的优势在于“灵活”和“成熟”,但也存在学习成本较高的问题,需要开发者根据项目复杂度选择合适的方案。

三、数据绑定:双向绑定 vs 单向绑定

数据绑定是“数据与视图同步”的具体实现方式,Vue 和 React 在此处的差异直接影响表单处理等场景的开发体验。

1. Vue:默认支持双向绑定(v-model)

Vue 提供了 v-model 指令,实现了“数据 - 视图”的双向绑定。v-model 本质是语法糖,底层通过监听输入事件(如 input、change)和设置数据值实现同步。在表单元素(输入框、复选框等)中使用时,开发者无需手动编写事件处理逻辑,极大简化了表单开发。

Vue 双向绑定代码示例:

<script setup>
import { ref } from 'vue'
const username = ref('')
const isAgree = ref(false)
</script>

<template>
  <div>
    <input v-model="username" placeholder="请输入用户名" />
    <p>用户名:{{ username }}</p>

    <input type="checkbox" v-model="isAgree" />
    <p>是否同意:{{ isAgree ? '是' : '否' }}</p>
  </div>
</template>

此外,Vue 还支持自定义组件的 v-model,通过 props 和 emits 实现父子组件间的双向数据同步,灵活性极高。

2. React:单向绑定(需手动处理事件)

React 严格遵循单向绑定原则:数据从 state 流向视图,视图中的用户操作(如输入)不会直接修改 state,而是需要通过事件处理函数调用 setState 手动更新 state,进而驱动视图重新渲染。在表单开发中,开发者需要手动编写 onChange 事件处理逻辑,将输入值同步到 state 中。

React 单向绑定代码示例:

import { useState } from 'react';

function App() {
  const [username, setUsername] = useState('');
  const [isAgree, setIsAgree] = useState(false);

  // 手动处理输入事件,同步到 state
  const handleUsernameChange = (e) => {
    setUsername(e.target.value);
  };

  const handleAgreeChange = (e) => {
    setIsAgree(e.target.checked);
  };

  return (
    <div>
      <input
        value={username}
        onChange={handleUsernameChange}
        placeholder="请输入用户名"
      />
      <p>用户名:{username}</p>

      <input
        type="checkbox"
        checked={isAgree}
        onChange={handleAgreeChange}
      />
      <p>是否同意:{isAgree ? '是' : '否'}</p>
    </div>
  );
}

React 16.8 后推出的 useForm 等库可以简化表单处理,但核心依然遵循单向绑定原则。这种设计虽然代码量稍多,但保证了数据流的清晰可追踪,避免了双向绑定中“数据来源不明确”的问题。

四、性能优化:自动优化 vs 手动优化

数据更新引发的重新渲染是影响前端性能的关键因素。Vue 和 React 在性能优化的思路上差异显著:Vue 倾向于“自动优化”,减少开发者的手动干预;React 则需要开发者通过 API 手动优化。

1. Vue:细粒度响应式 + 自动 Diff 优化

Vue 的响应式系统本身就是一种性能优化:由于响应式数据会精准追踪依赖,只有使用了该数据的组件才会在数据更新时重新渲染,实现了“细粒度更新”。此外,Vue3 在编译阶段会进行一系列优化,如:

  • 静态提升:将静态 DOM 节点(如无数据绑定的 div)提升到渲染函数外部,避免每次渲染都重新创建;
  • PatchFlags:标记动态节点的更新类型(如仅文本更新、仅 class 更新),在 Diff 时只检查标记的动态节点,减少 Diff 开销;
  • 缓存事件处理函数:避免每次渲染都创建新的函数实例,减少不必要的重新渲染。

对于复杂场景,Vue 也提供了手动优化 API,如 computed(缓存计算结果)、watch(精准监听数据变化)、shallowRef/shallowReactive(浅响应式,避免深层监听开销)等,但大多数情况下,开发者无需手动优化即可获得较好的性能。

2. React:全组件重新渲染 + 手动优化 API

React 的默认行为是:当组件的 state 或 props 发生变化时,组件会重新渲染,并且会递归重新渲染所有子组件。这种“全组件重新渲染”在复杂项目中可能导致性能问题,因此 React 提供了一系列手动优化 API:

  • React.memo:缓存组件,只有当 props 发生浅变化时才重新渲染;
  • useMemo:缓存计算结果,避免每次渲染都重新计算;
  • useCallback:缓存事件处理函数,避免因函数实例变化导致子组件不必要的重新渲染;
  • useMemoizedFn(第三方库,如 ahooks):进一步优化函数缓存,支持深层依赖对比。

React 手动优化代码示例:

import { useState, useCallback, memo } from 'react';

// 子组件:使用 React.memo 缓存
const Child = memo(({ count, onIncrement }) => {
  console.log('子组件重新渲染');
  return (
    <button onClick={onIncrement}>
      子组件:增加计数(当前:{count})
    </button>
  );
});

function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('张三');

  // 使用 useCallback 缓存事件处理函数
  const handleIncrement = useCallback(() => {
    setCount(count + 1);
  }, [count]); // 依赖 count,只有 count 变化时才重新创建函数

  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="修改姓名"
      />
      <Child count={count} onIncrement={handleIncrement} />
    </div>
  );
}

在上述示例中,若不使用 React.memo 和 useCallback,修改 name 时,Child 组件也会重新渲染(因为父组件重新渲染会创建新的 onIncrement 函数实例);使用优化 API 后,只有 count 变化时,Child 组件才会重新渲染。React 的优化思路要求开发者对“重新渲染”有清晰的认知,学习成本较高,但也赋予了开发者更精细的性能控制能力。

五、总结:差异对比与选型建议

通过以上维度的对比,我们可以清晰地看到 Vue 和 React 数据体系的核心差异,下表对关键特性进行了汇总:

对比维度 Vue React
核心设计理念 响应式数据驱动,自动同步视图 单向数据流,函数式组件,可预测性优先
状态管理 内置 provide/inject,官方推荐 Pinia(轻量易用) 基础 useContext + useReducer,生态丰富(Redux Toolkit、Zustand 等)
数据绑定 默认支持双向绑定(v-model),表单开发简洁 单向绑定,需手动处理事件同步数据
性能优化 细粒度响应式 + 编译时自动优化,手动优化需求少 默认全组件重新渲染,需手动使用 memo/useMemo 等 API 优化
学习成本 较低,API 直观,接近原生 JavaScript,渐进式学习 较高,需理解函数式编程、不可变数据、重新渲染等概念

选型建议:

  1. 小型项目/快速迭代项目:优先选择 Vue。其响应式系统和双向绑定能大幅提升开发效率,学习成本低,团队上手快。
  2. 中大型项目/复杂状态管理项目:两者均可。若团队熟悉函数式编程,追求数据流可预测性,可选择 React + Redux Toolkit/Zustand;若团队更注重开发效率,希望减少手动优化工作,可选择 Vue3 + Pinia。
  3. 跨端项目:React 生态的 React Native 成熟度更高,适合需开发原生 App 的项目;Vue 生态的 Uni-app、Weex 更适合多端(小程序、H5、App)快速开发。
  4. 团队技术栈:若团队已有 JavaScript 基础,Vue 上手更平滑;若团队熟悉 TypeScript 和函数式编程,React 更易融入。
❌