阅读视图

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

一文看懂:Vue3 watch 用 VuReact 转成 React 长啥样

大家好,我是专注前端框架迁移、编译工具实践的掘金博主~在 Vue3 转 React 的过程中,watch 作为最常用的响应式监听 API,手动改写很容易丢失逻辑、写错依赖。

今天继续用 VuReact 工具,给大家带来 Vue3 watch → React 编译对照,全程一比一还原、保留所有行为与内链,看完直接上手迁移。


前言

先明确核心: VuReact 是能将 Vue 3 代码编译为标准、可维护 React 代码的工具 它最大亮点:编译阶段自动分析依赖、自动生成依赖追踪,完美对齐 Vue 响应式监听行为,不用手动处理 React Hooks 依赖。

本文只聚焦一个高频 API: 👉 Vue3 watch → React 等价代码 全程对照,不冗余、直接看核心。

前置约定(避免理解偏差)

为了示例清爽,先统一两点:

  1. 只保留核心逻辑,省略组件包裹、无关配置
  2. 默认你已熟悉 Vue3 watch 用法与核心行为

一、基础版:watch → useWatch

Vue 标准 watch 监听,支持 immediate、清理函数 onCleanup,VuReact 直接编译为 useWatch

Vue 源码

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

watch(
  userId,
  async (newId, oldId, onCleanup) => {
    let cancelled = false;
    onCleanup(() => {
      cancelled = true;
    });
    const data = await fetchUser(newId);
    if (!cancelled) {
      userData.value = data;
    }
  },
  { immediate: true },
);
</script>

VuReact 编译后 React 代码

import { useVRef, useWatch } from '@vureact/runtime-core';
const userId = useVRef(1);

useWatch(
  userId,
  async (newId, oldId, onCleanup) => {
    let cancelled = false;
    onCleanup(() => {
      cancelled = true;
    });
    const data = await fetchUser(newId);
    if (!cancelled) {
      setUserData(data);
    }
  },
  { immediate: true },
);

核心要点

  • Vue watch() 直接编译为 useWatch
  • 完全保留:回调参数、immediateonCleanup 清理机制
  • 编译阶段自动分析依赖、深度追踪,无需手动管理依赖数组

二、深度监听 & 多源监听:对象/数组来源兼容

watch 监听对象内部属性、多源数组时,VuReact 同样支持 deep 与多源写法,行为完全对齐 Vue。

Vue 源码(深度监听 + 多源监听)

<script setup>
import { reactive, watch } from 'vue';
const state = reactive({
  info: { name: 'Vureact', version: '1.0' },
  count: 0,
});

// 深度监听对象内部
watch(
  () => state.info,
  (newInfo) => {
    console.log('对象内部变化:', newInfo.name);
  },
  { deep: true },
);

// 多源监听
watch([state.count, () => state.info.name], ([newCount, newName]) => {
  console.log('计数:', newCount, '名称:', newName);
});
</script>

VuReact 编译后 React 代码

import { useReactive, useWatch } from '@vureact/runtime-core';
const state = useReactive({
  info: { name: 'Vureact', version: '1.0' },
  count: 0,
});

useWatch(
  () => state.info,
  (newInfo) => {
    console.log('对象内部变化:', newInfo.name);
  },
  { deep: true },
);

useWatch([state.count, () => state.info.name], ([newCount, newName]) => {
  console.log('计数:', newCount, '名称:', newName);
});

对应关系

  • 监听函数写法、deep: true 深度监听完全保留
  • 多源数组监听直接兼容
  • 编译器自动做依赖分析,不用手动写 deps

三、一句话总结

用 VuReact 做 Vue3 → React 迁移,watch 相关规则:

  1. watchuseWatch
  2. 支持 immediate / deep / onCleanup 全部选项
  3. 支持单源、函数返回值、多源数组监听
  4. 依赖自动追踪,无需手动管理依赖数组
  5. 行为 1:1 对齐 Vue,迁移零逻辑损耗

相关资源

❤️ 觉得有用就 点赞 + 收藏 + 关注,持续更新前端迁移/编译工具实战!

一文看懂:Vue3 watchEffect 用 VuReact 转成 React 长啥样

大家好,我是专注前端框架迁移、编译工具实践的掘金博主~最近很多同学在做 Vue3 → React 技术栈迁移,被响应式 API 对齐、依赖手动管理搞得头大,尤其是 watchEffect 这种自动依赖收集的核心 API,在 React 里很容易漏写依赖。

今天就用 VuReact 这个编译工具,直接把 Vue3 watchEffect 的各种用法一比一翻译成标准可维护的 React 代码,全程对照、看完即用。


前言

先明确核心: VuReact 是能将 Vue 3 代码编译为标准、可维护 React 代码的工具 它最大亮点:编译阶段自动分析依赖、自动生成依赖数组,完美对齐 Vue 响应式行为,不用手动维护 React Hooks 依赖。

本文只聚焦一个高频 API: 👉 Vue3 watchEffect → React 等价代码 全程对照,不冗余、直接看核心。

前置约定(避免理解偏差)

为了示例清爽,先统一两点:

  1. 只保留核心逻辑,省略组件包裹、无关配置
  2. 默认你已熟悉 Vue3 watchEffect 用法与行为

一、基础版:watchEffect → useWatchEffect

Vue 最常用的基础 watchEffect,自动收集依赖、自动触发副作用。

Vue 源码

<script setup>
import { ref, watchEffect } from 'vue';
const count = ref(0);

watchEffect(() => {
  console.log(`当前计数是: ${count.value}`);
});
</script>

VuReact 编译后 React 代码

import { useVRef, useWatchEffect } from '@vureact/runtime-core';
const count = useVRef(0);

useWatchEffect(() => {
  console.log(`当前计数是: ${count.value}`);
}, [count.value]);

核心要点

  • Vue watchEffect() 直接编译为 useWatchEffect
  • 编译阶段自动分析依赖并生成精准依赖数组,无需手动管理
  • 完全模拟 Vue watchEffect 的自动依赖收集、清理机制、停止控制

二、带 flush 选项:post / sync 对齐渲染时机

Vue 中通过 flush: 'post' / flush: 'sync' 控制执行时机,VuReact 直接映射为专用 Hook,保持渲染时机一致。

Vue 源码(post + sync)

<script setup>
import { ref, watchEffect } from 'vue';
const width = ref(0);
const elRef = ref(null);

// DOM 更新后执行
watchEffect(
  () => {
    if (elRef.value) {
      width.value = elRef.value.offsetWidth;
    }
  },
  { flush: 'post' },
);

// 同步立即执行
watchEffect(
  () => {
    console.log(elRef.value);
  },
  { flush: 'sync' },
);
</script>

VuReact 编译后 React 代码

import { useVRef } from '@vureact/runtime-core';
import { useWatchPostEffect, useWatchSyncEffect } from '@vureact/runtime-core';

const width = useVRef(0);
const elRef = useVRef(null);

useWatchPostEffect(
  () => {
    if (elRef.value) {
      width.value = elRef.value.offsetWidth;
    }
  },
  [elRef.value, width.value, elRef.value.offsetWidth]
);

useWatchSyncEffect(
  () => {
    console.log(elRef.value);
  },
  [elRef.value]
);

对应关系

  • flush: 'post'useWatchPostEffect
  • flush: 'sync'useWatchSyncEffect
  • 执行时机、依赖追踪、副作用行为完全对齐 Vue
  • 依赖数组依旧自动生成,无需手动编写

三、一句话总结

用 VuReact 做 Vue3 → React 迁移,watchEffect 相关规则:

  1. watchEffectuseWatchEffect
  2. flush: 'post'useWatchPostEffect
  3. flush: 'sync'useWatchSyncEffect
  4. 依赖自动收集、deps 自动生成,不用手动维护
  5. 行为 1:1 对齐 Vue,迁移成本极低

相关资源

互动一下

你在 Vue 转 React 时,最头疼哪个 API? watch / computed / defineProps / defineEmits? 评论区留言,下期直接出对照编译手册

❤️ 觉得有用就 点赞 + 收藏 + 关注,持续更新前端迁移/编译工具实战!

antdv-next/x:面向 Vue 的 AI 组件体系

写在前面

antdv-next/x 的核心价值,是让 Ant Design X 的源设计体系在 Vue 中可复用、可扩展、可落地。

如果你正在做 AI 产品,这意味着你不用从零搭一套“聊天+生成+引用+反馈”的界面体系,也不用在一致性和开发效率之间反复取舍。

antdv-next/x 把这些高频能力沉淀成可复用的 Vue 组件,让团队可以更快上线、更稳迭代。

为什么现在需要它?

传统组件库解决的是通用页面问题,但 AI 产品面临的是另一套体验挑战:

  • 回答要流式呈现,状态要可感知
  • 输入不只是文本,还包括 Prompt 组织与附件
  • 多轮会话需要上下文切换与管理
  • 输出结果需要复制、重试、反馈、引用溯源

这些能力如果每个项目都重写一遍,代价会非常高:

  • 开发周期被拉长
  • 交互风格难统一
  • 后续维护与扩展成本持续上升

antdv-next/x 带来的价值

1. 更快落地 AI 界面

开箱即用的 Vue 3 AI 组件,减少重复造轮子,把时间投入到业务差异化能力上。

2. 更统一的产品体验

提炼自 Ant Design X 的交互语言与视觉风格,并与 antdv-next 体验协同,降低“页面像拼起来的”割裂感。

3. 更灵活的场景扩展

不仅能做 Chat,还能覆盖 Agent 任务流、知识问答、附件处理、推理过程展示等复合场景。

4. 更稳的工程基础

内建 Markdown 增强渲染能力,支持流式渲染、公式、代码高亮、Mermaid;同时提供 TypeScript 类型支持,并兼容 SSR 与 Electron。

设计取向:融合,而非照搬

antdv-next/x 不是 React 方案的机械迁移,而是面向 Vue 生态的原生化实现:

  • 保留 Vue 的表达习惯(Slots、Composition API)
  • 强化可定制渲染,适应 AI 场景快速变化
  • 对齐 antdv-next 的主题与开发体验

适合谁用?

  • 已经基于 antdv-next 开发业务系统的团队
  • 正在搭建 AI 助手、Copilot、问答、Agent 类产品的团队
  • 需要“快速上线 + 长期可维护”并重的团队

写在最后

如果你希望在 Vue 生态里,以更低成本交付高质量 AI 界面,antdv-next/x 是一条非常务实的路径。

npm install @antdv-next/x

欢迎试用,也欢迎在 GitHub 提 Issue 一起共建。

全面升级!看看人家的后台管理系统,确实清新优雅!

关注过我的mall项目的小伙伴应该有所了解,mall项目的后台管理系统一直都是Vue2版本的,主要原因是项目从Vue2升级到Vue3基本等于要重写了。 最近我花了一个月的时间,将mall项目的后台管理系统升级到了Vue3版本,今天和大家聊聊做了哪些升级!

项目介绍

mall-admin-web是mall电商项目后台管理系统的前端项目,基于Vue3+Element-Plus实现。主要包括商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等功能。

下面是mall-admin-web项目运行的效果图,界面还是很清新优雅的!如果你想体验完整功能的话,可以访问这个在线演示地址:www.macrozheng.com/admin/

技术栈

mall-admin-web技术栈已经全面升级,基于目前主流的前端技术栈,版本也是比较新的,具体技术栈如下。

技术 说明 版本
Vue 前端框架 3.5.25
Element Plus 前端UI框架 2.12.0
Vue Router 路由框架 4.6.3
Pinia 全局状态管理框架 3.0.4
Pinia Plugin Persistedstate Pinia持久化插件 4.7.1
Axios 前端HTTP框架 1.13.2
Vue-charts 基于Echarts的图表框架 8.0.1
TinyMCE Vue 富文本编辑器 5.1.1
Js-cookie cookie管理工具 3.0.5

升级内容

这里和大家聊聊mall-admin-web做了哪些升级!

Vue2升级Vue3

项目的Vue版本从之前的2.7.2升级到了3.5.25,改动还是挺大的,之前使用的选项式API都已经改成了Vue3的组合式API。

我在升级项目的同时,给代码添加了更加详尽的注释,方便大家来学习。

之前经常有小伙伴问接口文档在哪里,其实把后端项目运行起来,就有接口文档了,我这里给前端调用的接口方法添加了详细的注释,大家也可以直接从代码中查看接口调用。

JavaScript升级TypeScript

TypeScript我们可以把它看作是带有类型的JavaScript,JavaScript里的支持的语法,它基本都支持。

项目中对于使用到的对象添加了类型支持,用起来有点Java中对象的感觉。

这样我们在编写代码时就可以有属性提示了,使用TypeScript我们在编译时就可以发现错误,以便及时修正。

这里有两者使用的优势对比,大家可以参考下!

Element UI升级Element Plus

由于Element UI已经停止更新,这里升级到了支持Vue3的Element Plus组件库,两者使用过程中的特性与优缺点对比如下。

Vuex升级Pinia

Pinia是Vue官方开发的状态管理库,使用它API更简洁,而且完美支持Vue3和TypeScript。

项目中的用户信息存储就使用了它,配合pinia-plugin-persistedstate插件,还可以实现数据的持久化。

两者使用过程中的特性与优缺点对比如下。

v-charts升级vue-charts

之前项目中使用的图表库v-charts已经停止维护,这里升级到了vue-charts,使用该库生成的图表功能也更加强大了!

两者使用过程中的特性与优缺点对比如下。

总结

今天给大家分享了mall后台管理系统前端的升级内容,主要是项目升级到了Vue3,一些过时的库也迁移到了新的库,升级之后项目更加适合学习了,感兴趣的小伙伴可以学习下!

项目地址

一个轻量级 Vue3 轮播组件:支持多视图、滑动距离决定切换数量,核心原理与 Swiper 对比

一个轻量级 Vue3 轮播组件:支持多视图、滑动距离决定切换数量,核心原理与 Swiper 对比

在这里插入图片描述

支持 slidesPerViewspaceBetween、滑动距离决定滑动数量,代码仅 400 行,核心原理全解析。

引言

在业务开发中,轮播图是几乎每个前端都会遇到的场景。Swiper 无疑是功能最全面的库,但它体积较大(核心库 ~30kB,加上模块更重),且在某些轻量化项目中显得有些“杀鸡用牛刀”。因此,我决定用 Vue 3 + TypeScript 手写一个轮播组件,只保留最常用的 NavigationPagination,同时支持多视图(slidesPerView)和间距(spaceBetween),并实现“根据滑动距离决定切换数量”的自然交互。

本文会详细讲解实现原理、核心难点,并与 Swiper 进行对比,希望能给正在造轮子或想深入理解轮播机制的你一些启发。

组件特性

  • 多视图模式:通过 slidesPerView 控制每屏显示几张幻灯片
  • 可配置间距spaceBetween 设置幻灯片之间的间隔
  • 循环播放:无缝无限滚动,复制首尾元素实现
  • 自动播放:支持悬停暂停
  • 拖拽滑动:鼠标/触摸拖拽,根据滑动距离(四舍五入)决定一次滑动的 slide 数量,而非固定 1 张
  • 导航与分页:分页器在非循环模式下显示可滑动步数(总条数 - 每屏个数 + 1
  • 点击事件:区分拖拽与点击,避免误触发
  • TypeScript:完整类型定义,便于接入大型项目

实现原理

1. 多视图与间距的布局计算

核心思路:使用 flex 布局,每个 slide 的宽度动态计算,右外边距实现间距。

const slideWidth = (containerWidth - (slidesPerView - 1) * spaceBetween) / slidesPerView;
const slideStep = slideWidth + spaceBetween; // 每次滚动的总步长

滚动时通过 transform: translate3d(-currentOffset * slideStep, 0, 0) 移动整个轨道。

2. 循环模式(Loop)的实现

真正的无限循环不是把数据无限复制,而是在原始数组前后各复制 slidesPerView 个 slide,形成“假首尾”。初始时偏移量设为复制品的起始位置。当用户滑动到复制品区域时,过渡结束后立即无动画跳转到对应的真实 slide,视觉上无感知。

关键步骤

  • displaySlides = [...clonesFront, ...originals, ...clonesBack]
  • displayOffset = cloneCount + activeIndex(循环模式)或 activeIndex(非循环)
  • 过渡结束后检测 displayOffset 是否小于 cloneCount 或大于 cloneCount + originals.length - 1,若是则修正 activeIndex 并重置位置。

注意:分页器在循环模式下仍显示原始数据条数,change 事件始终返回原始索引。

3. 根据滑动距离决定滑动数量

很多简单轮播只支持一次滑动一张,体验呆板。我们希望像 Swiper 那样:拖拽超过半个 slide 宽度就切换,且滑动距离越大,一次切换的张数越多

实现方法:

  • 拖拽结束时计算 deltaSlides = Math.round(dragDistance / slideStep)
  • 目标索引 = currentIndex - deltaSlides(向右滑动为正,索引减少)
  • 调用 goTo(newIndex),内部自动处理边界和循环取模。

4. 分页器点数计算

这是许多开发者容易出错的地方。假设有 20 张图,每屏显示 3 张,那么分页器应该有几个点?

  • 非循环模式:用户可以滑动到的不同起始索引有 20 - 3 + 1 = 18 个位置,因此分页器应为 18 个点,每个点代表一组可见 slide。
  • 循环模式:由于可以无限滚动,分页器仍然显示 20 个点,对应原始数据的索引。

组件中通过 maxStartIndex = slides.length - slidesPerView 计算最大起始索引,paginationCount = loop ? slides.length : maxStartIndex + 1

5. 拖拽与点击的区分

直接给 slide 绑 @click 会导致拖拽结束后也触发点击。解决方案:在 touchstart/mousedown 时设置 dragOccurred = false,在 touchmove 中检测移动距离超过 5px 时置为 truetouchend 时重置(延迟一帧)。click 事件检查该标志,若为 true 则忽略。

6. 自动播放与性能优化

  • 自动播放使用 setInterval,在用户交互(拖拽、点击导航)时重置定时器。
  • 窗口 resize 时重新计算宽度并修正位置。
  • 使用 will-change: transform 开启 GPU 加速。

与 Swiper 的对比

维度 本组件 Swiper
体积 ~400 行源码,无依赖 核心 ~30KB,完整功能 ~70KB+
功能覆盖 Navigation, Pagination, 多视图, 循环, 自动播放, 拖拽滑动数量 所有你能想到的轮播功能(缩略图、3D 流、懒加载、RTL 等)
学习成本 极低,Props 直观 配置项丰富,需要查阅文档
扩展性 简单,可自由修改源码 通过模块和 API 扩展,但定制复杂功能仍需理解内部机制
TypeScript 原生 TS 编写,类型完整 有 @types/swiper,但配置项类型复杂
移动端适配 支持触摸,已处理被动事件 专业级,手势非常顺滑
维护性 个人项目,需自行维护 社区维护,更新及时
适用场景 轻量级项目、特定场景、学习目的 企业级、复杂交互、追求稳定全面

总结:如果你的项目只需要基础轮播且对体积敏感,或者你想完全掌控交互细节,这个组件是很好的选择;如果需要支持 IE、复杂手势或特殊效果,Swiper 仍是首选。

组件使用示例

<template>
  <Carousel
    :slides="banners"
    :slidesPerView="3"
    :spaceBetween="20"
    :loop="true"
    :autoplay="true"
    @slide-click="onClick"
  >
    <template #slide="{ item }">
      <div class="card">
        <img :src="item.url" />
        <p>{{ item.title }}</p>
      </div>
    </template>
  </Carousel>
</template>

核心代码片段

拖拽滑动数量计算

const endDrag = () => {
  const deltaSlides = Math.round(dragDelta.value / slideStep.value);
  if (deltaSlides !== 0) {
    goTo(activeIndex.value - deltaSlides);
  } else {
    // 回弹
    wrapperRef.value.style.transform = `translate3d(${translateDistance.value}px, 0, 0)`;
  }
};

循环修正

const performLoopCorrection = () => {
  const offset = displayOffset.value;
  const min = cloneCount.value;
  const max = cloneCount.value + slidesLength.value - 1;
  if (offset < min) {
    activeIndex.value += slidesLength.value;
    jumpToOffset(cloneCount.value + activeIndex.value, true);
    emit('loop-correct', activeIndex.value);
  } else if (offset > max) {
    activeIndex.value -= slidesLength.value;
    jumpToOffset(cloneCount.value + activeIndex.value, true);
    emit('loop-correct', activeIndex.value);
  }
};

总结

造轮子不是为了重复发明,而是为了深入理解。通过实现这个轮播组件,我掌握了多视图布局、循环复制的技巧、拖拽距离映射滑动数量、分页器正确计数等核心知识。相比直接使用 Swiper,这个组件让我的 Vue 能力提升了一个台阶。

如果您的项目需要轻量级、可定制的轮播,不妨试试这个组件;如果您需要更全面的功能,Swiper 依然是标杆。希望这篇文章能给您带来启发!


组件代码仓库:可在评论区留言获取完整源码。

你的 Vue 3 reactive(),VuReact 会编译成什么样的 React?

VuReact 是一个能将 Vue 3 代码编译为标准、可维护 React 代码的工具。今天就带大家直击核心:Vue 中高频使用的 reactive()shallowReactive(),经过 VuReact 编译后会变成什么样的 React 代码?

前置约定

  1. 示例只保留核心逻辑,省略完整组件包裹
  2. 你已熟悉 Vue 3 reactive / shallowReactive 用法

一、Vue reactive() → React useReactive()

reactive 是 Vue 3 最核心的对象响应式 API,在 VuReact 中会被精准映射。

基础编译对照

Vue 输入

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

  const state = reactive({
    count: 0,
    title: 'VuReact',
  });
</script>

VuReact 输出(React)

import { useReactive } from '@vureact/runtime-core';

const state = useReactive({
  count: 0,
  title: 'VuReact',
});

reactive 直接编译为 useReactive Hook:

  • 完全保留 Vue 响应式语义
  • 直接修改属性自动触发视图更新
  • 深层对象、数组、Map/Set 全部支持
  • 和 React 生命周期完美协同

TypeScript 场景:类型完整保留

Vue 输入(TS)

<script lang="ts" setup>
  import { reactive } from 'vue';

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

  const state = reactive<{
    loading: boolean;
    users: User[];
    config: Record<string, any>;
  }>({
    loading: false,
    users: [],
    config: { theme: 'dark' },
  });
</script>

VuReact 输出(TS)

import { useReactive } from '@vureact/runtime-core';

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

const state = useReactive<{
  loading: boolean;
  users: User[];
  config: Record<string, any>;
}>({
  loading: false,
  users: [],
  config: { theme: 'dark' },
});

接口、泛型、类型约束完全迁移
React 侧智能提示、类型检查全部正常
不用改一行类型逻辑


二、Vue shallowReactive() → React useShallowReactive()

浅层响应式用于性能优化,只监听顶层属性变化,VuReact 同样完美对齐。

基础编译对照

Vue 输入

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

  const state = shallowReactive({
    nested: { count: 0 },
  });
</script>

VuReact 输出(React)

import { useShallowReactive } from '@vureact/runtime-core';

const state = useShallowReactive({
  nested: { count: 0 },
});

useShallowReactive 行为完全对齐 Vue:

  • 修改顶层属性 → 触发更新
  • 修改深层嵌套属性 → 不触发更新
  • 替换整个对象 → 触发更新
  • 适合大型列表、复杂状态、第三方数据等性能场景

总结一句话

  • Vue reactive → React useReactive
  • Vue shallowReactive → React useShallowReactive
  • 响应式行为一致
  • TypeScript 类型一致
  • 开发心智完全一致

用 VuReact,你可以:

  • 继续用 Vue 3 舒服的写法
  • 直接产出可维护的 React 代码
  • 无痛渐进迁移,不用一次性重构

🔗 相关资源

你的 Vue 3 ref(),VuReact 会编译成什么样的 React?

VuReact 是一个能将 Vue 3 代码编译为标准、可维护 React 代码的编译工具,并非运行时混合框架。今天我们直接看核心:Vue 高频使用的 ref() / shallowRef(),经过 VuReact 编译后会对应 React 的哪些代码?

前置约定

  1. 文中 Vue/React 代码均为核心逻辑简写,省略完整组件与冗余结构
  2. 你已熟悉 Vue 3 refshallowRef 的用法与行为

一、Vue ref() → React useVRef()

ref 是 Vue 3 最基础的响应式 API,在 VuReact 中会被直接编译为 React Hook。

基础编译对照

Vue 输入

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

VuReact 输出(React)

import { useVRef } from '@vureact/runtime-core';
const count = useVRef(0);

ref 会被编译成 useVRef,它是 Vue ref 在 React 里的语义完全对齐的适配 API,保留 .value 访问与响应式更新行为。

带 TypeScript 类型场景

Vue 输入(TS)

<script lang="ts" setup>
  const title = ref<string>('');
  const isLoading = ref<boolean>(false);
  const userList = ref<Array<{ id: number; name: string }>>([]);
  const config = ref<Record<string, any>>({ theme: 'dark' });
</script>

VuReact 输出(TS)

const title = useVRef<string>('');
const isLoading = useVRef<boolean>(false);
const userList = useVRef<Array<{ id: number; name: string }>>([]);
const config = useVRef<Record<string, any>>({ theme: 'dark' });

TS 泛型、类型注解完整保留,React 侧类型提示完全可用。


二、Vue shallowRef() → React useShallowVRef()

shallowRef 是浅层响应式 API,只监听顶层引用变化,适合大对象性能优化。

基础编译对照

Vue 输入

<script setup>
  import { shallowRef } from 'vue';
  const count = shallowRef({ a: { b: 1, c: { d: 2 } } });
</script>

VuReact 输出(React)

import { useShallowVRef } from '@vureact/runtime-core';
const count = useShallowVRef({ a: { b: 1, c: { d: 2 } } });

useShallowVRef 完全对齐 shallowRef 行为:

  • 修改嵌套属性 → 不触发更新
  • 直接替换 .value触发更新

🔗 相关资源

Vue3 转 React:组件透传 Attributes 与 useAttrs 使用详解|VuReact 实战

在 Vue3 迁移 React、跨框架组件封装的场景里,透传 Attributes 是几乎必用、但极易踩坑的能力。Vue 的 $attrs / useAttrs 和 React 的 props 体系设计差异很大,而 VuReact 作为稳定的 Vue3 → React 编译工具,已经把这套逻辑做了完整对齐。

本文带你一次性搞懂:透传属性是什么、为什么必须用 useAttrs、TS 怎么写、转换后长什么样,直接复制就能用。


一、先搞懂:透传 Attributes 到底是什么?

1. Vue 官方定义

透传 attribute:传给组件,但没有被声明为 props / emits 的属性或事件监听器。 最常见:classstyleid、自定义属性、v-on 监听等。

Vue 默认会把它们自动继承到组件根节点,也可以用 $attrsuseAttrs() 手动控制。

2. React 里的等价逻辑

React 没有“透传”这个名词,但行为一致: 所有没在 Props 里定义的属性,都属于“透传属性”,全部挂在 props 上。

区别是:

  • Vue:运行时自动处理
  • React + TS:必须显式写类型,否则报错

3. VuReact 的核心适配规则

VuReact 把透传属性统一理解为: 无类型约束的运行时对象 + 已声明 Props 合并 = 最终组件属性

  • 组件无 Props → 自动生成:props: Record<string, unknown>
  • 组件有 Props → 自动交叉类型:Props & Record<string, unknown>

二、关键:必须从 $attrs 转向 useAttrs()

Vue 里有两种写法:

  • $attrs:运行时隐式变量 → VuReact 无法静态分析
  • useAttrs():显式 API → VuReact 完美支持、推荐唯一写法

1. Vue 中标准 useAttrs 写法(必背)

<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>

好处:

  • 编译器可静态识别
  • 支持 TS 类型注解
  • 符合 React“显式优于隐式”的习惯

2. VuReact 转换规则(一张表看懂)

Vue useAttrs 写法 React 转换结果
无类型 const attrs = props as Record<string, unknown>
类型断言 as Attrs const attrs = props as Attrs
变量带类型 attrs: Attrs const attrs = props as Attrs
搭配 defineProps Props & Record<string, unknown>

三、实战示例:从 Vue 到 React 完整对照

示例 1:基础用法(无 TS)

Vue 输入

<template>
  <div :class="attrs.class" :style="attrs.style">
    {{ attrs.title }}
  </div>
</template>

<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>

React 输出

import { memo } from 'react'

const Comp = memo((props: Record<string, unknown>) => {
  const attrs = props as Record<string, unknown>

  return (
    <div className={attrs.class} style={attrs.style}>
      {attrs.title}
    </div>
  )
})

export default Comp

示例 2:TS 类型增强(企业级推荐)

Vue 输入

<template>
  <div :class="attrs.class" :style="attrs.style">
    {{ attrs.customTitle }}
  </div>
</template>

<script setup lang="ts">
import { useAttrs } from 'vue'

interface CustomAttrs {
  class?: string
  style?: React.CSSProperties
  customTitle?: string
  [key: string]: unknown
}

const props = defineProps<{
  id: string
}>()

const attrs = useAttrs() as CustomAttrs
</script>

React 输出

import { memo } from 'react'

interface CustomAttrs {
  class?: string
  style?: React.CSSProperties
  customTitle?: string
  [key: string]: unknown
}

type ICompProps = { id: string }

const Comp = memo((props: ICompProps & Record<string, unknown>) => {
  const attrs = props as CustomAttrs

  return (
    <div className={attrs.class} style={attrs.style}>
      {attrs.customTitle}
    </div>
  )
})

export default Comp

示例 3:动态属性 / 可选链(真实业务常用)

Vue 输入

<template>
  <div
    :class="[
      'base',
      attrs.class,
      attrs.xx?.class,
      attrs['custom-class']
    ]"
  >
    {{ attrs?.xxx?.content }}
  </div>
</template>

React 输出

import { memo } from 'react'
import { dir } from '@vureact/runtime-core'

const Comp = memo((props: Record<string, unknown>) => {
  const attrs = props

  return (
    <div
      className={dir.cls([
        'base',
        attrs.class,
        attrs.xx?.class,
        attrs['custom-class']
      ])}
    >
      {attrs?.xxx?.content}
    </div>
  )
})

四、避坑指南(VuReact 必看)

  1. 必须用 useAttrs(),禁止用 $attrs 编译器无法分析运行时变量,会丢属性。

  2. TS 尽量写接口 有利于提示、重构、避免空值报错。

  3. class/style 自动适配 classclassName style → 自动适配 React.CSSProperties

  4. defineProps + useAttrs 会自动合并类型 不用手动改。

  5. JS 项目直接用 会被编译成 const attrs = props,完全兼容。


五、总结

VuReact 处理透传 Attributes 的核心思想只有一句话: 把 Vue 隐式的 $attrs 变成显式的 useAttrs,再映射到 React 的 props 体系。

  • 你只管按 Vue 官方写法写
  • 编译器自动转成标准 React TSX
  • 类型安全、生产可用、迁移成本极低

正在做 Vue3 → React 迁移的同学,这套透传方案可以直接进团队规范。


🔗 相关资源


推荐阅读

#Vue3 #React #Vue转React #VuReact #前端迁移 #useAttrs #组件封装 #TypeScript

vue3中静态提升和patchflag实现

1. 更快的 Virtual DOM (VDOM) - 具体体现

Vue 3 在虚拟 DOM 方面的改进是多方面的,旨在提高渲染效率和减少不必要的计算。

A. 编译时优化 (Compile-time Optimizations)

这是 Vue 3 与 Vue 2 最大的区别之一。Vue 2 的 VDOM diff 过程是在运行时进行的,它需要逐个比较节点和属性。而 Vue 3 的编译器在构建阶段就能分析模板,生成包含“优化提示”的渲染函数。

  • 静态提升 (Hoisting/Diff Skipping): 编译器会识别出模板中的静态节点(即内容不会改变的节点),并将它们提取到渲染函数之外。在后续更新时,Vue 完全跳过对这些节点的比较,因为它们永远不会变。
<!-- 模板 -->
<div>
  <h1>This is static</h1> <!-- 静态节点 -->
  <p>{{ dynamicValue }}</p> <!-- 动态节点 -->
  <span>Another static content</span> <!-- 静态节点 -->
</div>

在 Vue 2 中,每次更新 dynamicValue 时,都会对整个 <div> 的所有子节点进行 diff。在 Vue 3 中,<h1><span> 会被提升,只对 <p> 进行比较,大大减少了工作量。

  • Block Tree (块树): Vue 3 会将动态节点组织成一棵“块树”。更新时,只需要遍历这棵更小的动态节点树,而不是整个 VDOM 树。
  • Patch Flags (补丁标志): 编译器会给动态节点打上标记(flag),标明该节点哪些部分可能会变化(如文本、class、props、事件监听器等)。在 diff 阶段,Vue 可以根据这些标志跳过不必要的比较,直接执行特定的更新操作。
<!-- 模板 -->
<p :class="className">{{ message }}</p>

编译器会知道这个 <p> 元素可能变化的部分是 class 和文本内容,并打上相应的 flag。更新时就不会去检查它的 id 或其他不变的属性。

B. 更高效的 Diff 算法

虽然核心思想仍是双端 Diff,但 Vue 3 的实现更加优化,尤其是在处理列表更新时。

  • 快速路径 (Fast Paths) for List Updates: 对于一些常见的列表更新模式(如在末尾添加元素、替换整个列表等),Vue 3 提供了专门的快速路径算法,避免了复杂的最长递增子序列计算。
  • 更精确的移动策略: 在处理列表项顺序改变时,Vue 3 的算法能更精确地判断哪些元素需要移动,哪些可以就地复用,从而减少 DOM 操作次数。

总结 VDOM 性能提升体现:

  • 更快的初始渲染: 静态节点提升和块树优化减少了首次渲染的计算量。
  • 更快的状态更新: Patch flags 和优化的 Diff 算法减少了状态变更时的比较和更新开销。
  • 更少的内存占用: Block tree 结构和静态提升减少了运行时需要跟踪的节点数量。

2.静态提升和pathflag例子

<template>
  <div id="app">
    <h1 class="title">Welcome to My App</h1>
    <p>{{ greeting }}</p>
    <ul>
      <li>Static Item 1</li>
      <li>Static Item 2</li>
      <li>{{ dynamicItem }}</li> <!-- 这一项是动态的 -->
    </ul>
    <button @click="changeGreeting">Change Greeting</button>
  </div>
</template>

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

const greeting = ref('Hello Vue 3!');
const dynamicItem = ref('Dynamic Item 3');

const changeGreeting = () => {
  greeting.value = 'Greetings from Vue 3!';
};
</script>

编译器分析和优化过程:

  1. 识别静态节点:
    • <h1>Welcome to My App</h1>:标签名、内容、class 属性都不变,是静态节点
    • <li>Static Item 1</li><li>Static Item 2</li>:标签名和内容都不变,是静态节点
    • <button>:标签名、内容和事件处理器(@click)都不变,是静态节点
  1. 识别动态节点:
    • <p>{{ greeting }}</p>:内容 {{ greeting }} 是动态的。
    • <li>{{ dynamicItem }}</li>:内容 {{ dynamicItem }} 是动态的。
    • <div id="app">:虽然 id 是静态的,但它包含了动态子节点,因此自身是动态的。
  1. 执行静态提升:
    • 编译器会将上面识别出的静态节点的 VNode 对象创建代码提取出来,放在渲染函数外面,通常赋值给一个变量(比如 _hoisted_1, _hoisted_2 等)。这样它们只会被创建一次。
  1. 添加 Patch Flags:
    • <p> 节点:它的内容是动态的,编译器会为其 VNode 添加 patchFlag: Text (数值通常是 1)。这告诉运行时,只需要比较和更新它的文本内容。
    • <li>{{ dynamicItem }}</li> 节点:它的内容是动态的,同样会添加 patchFlag: Text (数值通常是 1)。
    • <ul> 节点:它的子节点列表是动态的(因为包含动态的 <li>),编译器会为其添加 patchFlag: Children (数值通常是 8 或更复杂的组合)。这告诉运行时,需要对其子节点进行 diff。

编译后生成的渲染函数(简化示意):

import { createElementVNode as _createElementVNode, createTextVNode as _createTextVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock, toDisplayString as _toDisplayString } from 'vue'

// --- 静态提升的 VNodes ---
// 这些 VNodes 只在模块加载时创建一次,后续渲染直接复用
const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", { class: "title" }, "Welcome to My App", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createTextVNode("Static Item 1")
const _hoisted_3 = /*#__PURE__*/_createTextVNode("Static Item 2")
const _hoisted_4 = /*#__PURE__*/_createElementVNode("button", { onClick: "changeGreeting" }, "Change Greeting", -1 /* HOISTED */)
// -----------------------------------

function render(_ctx, _cache, $props, $setup, $data, $options) {
  // `_ctx` 通常包含 `greeting` 和 `dynamicItem` 等响应式数据
  return (_openBlock(), _createBlock("div", { id: "app" },
    [
      _hoisted_1, // 直接复用,无需 diff
      _createElementVNode("p", null, _toDisplayString($setup.greeting), 1 /* TEXT */), // patchFlag: 1
      _createElementVNode("ul", null, [
        _hoisted_2, // 直接复用,无需 diff
        _hoisted_3, // 直接复用,无需 diff
        _createElementVNode("li", null, _toDisplayString($setup.dynamicItem), 1 /* TEXT */) // patchFlag: 1
      ], 16 /* FULL_PROPS */), // patchFlag: 16 (这里可能表示子节点是动态的,需要 diff)
      _hoisted_4  // 直接复用,无需 diff
    ]
  ))
}

关键点解读:

  • _hoisted_1, _hoisted_2, _hoisted_3, _hoisted_4:这些都是在编译时创建好的静态 VNode 对象。/* HOISTED */ 注释表明它们被提升了。在运行时,渲染函数直接使用这些对象,而不必每次都重新创建。
  • _createElementVNode("p", ...)_createElementVNode("li", ...):这些是动态节点,每次渲染时都需要重新创建 VNode。
  • 1 /* TEXT */:这就是 patchFlag。它告诉运行时,这个 VNode 只需要关心文本内容的变化。当 $setup.greeting$setup.dynamicItem 改变时,运行时只需比较新旧文本字符串,然后更新真实 DOM 的 textContent,而不需要比较 classid 等其他属性。
  • 16 /* FULL_PROPS */ (或类似的数值):<ul>patchFlag 表明其子节点是动态的,需要进行子节点的 diff。

通过这种方式,Vue 3 在编译时就对模板进行了深度优化,使得运行时的渲染和更新过程更加高效

前端视频媒体带声音自动播放方案最佳实践和教程

在前端开发过程中,经常会碰到这样的需求,自动播放视频,要求默认带声音。在浏览器环境下,视频媒体自动播放是可以的,默认静音的自动播放可以正常执行。浏览器不能自动播放的限制,仅针对带声音的自动播放。当网页无用户交互、媒体参与度不足时,带声音的自动播放会被浏览器拦截。

本文结合「用户交互触发」「媒体参与度优化」「跨域权限下放」三大核心场景,提供可落地的实现方案,附代码片段与关键细节说明,明确区分静音与带声音自动播放的实现差异。看完如有帮助,谢谢三连~

1. 核心前提:浏览器自动播放策略

浏览器对自动播放的限制核心的是「避免声音突然打扰用户」,具体规则如下:

  1. 「静音自动播放」:默认允许,无需用户交互、无需满足媒体参与度阈值,可直接实现;
  2. 「带声音自动播放」:受严格限制,需满足以下任一条件才可正常执行:
  3. 用户有强交互(如点击、Tab切换、滑动等),且交互触发在当前域名下;
  4. 媒体参与度(Media Engagement)达标(不同浏览器阈值不同,阈值达标后浏览器会放宽限制);
  5. 父元素(如顶级页面)已获得带声音播放权限,可下放给子iframe(同域名/配置跨域权限后)。

1.1. 什么是媒体参与度?

媒体参与度

媒体参与度(Media Engagement)是浏览器(以Chrome为首)内置的一种用户行为评分机制,核心是通过监测用户对当前域名的媒体交互行为,评估该网站的信任度,进而决定是否放宽带声音自动播放的限制,本质是浏览器给予域名的「信任积分」。

其判定依据主要包括:用户在当前域名播放过音频/视频、进行过点击/滑动/输入等交互操作、停留时间较长或浏览多个页面等;得分越高,浏览器对该域名的信任度越高,越容易允许带声音自动播放。

查看 Chrome 浏览器媒体参与度:直接访问 chrome://media-engagement,可查看当前域名的参与度得分及阈值。其中,Score 为当前得分(0-100分),Threshold 为准入阈值(通常20-30分,因浏览器版本和设备而异),Engaged 为 true 时,说明得分达标,浏览器授予带声音自动播放权限。

2. 基础实现:静音自动播放

该方式无需交互,直接可用。

2.2. 实现逻辑

无需用户前置交互,直接创建媒体元素并设置 muted="true",即可实现静音自动播放; 若需切换为带声音播放,需通过用户交互触发(如点击按钮取消静音)。

2.3. 案例

<!-- 页面结构:静音自动播放 + 手动取消静音按钮 -->
<div class="media-container">
  <video 
    id="videoPlayer" 
    muted="true"  <!-- 关键设置静音实现自动播放 -->
    autoplay       <!-- 自动播放属性 -->
    loop           <!-- 可选:循环播放 -->
    style="width: 100%;"
  >
    <source src="your-video-url.mp4" type="video/mp4">
  </video>
  <button id="unmuteBtn" style="margin-top: 10px; padding: 8px 16px;">
    点击开启声音
  </button>
</div>

<script>
  const video = document.getElementById('videoPlayer');
  const unmuteBtn = document.getElementById('unmuteBtn');

  // 静音自动播放无需额外触发,浏览器默认允许
  console.log('静音自动播放已执行');

  // 用户交互触发:取消静音(带声音播放)
  unmuteBtn.addEventListener('click', async () => {
    try {
      // 取消静音并尝试带声音播放(需用户交互触发,否则会报错)
      video.muted = false;
      await video.play();
      console.log('带声音播放成功');
    } catch (error) {
      console.log('带声音播放失败(未满足权限条件):', error);
      // 失败后恢复静音,避免影响自动播放
      video.muted = true;
    }
  });
</script>

2.4. 关键细节

  • 只要设置 muted="true"autoplay 属性可直接生效,无需用户交互;
  • 带声音播放必须通过用户交互触发(如点击按钮),否则即使取消静音,play() 也会被拦截;
  • 建议搭配 try/catch 包裹带声音播放逻辑,避免报错影响页面正常运行。

3. 带声音自动播放

若需实现「无需用户每次交互,即可带声音自动播放」,核心是提升当前域名的媒体参与度:

  1. 设计引导页,让用户完成高频交互(如点击、滑动、按键);
  2. 引导页中播放静音媒体,持续积累媒体参与度;
  3. 参与度达标后,跳转至目标页面,此时浏览器会默认允许带声音自动播放。

这里的引导页,实际上可以是任意页面,设计得让用户无感,只要发生交互即可。

示例代码:

<!-- 引导页:用于提升媒体参与度,为带声音自动播放铺路 -->
<div class="guide-page" style="text-align: center; padding: 50px 0;">
  <h3>点击任意区域进入播放页</h3>
  <p id="guideTip" style="margin: 20px 0; color: #666;">当前媒体参与度:<span id="engagementScore">0</span>(达标即可带声音自动播放)</p>
</div>

<script>
  // 创建静音音频(用于积累参与度,不干扰用户)
  const engagementAudio = new Audio('silent-audio.mp3');
  engagementAudio.loop = true;
  engagementAudio.muted = true; // 静音播放,避免打扰

  // 监听用户交互,触发参与度提升
  document.querySelector('.guide-page').addEventListener('click', async () => {
    // 首次点击触发静音播放,开始积累参与度
    await engagementAudio.play();
    // 模拟参与度更新(实际可通过 chrome://media-engagement 查看真实值)
    updateEngagementScore();
    // 假设参与度达标(模拟值≥80),跳转至目标页面
    const currentScore = Number(document.getElementById('engagementScore').textContent);
    if (currentScore >= 80) {
      setTimeout(() => {
        window.location.href = 'autoplay-page.html'; // 目标带声音自动播放页面
      }, 1000);
    }
  });

  // 模拟媒体参与度积累
  function updateEngagementScore() {
    const scoreSpan = document.getElementById('engagementScore');
    let currentScore = Number(scoreSpan.textContent) + 20;
    scoreSpan.textContent = Math.min(currentScore, 100); // 参与度上限100
  }
</script>

3.1. 媒体参与度提升流程图

插图1.png

4. 跨域 iframe 自动播放

  • 静音自动播放:iframe 可直接实现,无需父页面权限;
  • 带声音自动播放:需父页面已获得带声音播放权限(通过用户交互/参与度达标),并将权限下放给 iframe(同域名/配置跨域权限)。

4.1. 案例

示例代码:

  • 父页面(同域名,已获带声音播放权限)
<!-- 父页面:通过用户交互获得带声音播放权限 -->
<button id="parentPlayBtn" style="padding: 8px 16px; margin: 20px 0;">点击开启带声音播放(授权)</button>
<iframe id="mediaIframe" src="iframe-page.html" width="800" height="450"></iframe>

<script>
  const parentPlayBtn = document.getElementById('parentPlayBtn');
  const iframe = document.getElementById('mediaIframe');
  let hasAudioPermission = false;

  // 父页面用户交互,获得带声音播放权限
  parentPlayBtn.addEventListener('click', async () => {
    const testAudio = new Audio('test-audio.mp3');
    try {
      await testAudio.play();
      testAudio.pause();
      hasAudioPermission = true;
      console.log('父页面已获得带声音播放权限');
      // 向iframe发送权限下放通知
      iframe.contentWindow.postMessage('autoplay-allowed', '*');
    } catch (error) {
      console.log('父页面带声音播放授权失败:', error);
    }
  });
</script>
  • iframe 页面
<!-- 子iframe:根据父页面权限,实现对应自动播放 -->
<video id="iframeVideo" muted="true" autoplay loop style="width: 100%;">
  <source src="iframe-video-url.mp4" type="video/mp4">
</video>

<script>
  const video = document.getElementById('iframeVideo');

  // 监听父页面权限通知,切换为带声音播放
  window.addEventListener('message', async (e) => {
    if (e.data === 'autoplay-allowed') {
      try {
        // 父页面已授权,尝试带声音播放
        video.muted = false;
        await video.play();
        console.log('iframe 带声音自动播放成功');
      } catch (error) {
        console.log('iframe 带声音播放失败:', error);
        video.muted = true; // 失败后恢复静音自动播放
      }
    }
  });
</script>

4.2. 关键细节

  • 跨域场景下,需在父页面响应头配置 Permissions-Policy: autoplay=(self "https://子域名.example.com"),允许权限下放;
  • 即使父页面授权,iframe 带声音播放仍建议用 try/catch 处理异常,避免权限失效导致播放失败;
  • 若父页面未授权,iframe 仍可正常实现静音自动播放,不影响基础体验。

5. 进阶方案:智能检测播放能力

封装音频/视频播放类,自动检测浏览器是否允许带声音播放:能播放则自动开启声音,不能则默认静音播放,无需手动判断,适配所有场景。

5.1. 案例

// 封装智能媒体播放类(适配音频/视频,自动区分静音/带声音)
class SmartMediaPlayer {
  constructor(mediaUrl, isVideo = false) {
    // 创建媒体元素(音频/视频)
    this.media = isVideo ? document.createElement('video') : document.createElement('audio');
    this.media.src = mediaUrl;
    this.media.loop = true; // 可选:循环播放
    this.canPlayWithAudio = false; // 标记是否可带声音播放
  }

  // 初始化检测:自动判断播放能力
  async init() {
    try {
      // 尝试带声音播放(无用户交互时,此处会报错)
      await this.media.play();
      this.canPlayWithAudio = true;
      console.log('可带声音自动播放');
    } catch (error) {
      // 带声音播放失败,切换为静音自动播放(默认允许)
      this.media.muted = true;
      await this.media.play();
      this.canPlayWithAudio = false;
      console.log('带声音播放受限,已切换为静音自动播放');
    }
  }

  // 手动切换声音(需用户交互触发)
  toggleAudio() {
    if (!this.canPlayWithAudio) return; // 未获得带声音权限,不执行
    this.media.muted = !this.media.muted;
  }

  // 播放/暂停控制
  togglePlay() {
    this.media.paused ? this.media.play() : this.media.pause();
  }
}

// 调用示例(音频)
(async () => {
  const audioPlayer = new SmartMediaPlayer('background-music.mp3');
  await audioPlayer.init();
  // 页面加载完成后自动播放(静音/带声音自动适配)
  audioPlayer.togglePlay();
})();

// 调用示例(视频)
(async () => {
  const videoPlayer = new SmartMediaPlayer('demo-video.mp4', true);
  await videoPlayer.init();
  // 追加到页面
  document.body.appendChild(videoPlayer.media);
})();

5.2. 智能播放能力检测流程图

插图2.png

6. 参考资料与注意事项

6.1. 官方参考

6.2. 开发注意事项

  • 静音自动播放虽无需交互,但建议搭配加载状态提示,避免用户误以为媒体未加载;
  • 带声音自动播放的核心是「用户交互」或「媒体参与度」,二者缺一不可,不可强行绕过浏览器限制;
  • 移动端浏览器对带声音自动播放的限制更严格,即使参与度达标,部分机型仍需用户交互触发;
  • 媒体文件建议压缩优化,避免加载延迟导致自动播放触发时机滞后,影响用户体验;
  • 可通过 chrome://media-engagement 调试当前域名的参与度,适配不同浏览器的阈值差异。

以上,在实际开发中,可根据业务需求(是否需要声音),组合使用以上方案,既满足自动播放需求,又符合浏览器权限策略,兼顾用户体验与开发合规性。

本次分享就到这儿啦,我是鹏多多,深耕前端的技术创作者,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~

PS:在本页按F12,在console中输入document.getElementsByClassName('panel-btn')[0].click();有惊喜哦~

往期文章

4.响应式系统基础:从发布订阅模式的角度理解 Vue3 的数据响应式原理

前言

我们从前面的文章中知道的所谓发布订阅模式的本质是不管代码结构如何变化,它的核心都是管理对象间的依赖关系,或者说是事件间的依赖关系,一方变化了,所有跟其建立依赖关系的依赖都将得到通知。同时发布者对象既可以是发布者也可以是订阅者,所以我们不能只从代码组织结构去分辨模式,而是从意图去分辨。

Vue2 的数据响应式的实现,在代码结构层面多少是看得出有经典发布订阅模式的架构影子,所以社区里也有人从发布订阅模式角度去分析过,但 Vue3 的数据响应式的实现从代码结构上来看跟所谓标准的发布订阅模式的代码架构差别是很大的。一般社区作者也不从发布订阅模式的角度去分析它的实现原理,那么今天就让我们从发布订阅模式的角度去理解 Vue3 的数据响应式原理吧。

发布订阅模式原理回顾

我们经过前面的学习,我们很容易通过发布订阅模式初步实现 Vue3 的 reactive API,代码如下:

class Dep {
  constructor() {
    // 订阅者存储中心
    this.subs = []
  }
  // 添加订阅者
  addSub(sub) {
    this.subs.push(sub)
  }
  // 通知订阅者
  notify() {
    this.subs.forEach(sub => sub())
  }
}
const dep = new Dep()
let activeEffect
// reactive
function reactive(data) {
    return new Proxy(data, {
        get(target, key) {
            // 存在依赖就把依赖收集到依赖存储中心
            activeEffect && dep.addSub(activeEffect)
            return Reflect.get(target, key) 
        },
        set(target, key, val) {
            const result = Reflect.set(target, key, val)
            // 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
            dep.notify()
            return result
        }
    })
}

我们就可以进行以下测试了:

const proxy = reactive({ author: 'Cobyte' })

// 订阅者
const subscriber = () => {
    console.log(`我是:${proxy.author}`)
}

activeEffect = subscriber
subscriber()
activeEffect = null

// 修改
proxy.author = 'coboy'

根据上一篇 Vue2 的数据响应式原理的实践,我们可以做小小的优化:

class Dep {
  // 省略...
-  addSub(sub) {
+  addSub() {
    if (activeEffect) {
-        this.subs.push(sub)
+        this.subs.push(activeEffect)
    }
  }
  // 省略...
}
// 省略...
function reactive(data) {
    return new Proxy(data, {
        get(target, key) {
            // 存在依赖就把依赖收集到依赖存储中心
-            activeEffect && dep.addSub(activeEffect)
+            dep.addSub()
            return Reflect.get(target, key) 
        },
        // 省略... 
    })
}

我们上面 reactive 的实现,每个订阅者还不能进行跟每个对象的属性进行隔离的。什么意思呢?看以下测试代码:

const proxy = reactive({ author: 'Cobyte', date: '2024-03-05' })

// 订阅者
const subscriber = () => {
    console.log(`我是:${proxy.author}`)
}
// 订阅者2
const subscriber2 = () => {
    console.log(`日期是:${proxy.date}`)
}
activeEffect = subscriber
subscriber()
activeEffect = subscriber2
subscriber2()
activeEffect = null

// 修改
proxy.author = 'coboy'

测试结果如下:

D01.png

我们可以看到最后修改 author 属性值的时候,两个订阅者函数都执行了。是因为我们在 getter 进行订阅的时候,把不同属性的订阅者都存储在同一个全局变量中了,而在 Vue2 中把每一个属性的消息代理都通过闭包进行了隔离,也就是每一个属性都拥有属于自己的消息代理,相当于每一个属性都是一个发布者。

而 Vue3 中的 Proxy API 很明显不能通过闭包来进行隔离每个属性的消息代理。那么我们根据前面的发布订阅模式的实践理解,还可以通过给消息代理对象通过添加 key 的方式来让订阅者只订阅自己感兴趣的内容。

那么相关代码修改如下:

class Dep {
  constructor() {
    // 订阅者存储中心
-    this.subs = []
+    this.subs = {}
  }
  // 添加订阅者
-  addSub() {
+  addSub(key) {
+    if (!this.subs[key]) {
+        this.subs[key] = []
+    }
    if (activeEffect) {
-    this.subs.push(sub)
+    this.subs[key].push(activeEffect)
    }
  }
  // 通知订阅者
-  notify() {
+  notify(key)
-    this.subs.forEach(sub => sub())
+    this.subs[key].forEach(sub => sub())
  }
}
const dep = new Dep()
let activeEffect
// reactive
function reactive(data) {
    return new Proxy(data, {
        get(target, key) {
            // 存在依赖就把依赖收集到依赖存储中心
-            dep.addSub()
+            dep.addSub(key)
            return Reflect.get(target, key) 
        },
        set(target, key, val) { 
            const result = Reflect.set(target, key, val)
            // 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
-            dep.notify()
+            dep.notify(key)
            return result
        }
    })
}

我们经过上面的修改再进行测试,我们发现已经可以正确打印我们期待的结果了。

D02.png

我们上面实现的 reactive 函数还存在一个问题,我们现在可以通过 key 来把不同订阅者进行分类,但不同的对象中可能会存在相同的 key,例子如下:

const proxy = reactive({ author: 'Cobyte', date: '2024-03-05' })

const proxy2 = reactive({ author: 'Cobyte2' })
// 订阅者
const subscriber = () => {
    console.log(`我是:${proxy.author}`)
}
// 订阅者2
const subscriber2 = () => {
    console.log(`我是:${proxy2.author}`)
}
activeEffect = subscriber
subscriber()
activeEffect = subscriber2
subscriber2()
activeEffect = null

// 修改
proxy.author = 'coboy'

测试结果如下:

D03.png

我们发现我们只修改了 proxy.author 的值,但订阅者2 subscriber2 也执行了,这不是我们期待的结果,所以我们还要迭代我们的功能。

我们既然可以添加 key 来让订阅者订阅自己喜欢的内容,那么是否还可以进行增加 key, 来区分不同的对象呢?我们把对象也当成一个 key,也就是在 getter 添加依赖的时候这样操作:dep.addSub(target, key, activeEffect),那么在 setter 的时候这样操作:dep.notify(target, key)。很明显我们可以通过 Map 来把一个对象作为一个 key。

所以我们对消息代理中心做以下修改:

class Dep {
  constructor() {
    // 订阅者存储中心
-    this.subs = {}
+    this.subs = new Map()
  }
  // 添加订阅者
-  addSub(key) {
+  addSub(target, key) {
+    let depsMap = this.subs.get(target)
+    if (!depsMap) {
+        depsMap = {}
+        this.subs.set(target, depsMap)
+    }
    
-    if (!this.subs[key]) {
-      this.subs[key] = []
-    }
+    if (!depsMap[key]) {
+        depsMap[key] = []
+    }
     if (activeEffect) {
-    this.subs[key].push(activeEffect)
+    depsMap[key].push(activeEffect)
     }
  }
  // 通知订阅者
-  notify(key) {
+  notify(target, key) {
-    this.subs[key].forEach(sub => sub())
+    const depsMap = this.subs.get(target)
+    if (!depsMap) return
+    const deps = depsMap[key] 
+    deps && deps.forEach(sub => sub())
  }
}

接着我们也去修改 reactive 中相关的地方:

function reactive(data) {
    return new Proxy(data, {
        get(target, key) {
            // 存在依赖就把依赖收集到依赖存储中心
-            dep.addSub(key)
+            dep.addSub(target, key)
            return Reflect.get(target, key) 
        },
        set(target, key, val) {
            const result = Reflect.set(target, key, val)
            // 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
-            dep.notify(key)
+            dep.notify(target, key)
            return result
        }
    })
}

我们重新测试,我们发现打印了如期的结果:

D04.png

桶的数据结构设计?

我们看到通过发布订阅模式去理解 Vue3 的数据响应式原理,理解起所谓依赖数据结构 ,是非常好理解的。我们通过由浅入深的地讲解所谓 数据结构的形成,它的形成是自然而然的形成的,而不是一开始就经过特别精心设计的,它没有那么的神秘,它是由最简单的功能一步步迭代形成的,是非常符合我们的日常开发规律的,因为我们日常的应用也是由最简单的功能开始慢慢迭代成非常复杂的功能。一开始所谓 ,只是一个 Array (可以简单理解为:[]) 的结构,后来我们通过增加 key 来区分不同的订阅者,这行为在发布订阅模式中就是通过 key 来让订阅者只订阅自己感兴趣的内容;增加 key 后, 的数据结构变为 Object -> Array (可以简单理解为:{ key: [] }),再后来我们继续增加响应式对象作为 key,来区分不同的属性,避免不同响应式数据中可能存在相同属性的情况。最后我们的 的数据结构变为 Map -> Object -> Array (可以简单理解为:{ target: { key: [] } })。

我们熟悉 Vue3 源码的同学会知道,所谓 的数据结构跟我们上面还是区别的,那其实都是性能优化迭代的结果,我们也可以继续迭代我们的功能。首先是我们的订阅者是通过 Array 的方式存储的,为了防止重复添加订阅者,我们需要在执行完订阅者函数之后把 activeEffect 变量设置为 null,同时也是为了确保只在副作用函数中读取响应式变量才进行依赖收集。我们可以把订阅者的存储方法改成 Set 的数据结构,因为 Set 具有自动去除重复的功能。

相关代码修改如下:

class Dep {
  // 省略...
  addSub(target, key, sub) {
    // 省略...
    if (!depsMap[key]) {
-        depsMap[key] = []
+        depsMap[key] = new Set()
    }
    if (activeEffect) {
-    depsMap[key].push(activeEffect)
+    depsMap[key].add(activeEffect)
    }
  }
  // 省略...
}

经过上面修改,我们的 结构变成了 Map -> Object -> Set。我们还可以继续优化,我们可以把中间的 Object 改成 Map,因为在频繁增删键值对和存储大量数据的场景下 Map 的性能要比 Ojbect 更好。

class Dep {
  // 省略...
  addSub(target, key, sub) {
    let depsMap = this.subs.get(target)
    if (!depsMap) {
-        depsMap = {}
        depsMap = new Map()
        this.subs.set(target, depsMap)
    }
+    let dep = depsMap.get(key)
-    if (!depsMap[key]) {
+    if (!dep) {
-        depsMap[key] = new Set()
+        dep = new Set()
+        depsMap.set(key, dep)
+    }
    if (activeEffect) {
-    depsMap[key].add(activeEffect)
+    dep.add(activeEffect)
    }
  }
  // 通知订阅者
  notify(target, key) {
    const depsMap = this.subs.get(target)
    if (!depsMap) return
-    const deps = depsMap[key] 
+    const deps = depsMap.get(key) 
    deps && deps.forEach(sub => sub())
  }
}

最后我们还可以继续优化的地方就是把存储订阅者的变量 this.subsMap 类型改成 WeakMap 类型。

class Dep {
  constructor() {
    // 订阅者存储中心
-    this.subs = new Map()
+    this.subs = new WeakMap()
  }
}

为什么不采用 WeakMap 而不采用 Map 呢?我们通过下面的一个例子来说明:

const map = new Map()
const weakMap = new WeakMap()

function test() {
    const mapObj = { test: 'mapObj' }
    const weakMapObj = { test: 'weakMapObj' }
    map.set(mapObj, true)
    weakMap.set(weakMapObj, true)
}

test()

console.log('map', map)
console.log('weakMap', weakMap)

我们从打印的结果中可以一目了然地看出两者的区别,WeakMap 对 key 是弱引用的,所谓弱引用就是一旦上下文执行完毕,WeakMap 中 key 对象没有被其他代码引用的时候,垃圾回收器就会把该对象从内存移除。Map 则不会把 key 对象进行移除,这样就会容易导致内存溢出,就算不内存溢出,当数据大的时候,操作性能也会下降,所以 Vue3 源码中就采用了 WeakMap。

最后小结 Vue3 底层源码是使用 WeakMap 和 Map 来构建依赖关系图,具体来说是:

  • targetMap 是一个WeakMap,键是响应式对象(target),值是一个Map(depsMap)。
  • depsMap 的键是对象的属性(key),值是一个Dep(即一个Set),存储了所有依赖该属性的副作用函数。

订阅者中介的实现

我们通过前面的文章对发布订阅模式的学习,可以知道发布者可以抽离一些公共功能统一放到一个中介类中,也就是所谓的事件总线或者消息代理,而订阅者同样也可以进行中介化,从而实现订阅者的多态化。所谓多态就是当不同的对象去执行同一个方法时会产生出不同的状态。我们通过上一篇文章可以知道 Vue2 中所谓的 Watcher 类其实就是订阅者中介,在项目中不同的组件其实底层都是通过 Watcher 类来执行的,而所谓依赖收集,其中收集的是 Watcher,那些响应式数据发生变化后去通知的也是 Watcher,然后再通过 Watcher 去执行具体的组件渲染。

那么 Vue3 的数据响应式也是通过发布订阅模式实现的,那么很自然的也存在订阅者中介。在 Vue3 源码中 ReactiveEffect 类从发布订阅模式的角度理解就是订阅者中介的角色,所以从发布订阅模式的角度理解 Vue3 的数据响应式原理,就非常容易理解为什么要有一个 ReactiveEffect 类了,甚至不用去看具体的实现细节,我们都可以知道 ReactiveEffect 所实现的功能是什么了。

我们知道 Vue2 中的 Watcher 有一个 update 方法,就是在发布者去通知所有订阅者的时候,订阅者统一执行的方法就是 update,那么很明显 ReactiveEffect 也同样需要这样的一个方法,在 Vue3 源码中这个方法叫 run,同样初始化的时候需要接收一个函数作为参数也就是具体订阅者需要做的事情。

ReactiveEffect 的初步实现:

class ReactiveEffect {
    constructor(fn) {
        this._fn = fn
    }
    run () {
        // 根据 Vue2 的数据响应式原理,我们知道在执行具体订阅者函数之前需要把当前订阅者赋值给一个中间变量。
        activeEffect = this
        this._fn()
        // 确保只在副作用函数中读取响应式变量才进行依赖收集
        activeEffect = null
    }
}

然后我们进行测试:

const proxy = reactive({ author: 'Cobyte', date: '2024-03-05' })
const _effect = new ReactiveEffect(() => {
    console.log(`我是:${proxy.author}`)
})
_effect.run()
proxy.author = 'coboy' 

我们可以看到正确打印了结果:

D06.png

同时我们发现 ReactiveEffect 的订阅者函数参数初始化在外部手动执行的,而 Vue2 的 Watcher 中的订阅者函数初始化在 Watcher 内部实例化的时候自动执行的,这个只是设计上区别。

我们把上述实现订阅的过程进行封装一下,那么就是 effect API 了,代码如下:

function effect(fn) {
    const _effect = new ReactiveEffect(fn)
    _effect.run()
}

从发布订阅模式的角度来看本质上 Vue3 的数据响应式实现原理跟 Vue2 的数据响应式原理的实现是一脉相承的。

互为订阅者

我们通过前面文章的学习,我们知道在 Vue2 中会存在发布者中介类 Dep 和订阅者类 Watcher 互为订阅者的情况,场景就是可能会取消某一个副作用函数的中的响应式数据的追踪,比如组件卸载了,那么我们就需要停止组件的依赖追踪。在 Vue3 中自然也存在这种场景,那么也就说在 Vue3 中也存在互为订阅者的情况。但在 Vue3 中的情况又会跟 Vue2 不一样,Vue2 是订阅者 Watcher 类直接订阅发布者中介类 Dep,因为在 Vue2 中每一个 Dep 实例都和一个发布者关联,也就是和每一个属性或者对象进行关联。而在 Vue3 中因为是通过 Proxy API 实现的数据响应式,每一个 Dep 的实例并不对应着具体的属性,所以我们要找到对应具体的属性的记录的变量,其实就是对应 key 的记录变量。

我们再看看 Dep 中关于对应 key 部分的订阅者记录变量部分代码:

class Dep {
  // 省略...
  addSub(target, key) {
    let depsMap = this.subs.get(target)
    if (!depsMap) {
        depsMap = new Map()
        this.subs.set(target, depsMap)
    } 
    let dep = depsMap.get(key)
    if (!dep) {
      dep = new Set()
      depsMap.set(key, deps)
    }
    if (activeEffect) {
      dep.add(activeEffect)
    }
  }
  // 省略...
}

我们可以看到对应每一个 key 的订阅者记录变量是 deps,所以我们只需要把对应的 deps 记录到 ReactiveEffect 中即可。

首先我们修改 ReactiveEffect 类,添加记录变量 deps

class ReactiveEffect {
+    // 记录哪些变量记录了该订阅者,在 Vue2 中则是记录哪些 Dep 记录了该 Watcher
+    deps = []
    // 省略...
}

接着我们在记录响应式数据对象的 key 的消息代理对象的地方把对应的 key 的消息代理对象添加到订阅者 ReactiveEffectdeps 变量中,代码如下:

class Dep {
  // 省略...
  addSub(target, key) {
    // 省略...
    if (activeEffect) {
      deps.add(activeEffect)
+      activeEffect.deps.push(deps)
    }
  }
  // 省略...
}

这样我们就完成了对应 key 的变量对 ReactiveEffect 的订阅,那么有订阅,也就有取消订阅。

取消订阅功能如下:

class ReactiveEffect {
    // 省略...
    // 取消订阅
+    stop () {
+      this.deps.forEach(dep => dep.delete(this))
+    }
}

接着我们再修改 effect API:

function effect(fn) {
    const _effect = new ReactiveEffect(fn)
    _effect.run()
+    return _effect
}

这样我们就可以进行以下测试了:

const proxy = reactive({ author: 'Cobyte', date: '2024-03-05' })

const _effect = effect(() => {
  console.log(`我是:${proxy.author}`)
})
proxy.author = 'Coboy'
// 取消订阅,也就是取消依赖追踪
_effect.stop()
proxy.author = '掘金签约作者'

打印结果如下:

D06.png

我们看到取消依赖追踪后,我们再去修改响应式数据,我们之前设置的订阅者函数就不再执行了,也就是得不到通知了。

那么停止依赖追踪之后,我又想它继续进行依赖追踪呢?这样我们就需要把 ReactiveEffect 中的 run 方法也返回出来。

我们继续进行 effect API 的功能迭代,新的修改如下:

function effect(fn) {
    const _effect = new ReactiveEffect(fn)
    _effect.run()
+    const runner = _effect.run.bind(_effect)
+    runner.effect = _effect
+    return runner 
}

这样我们就可以在取消依赖追踪后,还可以在某个时机中又恢复依赖追踪了,测试代码如下:

const proxy = reactive({ author: 'Cobyte', date: '2024-03-05' })

const runner = effect(() => {
  console.log(`我是:${proxy.author}`)
})
proxy.author = 'Coboy'
// 取消订阅,也就是取消依赖追踪
runner.effect.stop()
proxy.author = '掘金签约作者'
// 恢复依赖追踪
runner()
proxy.author = '恢复依赖追踪了'

我们可以看到如期打印了我们期待的结果:

D07.png

为什么 Vue3 的发布订阅模式不采用传统代码结构?

我们上面实现 Vue2 的数据响应式原理是很明显采用了发布订阅模式的,因为我们存在一个发布者中介类 Dep,这个代码结构跟传统教学中的发布订阅模式中的代码结构是很相似的。但实际上 Vue3 源码中是不存在发布者中介类的,也就是跟传统发布订阅模式的代码结构是不相同的,那么是否意味着 Vue3 并没有采用发布订阅模式呢?答案是否定的,正如我们前面文章中所说的那样,判断模式不能从代码结构上进行判断,而应该从代码意图。

class Dep {
  constructor() {
    // 订阅者存储中心
    this.subs = new WeakMap()
  }
  // 添加订阅者
-  addSub(target, key) {
+  track(target, key) {
    let depsMap = this.subs.get(target)
    if (!depsMap) {
        depsMap = new Map()
        this.subs.set(target, depsMap)
    }
    let dep = depsMap.get(key)
    if (!dep) {
      dep = new Set()
      depsMap.set(key, dep)
    }
    if (activeEffect) {
      dep.add(activeEffect)
      activeEffect.deps.push(dep)
    }
  }
  // 通知订阅者
-  notify(target, key) {
+  trigger(target, key) {
    const depsMap = this.subs.get(target)
    if (!depsMap) return
    const deps = depsMap.get(key)
    deps && deps.forEach(effect => effect.run())
  }
}

上面我们经过对方法名称的修改,我们的代码结构从命名上跟 Vue3 源码有些类似了,我们接着把 Dep 类也去掉:

  // 全局订阅者记录变量
  const targetMap = new WeakMap()
  // 添加订阅者
  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)
    }
    if (activeEffect) {
      dep.add(activeEffect)
      activeEffect.deps.push(dep)
    }
  }
  // 通知订阅者
  function trigger(target, key){
    const depsMap = targetMap.get(target)
    if (!depsMap) return
    const deps = depsMap.get(key)
    deps && deps.forEach(effect => effect.run())
  }

接着我们也要把 reactive 中的相关代码也进行修改:

function reactive(data) {
    return new Proxy(data, {
        get(target, key) {
            // 存在依赖就把依赖收集到依赖存储中心
-            dep.addSub(target, key)
+            track(target, key)
            return Reflect.get(target, key) 
        },
        set(target, key, val) {
            const result = Reflect.set(target, key, val)
            // 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
-            dep.notify(target, key)
+            trigger(target, key)
            return result
        }
    })
}

我们可以看到经过上述修改之后,我们的代码结构跟 Vue3 源码是一模一样的了,但并不是说代码结构变了,模式也变了,上述代码结构依然是发布订阅模式。那么 Vue3 为什么要把依赖收集和依赖触发的函数进行分开呢?主要是因为分开之后依赖收集和依赖触发的函数就可以分别独立导出了,给其他功能 API 比如 ref、computed 使用了,代码可以达到最极致的抽象及复用。

确保只在副作用函数中读取响应式变量才进行依赖收集

不采用 Proxy API 实现数据响应式

因为 Proxy 无法提供对原始值的代理,所以我们需要对原始值的响应式进行特别处理,我们可以使用一层对象作为包裹,间接实现原始值的响应式方案。

当我们不通过 Proxy 实现代理的时候,除了使用 Vue2 中使用的 Object.defineProperty以外,我们还可以根据前面总结的实践规律,我们只需要可以实现在数据读取的时候进行依赖收集,然后在数据更改的时候进行依赖触发就可以了。那么明显我们可以使用在发布订阅模式那篇中讲到的公众号的例子。

// 定义发布者公众号
const weChatOfficialAccount = {
    // 订阅公众号的人的记录列表
    subscribers: [],
    // 文章内容
    article: '原始值内容',
    // 发布文章
    setArticle(value) {
        this.article = value
        // 更新文章的时候通知所有的订阅者
        this.notify()
    },
    // 添加订阅者
    addDep(fn) {
        // 把订阅者添加进记录列表
        this.subscribers.push(fn) 
    },
    // 广播信息
    notify(title) {
        // 发布信息时就是把记录列表中的订阅者全部通知一次
        this.subscribers.forEach(fn => fn(title));
    }
}

上述代码就是前面我们实现公众号讲解发布订阅模式的例子。在上述例子中,我们实现了在数据更新的时候触发依赖,也就是 setArticle 函数。那么我们再实现在数据读取的时候进行依赖收集即可,为了现在这个功能,我们把读取 article 属性值的行为也封装成一个函数。

代码如下:

// 定义发布者公众号
const weChatOfficialAccount = {
    // 订阅公众号的人的记录列表
    subscribers: [],
    // 文章内容
    article: '',
+    getArticle() {
+        return this.article
+    },
    // 省略...
}

这样我们就可以通过以下的方式获取文章内容了:

effect(() => {
    console.log(`原始值内容:${weChatOfficialAccount.getArticle()}`)
})
// 更改内容
weChatOfficialAccount.setArticle(520)

同时我们的发布者的通知函数也需要进行修改:

// 定义发布者公众号
const weChatOfficialAccount = {
    // 省略...
    // 广播信息
-    notify(title) {
+    notify() {
        // 发布信息时就是把记录列表中的订阅者全部通知一次
-        this.subscribers.forEach(fn => fn(title))
+        this.subscribers.forEach(dep => dep.run())
    }
}

那么我们就可以在 getArticle 函数中进行依赖收集了:

// 定义发布者公众号
const weChatOfficialAccount = {
    // 订阅公众号的人的记录列表
    subscribers: [],
    // 文章内容
    article: '',
    getArticle() {
+        // 进行依赖收集,也就是进行订阅
+        if (activeEffect) this.addDep(activeEffect)
        return this.article
    },
    // 省略...
}

这样我们的测试结果如下:

D08.png

我们上述方式是通过一个典型的发布订阅模式来实现对一个对象的观察,当这个对象发生改变之后,所有依赖该对象的订阅者都将得到通知。

我们通过一个工厂函数上面的公众号对象进行进行封装,代码如下:

// ref 工厂函数
function ref(value) {
    return {
        // 订阅公众号的人的记录列表
        subscribers: [],
        // 文章内容
        _value: value,
        getArticle() {
            if (activeEffect) this.addDep(activeEffect)
            return this._value
        },
        // 发布文章
        setArticle(value) {
            this._value = value
            // 更新文章的时候通知所有的订阅者
            this.notify()
        },
        // 添加订阅者
        addDep(fn) {
            // 把订阅者添加进记录列表
            this.subscribers.push(fn) 
        },
        // 广播信息
        notify() {
            // 发布信息时就是把记录列表中的订阅者全部通知一次
            this.subscribers.forEach(dep => dep.run());
        }
    }
}

我们可以看到经过上述的代码封装之后,我们实现了对原始值的响应式。那么接下来我们希望通过普通的方式获取和设置对象的值:

const weChatOfficialAccount = ref('初始值')
effect(() => {
    console.log(`原始值内容:${weChatOfficialAccount.article}`)
})
// 更改内容
weChatOfficialAccount.article = 520

通过前面的学习我们知道除了使用 Object.defineProperty 进行显式声明属性访问器之外,还可以通过字面量的方式,本质还是属性访问器

修改如下:

function ref(value) {
    return {
        // 订阅公众号的人的记录列表
        subscribers: [],
        // 文章内容
        _value: value,
-        getArticle() {
+        get article() {
            if (activeEffect) this.addDep(activeEffect)
            return this._value
        },
        // 发布文章
-        setArticle(value) {
+        set article(value) {
            this._value = value
            // 更新文章的时候通知所有的订阅者
            this.notify()
        },
        // 省略...
}

经过上述修改之后,我们就可以通过属性访问器像普通方式那样访问和设置对象的属性值了。

那么为了跟 Vue3 的 ref API 设计一致,我们把 article 属性改成 value

function ref(value) {
    return {
        // 订阅公众号的人的记录列表
        subscribers: [],
        // 文章内容
        _value: value,
-        get article() {
+        get value() {
            if (activeEffect) this.addDep(activeEffect)
            return this._value
        },
        // 发布文章
-        set article(value) {
+        set value(value) {
            this._value = value
            // 更新文章的时候通知所有的订阅者
            this.notify()
        },
        // 省略...
}

那么改了之后我们的 ref 就跟 Vue3 的一样用法了:

const weChatOfficialAccount = ref('初始值')
effect(() => {
    console.log(`原始值内容:${weChatOfficialAccount.value}`)
})
// 更改内容
weChatOfficialAccount.value = 520

接着我们对依赖收集函数 track 和依赖触发函数 trigger 进行修改让我们的代码尽可能地复用。修改如下:

// 添加订阅者
function track(target, key) {
    // 省略...
    if (activeEffect) {
-        dep.add(activeEffect)
-        activeEffect.deps.push(dep)
+        trackEffect(dep)
    }
}
+ function trackEffect(dep) {
+     dep.add(activeEffect)
+     activeEffect.deps.push(dep)
+ }
// 通知订阅者
function trigger(target, key) {
    // 省略...
-    deps && deps.forEach(effect => effect.run())
+    triggerEffect(deps)
}
+ function triggerEffect(deps) {
+     if(deps) {
+         deps.forEach(effect => effect.run());
+     }
}

接着我们进行重构 ref 函数:

function ref(value) {
    return {
        // 订阅公众号的人的记录列表
-        subscribers: [],
+        dep: new Set()
        // 文章内容
        _value: value,
        get value() {
-            if (activeEffect) this.addDep(activeEffect)
+            if (activeEffect) trackEffect(this.dep)
            return this._value
        },
        // 发布文章
        set value(value) {
            this._value = value
            // 更新文章的时候通知所有的订阅者
-            this.notify()
+            triggerEffect(this.dep)
        },
-        // 添加订阅者
-        addDep(fn) {
-            // 把订阅者添加进记录列表
-            this.subscribers.push(fn) 
-        },
-        // 广播信息
-        notify() {
-            // 发布信息时就是把记录列表中的订阅者全部通知一次
-            this.subscribers.forEach(dep => dep.run());
-        }
-    }
}

我们可以看到经过重构之后,我们的 ref 函数就变得比较整洁了,我们 ref 中的部分发布订阅的功能就和前面 reative 的发布订阅已经实现的功能代码进行了复用。

我们通过前面文章的学习,我们知道 Vue3 的 ref 底层是通过 OOP 的方式进行实现的,但本质还是跟我们上面一样的,那么我们也通过 OOP 的方式实现一遍吧。

实现代码如下:

class RefImpl {
    _value
    dep = new Set()
    constructor(value) {
        // 如果传进来的是对象那么最终还是通过 reactive API 实现数据响应式
        this._value = isObject(value) ? reactive(value) : value
    }
    get value() {
       // 存在依赖就把依赖收集到依赖存储中心
       if (activeEffect) trackEffect(this.dep)
       return this._value 
    }
    set value(val) {
        this._value = val
        // 更新文章的时候通知所有的订阅者
        triggerEffect(this.dep)
    }
}

function ref(value) {
    return new RefImpl(value)
}

最终我们的测试结果还是一样的,这里唯一值得注意的是,如果传进来的是对象那么最终还是通过 reactive API 实现数据响应式。

API 的设计技巧及知识的串联

我们上文中实现的数据响应式代码中,有一个函数的名称叫:observe,还有一个类叫:Observer,在 Vue2 源码中也是这么起名的。那么为什么要这么起名称呢?这么起名称有什么特殊的含义吗?

我们上面这个所谓数据响应式的原理,其实是在观察数据的变化,跟我们在 web 开发中观察 DOM 对象的变化的行为是很像的,甚至可以说本质是一样的。

MutationObserver 与 Vue2 数据响应式的联系

我们如果要观察一个 DOM 对象发生改变了就进行某些操作的话,可以通过 MutationObserver API来实现。例子如下:

// 获取 DOM 对象
const targetNode = document.querySelector('#some-id');

// 观察者回调函数
const subscriber = (mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'childList') {
      mutation.addedNodes.forEach((addedNode) => {
        console.log(`添加了子元素:${addedNode.nodeName}`);
        // 执行相应的处理逻辑
      });
      mutation.removedNodes.forEach((removedNode) => {
        console.log(`移除了子元素:${removedNode.nodeName}`);
        // 执行相应的处理逻辑
      });
    }
  });
} 

// 创建一个观察器实例并传入回调函数,当观察到变动时便执行回调函数
const observer = new MutationObserver(subscriber);
// 配置需要观察的选项
const config = {
  childList: true, // 观察子元素是否发生变化
};
// 观察 DOM 对象是否发生变化
observer.observe(targetNode, config);

我们从上面的代码可以看出 MutationObserver 所做的事情,跟我们 Vue2 中对响应式数据的监听是一样的。DOM 对象就是我们 Vue2 中的响应式数据,当它发生变化之后就会去触发回调函数执行,相当于 Vu2 中的响应式数据发生改变后会触发 Watcher 一样。所以 MutationObserver 本质也是一个发布订阅模式,但它使用方式跟我们所谓传统的发布订阅模式是不一样的,但正如我们前面说的理解一种模式不应该从代码组织结构去进行分辨,而是意图。

所以我们从 Vue2 的数据响应式实现原理,就可以联系到 MutationObserver,然后联系它们的相同点,从而加深我们对知识的理解。当然尤雨溪当初给 Vue2 对一个对象实现数据响应式的处理函数和类命名为 observeObserver,是否参考了 MutationObserver 的 API 命名规则我们无从考证,但它们的工作方式值得我们联系,从而加深我们的知识理解。

总结

本文从发布订阅模式的核心思想出发,深入剖析了 Vue3 响应式系统的设计本质。发布订阅模式的关键在于管理对象间的依赖关系——一方变化时,所有依赖方都能得到通知,而非拘泥于特定的代码结构。Vue3 虽然不再像 Vue2 那样拥有显式的 Dep 类,但其底层依然遵循这一模式。

通过逐步迭代,我们自然形成了 Vue3 中著名的“桶”数据结构:最初用一个数组存储订阅者,然后按属性 key 分类,再按响应式对象 target 隔离,最终演变为 WeakMap(target) → Map(key) → Set(effect) 的依赖图。这种结构并非凭空设计,而是功能迭代的自然产物,体现了发布订阅模式在 Proxy 场景下的灵活应用。

Vue3 中的 ReactiveEffect 类扮演了订阅者中介的角色,类似于 Vue2 的 Watcher,负责管理具体副作用函数的执行与依赖追踪。通过 effect 函数封装,我们可以轻松创建响应式副作用,并借助 stop 机制实现取消订阅,这体现了订阅者与发布者之间“互为订阅”的关系。

值得注意的是,Vue3 将依赖收集(track)和依赖触发(trigger)拆分为独立函数,而非保留传统的 Dep 类结构。这一设计变化并非模式的改变,而是为了提升代码复用性,让 refcomputed 等 API 也能共享同一套响应式核心。

此外,对原始值的响应式实现(ref)同样基于发布订阅模式——通过属性访问器(getter/setter)在读取时收集依赖,在修改时触发更新。当 ref 包裹对象时,内部会回退到 reactive 处理,保证了逻辑的一致性。

最后,从 API 命名(如 observe / Observer)到与浏览器原生 MutationObserver 的类比,都能看出响应式系统与观察者模式之间的深刻联系。理解这些设计背后的模式思想,远比记忆具体代码实现更有价值。

上述文章写于:2023 年,由于个人原因今年 2026 年发布。

我是程序员Cobyte,现在已转向研究 AI Agent,欢迎添加 v: icobyte,学习交流 AI Agent 应用开发。

《前端周刊》尤大开源 Vite+ 全家桶,前端工业革命启动;尤大爆料 Void 云服务新产品,Vite 进军全栈开发;ECMA 源码映射规范......

🌐 今日要闻

打破信息壁垒,走近全球前端。Hello World 大家好,我是林语冰。

欢迎阅读《Web 周刊》,上周全球 Web 开发圈的主要情报如下:

  • 🎉 尤大出席 Vue 大会,发表了关于 Vue & Vite 的重要讲话
  • ✨ Vue 生态“文艺复兴“,“蒸汽模式“公测,Pinia Colada 新品首发
  • ✅ TC39 工作组推进 ECMA 源码映射规范
  • 👍 Vite 生态“工业革命“,Vite+ 全家桶免费开源
  • 🙏 尤大爆料 Void 一键部署云平台,Vite 进军全栈开发

PS:本文附带甜妹解说的动画视频,粉丝请搜索哔哩哔哩@Web情报局

🎉 Vue 生态文艺复兴

近日,Vue 之父 & Vite 之父 & VoidZero CEO 尤雨溪出席了 Vue 阿姆斯特丹大会和 D2 技术大会,发表了重要讲话。

cover.png

本期我们就来回顾一下这位“前端之神“已公开的关于 Vite & Vue 生态的最新情报和未来规划。

Vue 3.6 Beta

Vue 是 GitHub 第二 UI 框架,也是唯一一个同时支持 SFC(单文件组件)/ JSX、集成 Signals(细粒度响应性)的渐进式框架。

Vue 目前发布了 v3.6-beta(公测版),主要包括:

  • 移植 alien-signals 重构 @vue/reactivity
  • 新增可选的 Vapor Mode 编译模式,这是一种专属于 <script setup> SFC 的无虚拟 DOM “蒸汽模式“
  • Vapor Mode 是 Vue 当前 API 的子集,所以部分功能受限,比如不支持 <Suspense /> 组件和 Options API

尤大最初在 2022 透露了 Vapor Mode(蒸汽模式),去年 Vue 从 Alpha 顺利晋升到 Beta,今年有望正式发布。但考虑到尤大还需要兼顾 Vite+ 等海量开源项目,暂不确定,敬请期待!

Vue Router 5.0

Vue Router 是 Vue 生态官方的客户端路由库,年初发布了 v5.0 主版本,主要包括:

  • 不再依赖 unplugin-vue-router,将其集成为 vue-router/unplugin,支持基于文件的路由
  • IIFE 构建不再包含 @vue/devtools-api,该模块升级到 v8.0 之后,不再提供 IIFE 格式
  • v5.0 是一个过渡版本,v6.0 将只支持纯 ESM 模块

Pinia Colada v1 首发

Pinia 近期没有主/次版本更新,但上线了新的产品 Pinia Colada。

pinia-colada.png

Pinia Colada 是基于 Pinia 的 Vue 专属异步状态管理库,第一个主版本正式发布,优点在于:

  • 支持缓存、去重、SWR(过期重验证)等高级功能,不用我们自己定义相关复杂逻辑
  • 无需手写 isLoading 等属性,内部封装后暴露这些接口
  • 消除数据请求的大量重复模板代码,更符合人体工学

colada-demo.png

Nuxt 4.4

Vue 生态的第一全栈元框架 Nuxt 发布了 v4.4 次版本,主要包括:

  • 新增 createUseFetch() 等工厂函数,支持组合拦截器定义高级实例,比如创建带有默认选项的 useApiFetch() 来替换 useFetch()
  • 新增 useAnnouncer() 组合函数和 <NuxtAnnouncer /> 组件,适用于页面内容动态变化、但焦点不变的场景,比如表单提交
  • 更棒的 import 保护,现在会显示建议和完整的追踪信息
  • 构建性能分析报告,显示构建阶段或打包插件的持续时间等数据,轻松诊断性能瓶颈

profiler.png

🎉 Vite 生态工业革命

而 Vite 生态,尤大所在的 VoidZero 团队掀起了一场前端“工业革命“。

上周我们提到了 Rust 驱动的第一个 Vite 稳定版 Vite 8 正式发布,替换 Rollup + esbuild,采用 Rolldown + Oxc,性能爆表。

此外,基于 Oxc 编译器的格式化神器 Oxfmt 发布了 beta(公测版),JavaScript 跟 TypeScript 的格式化功能 100% 兼容 Prettier,但性能比快了 30 倍。

本期补充更多 Vite 生态的进展,包括 Oxlint 和 Vitest。

Oxlint JS 插件 Alpha

ESLint 的 Rust 移植版 Oxlint 也有新进展,它的 JS 插件进入 Alpha 阶段,目前 100% 通过 ESLint 内置规则的官方测试套件。

oxc-test.png

具体而言,Oxlint 采用 Rust 重写了 650+ 多条代码质检规则,涵盖了 ESLint 的大部分规则。即使没有使用 Rust 重写的规则,Oxlint 也提供了 oxlint-plugin-eslint 插件来无缝迁移,使得 Oxlint 100% 兼容 ESLint 的所有内置规则。

性能方面,Oxlint 团队把 Node 源码库的 ESLint 替换掉,进行测评跑分,性能暴涨近 5 倍。

Oxlint JS 插件的成熟意味着目前 JS 生态现存的 ESLint 插件,比如非官方的社区插件,也能无缝迁移到 Oxlint 项目。这样用户无需重写插件,同时部分受益于 Rust 的原生性能。

Vitest 4.1

Vite 生态衍生的 Vitest 是 GitHub 前十的测试框架,近期也发布了 4.1 次版本,主要包括:

  • 采用新鲜出炉的 Vite 8
  • 测试标签分组,按标签设置或筛选测试,借鉴 pytest 筛选标签的自定义语法
  • 开发体验优化,比如自定义 UI 窗口配置,Playwright 追踪视图改进,自动生成 GitHub Actions Job 摘要
  • Vitest 的 VS Code 扩展现在支持 Deno,import 语句后会显示模块加载时间

vscode.png

🛜 官方情报

ECMA-426 源码映射格式规范

Source Map(源码映射)是一种特殊的 JSON 文件,用于在我们编写的源码和运行时代码之间进行映射。

举个栗子,我们在开发时可能编写一些强大的方言,比如 Sass 或 TypeScript,再把它们转换成 HTML、CSS 或 JavaScript,这样浏览器才能正常执行。

source-map.gif

问题在于,当我们使用 devtools(开发者工具) debug 时,我们希望直接定位到源码,而不是编译或压缩后人类不可读的代码。

这时就要用到 Source Map 了,这个 JSON 文件中保留了源码和运行时代码之间的映射关系,比如哪一行、哪一列等等。

过去,Source Map 并未被标准化,大家通过一份谷歌文档约定实现,但一些功能始终无法协调。

为此,彭博社成立了 TC39-TG4(源码映射工作组),制定了 ECMA-426(源码映射格式规范)。

ecma.png

近年来,它们标准化了更多功能,Scopes 和 Range Mappings 也即将上线 devtools!

React 文档更新

React 文档更新了 <ViewTransition /> 组件结合 <Activity /> 组件章节,如果你想让组件在保持状态的同时实现进场或出场动画,可以使用 <Activity /> 组件。

react-doc.png

Svelte 最佳实践

Svelte 文档更新了“最佳实践“章节,帮助大家编写快速健壮的 Svelte 应用,可以将 svelte-core-bestpractices 投喂给 AI 代理作为 Skills 使用。

svelte-doc.png

🛠️ 工具推荐

Vite+ 全家桶开源

随着 Vite 生态的各个工具逐渐成熟,尤大创立的 Void Zero 公司也官宣:Vite+ 进入 Alpha 阶段!

Vite+ 是将 Vite 生态所有开发工具叠加在一起的一体化工具链,由 Vite Task 任务运行器驱动,提供了 vp install / vp dev 等命令。

具体而言,Vite+ 把 Vite 生态的所有流行的开源软件 —— Vite、Vitest、Oxlint、Oxfmt、Rolldown 和 tsdown 都添加到一个全家桶,用于开发、测试、代码质检、格式化和构建生产环境项目。

此外,Vite+ 还支持管理 pnpm / Bun 等包管理器,甚至能管理 Node 的版本。同时,我们配置的 lint 或格式化规则,比如 eslint.config.js.prettierrc 等配置文件,可以整合到单一的 vite.config.js 中。

vite-plus.gif

之前,Vite+ 原本要求对企业用户付费授权,现在尤大直接开源,完全免费。不管是公司还是独立开发者,都能纵享丝滑了,感谢 Void Zero 的慷慨!

随着 Vite+ 官宣 Alpha 版本,很多项目开始试用,Vite+ 目前已经集成到 Vue 源码的相关分支,还有 Vue CLI create-vue,进一步投入到生产环境测评。

Void Cloud 全栈开发

尤大在 Vue 大会演讲的最后,致敬乔布斯经典的“One More Thing“环节放大招,透露了新产品 Void Cloud。

VOID 是 Vite+ / Optimized(优化)/ Isomorphic(同构)/ Deployment(部署)这几个设计理念的首字母缩写,这是一款 Vite 专属的云服务插件,也是一个也云服务平台,或者全栈元框架。

Void 内置了强大的后端 SDK(软件开发工具),包括数据库、键值存储、对象存储、AI 推理、身份认证等后端应用常见功能,可以按需采用。

void.gif

由于 Void 基于 Vite 生态,因此所有前端框架/元框架天然支持,比如 React、Vue、Nuxt 等,且支持静态站点生成或服务端渲染等不同渲染方式。

Void Cloud 旨在让 Vite 用户能够一键部署,直接上线全栈应用,标志着 Vite 生态将进军全栈开发领域。

Antdv Next 组件库

我一般不推荐组件库,因为 GitHub 有大量成熟的组件库供大家白嫖,容易选择困难。但最近新出了一个 Vue 3 的组件库,它就是 Antdv Next!

Antdv Next 是一套开箱即用的高质量 Vue3 企业级组件库,基于阿里系的蚂蚁设计系统构建。

阿里系之前 Ant Design Vue 是比较流行的组件库,虽然其源码仓库还有提交,但我发现 2024 之后就没有再发布新版本了,目测不会推出新功能了。Ant Design Vue 支持 Vue 2 和 Vue 3,而 Antdv Next 只服务于 Vue 3。

我粗略看了一下,Antdv Next 采用了现代化的技术栈,比如 Vite / Vitest / pnpm 等,可以集成 AI、UnoCSS、Tailwind CSS 等。

Vue 初学者最不习惯的应该是 Antd 系列的组件源码都是基于 TSX 来实现,而不是常见的 SFC,但这只影响开源贡献,不会影响我们以 SFC 的方式使用。

由于组件实现采用了 TSX,Antdv Next 的自定义主题相应地也采用了 CSS-in-JS,其主题是目前我个人比较喜欢的亮点之一。

antdv-next.gif

总之,Antdv Next v1.0 已经正式官宣,值得继续关注,欢迎大家去 GitHub star 支持一波~

🙏 特别鸣谢

以上就是本期《Web 周刊》的全部内容了,希望对你有所帮助。

👍 感谢大家按赞跟转发分享本文,你的手动支持是我坚持创作的不竭动力喔。

😘 已经关注我的粉丝们,我们下期再见啦,掰掰~~

cat-thank.gif

从输入 URL 到页面:一个 Vue 项目的“奇幻漂流”

🧭 从 URL 到页面:一个 Vue 项目的“奇幻漂流”

这是一段你每天都可能经历的旅程:在浏览器输入一个地址,按下回车,几毫秒后,一个 Vue 单页应用就活生生地出现在屏幕上。这背后发生了什么?

Vue 的响应式系统、虚拟 DOM、编译器和“发布‑订阅”主角们——Observer、Dep、Watcher、Patch——是如何协作的?

让我们像侦探一样,一步步追踪这段旅程,用有趣但不失严谨的方式,把整个技术链路掰开揉碎。

🚀 第一站:浏览器 —— 资源的“快递小哥”

输入 URL → DNS 解析 → TCP 连接 → 请求 HTML → 接收响应

当你在地址栏敲下 https://my-vue-app.com,浏览器立刻化身快递调度中心:

  1. DNS 查询: 把域名变成 IP 地址(比如 192.0.2.1)。
  2. TCP 握手: 与服务器建立可靠连接。
  3. 发送 HTTP 请求: 告诉服务器“我要你的首页”。
  4. 服务器返回 HTML: 通常一个极简的 index.html,里面只有一个 <div id="app"></div> 和一串 <script src="/js/chunk-vendor.js"> 之类的标签。

这时 Vue 还没现身,只是一个空壳 HTML 被浏览器解析。但关键的 JS 文件已经开始下载——它们才是 Vue 的“灵魂”。

📦 第二站:Vue 实例诞生 —— “造物主”的仪式

当浏览器加载并执行完打包后的 JS 文件(通常由 Webpack/Vite 生成),Vue 的舞台正式搭好。

// main.js —— 一切从这里开始
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

这行代码背后,Vue 内部展开了一场精密的初始化交响乐:

🎼 乐章一:合并选项 & 生命周期初始化

  • 将传入的 routerstorerender 等与默认配置合并。
  • 设置内部标志(如 _isMounted),调用 beforeCreate 钩子。

🎼 乐章二:数据响应式 —— Observer 的“大改造”

beforeCreate钩子执行完,执行initState 接着初始化 injectinitState(propsdatacomputedwatch)provide

function initState(vm) {
    initProps(vm, opts.props);
    initMethods(vm, opts.methods); // 处理 methods
    initData(vm);       // 调用 observe() 将 data 转为响应式
    initComputed(vm, opts.computed);// 处理 computed
    initWatch(vm, opts.watch); // 处理 watch
}

响应式data这是最精彩的部分。Vue 会遍历 data() 返回的对象,递归地把每一个属性变成响应式:

  • Vue 2:用 Object.defineProperty 重写 getter/setter,每个属性配一个专属的 Dep(依赖管理器)。
  • Vue 3:用 Proxy 代理整个对象,更强大(能监听属性添加/删除)。
    // 简化的响应式模型
    data() {
      return { count: 0, user: { name: 'Alice' } }
    }
    
    // ↓ 响应式数据 内部主要实现

    // 1. Observer(观察者)- 数据劫持
    /**核心工作:
     *  - 为对象添加 __ob__ 属性,指向 Observer 实例
     *  - 对数组:重写 push/pop/shift/unshift/splice/sort/reverse 方法
     *  - 对对象:调用 defineReactive 将每个属性转换为 getter/setter
     */
    class Observer {
        constructor(value, shallow = false, mock = false) {
            this.value = value;
            this.shallow = shallow;
            this.dep = new Dep();        // 每个 Observer 持有一个 Dep
            this.vmCount = 0;
            def(value, '__ob__', this);  // 在对象上标记 __ob__
    
            if (isArray(value)) {
                // 数组:拦截变异方法
                this.observeArray(value);
            } else {
                // 对象:遍历每个属性,转换为 getter/setter
                const keys = Object.keys(value);
                for (let i = 0; i < keys.length; i++) {
                    const key = keys[i];
                    defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock);
                }
            }
        }
    }
    function defineReactive(obj, key, val) {
        observe(val); // 递归处理嵌套对象
        const dep = new Dep(); // 每个属性有自己的依赖管理器
        Object.defineProperty(obj, key, {
            get() {
                if (Dep.target) { // 当前正在执行的 Watcher
                    dep.addSub(Dep.target); // 依赖收集
                }
                return val;
            },
            set(newVal) {
                if (newVal !== val) {
                    val = newVal;
                    observe(newVal); // 新值如果是对象,也需要转为响应式
                    dep.notify(); // 派发更新,通知所有 Watcher
                }
            }
        });
    }
    
    // Dep 类 是一个依赖收集器,充当发布-订阅模式的调度中心:
    class Dep {
        constructor() { this.subs = []; }
        addSub(watcher) { this.subs.push(watcher); }
        notify() { this.subs.forEach(w => w.update()); }
    }
    // ↓ 经过 Observer
    count 拥有了 getter/setter + 一个 Dep
    user 对象也被递归改造,name 同样拥有 getter/setter + Dep

同时,computedwatch 也会创建对应的 Watcher(观察者)。但此时它们都只是“预备役”,还没有真正去订阅数据。

🎼 乐章三:created 钩子触发

现在 datacomputedmethods 都已经可用,但 DOM 还不存在。你可以在 created 里发起异步请求、设置定时器,因为响应式数据已经 ready。

🛠️ 第三站:编译 —— 模板如何变成“渲染函数”?

Vue 有两种方式获得 render 函数:

  • 你直接提供了(比如单文件组件里的 <script> 导出 render)。
  • 或者 Vue 需要编译模板——这是最通用的方式。 假设我们有一个模板:
<div id="app">
  <p>{{ message }}</p>
  <button @click="count++">Click me</button>
</div>

Compiler 会做三件事:

  1. 解析(Parse): 把模板字符串转换成 AST(抽象语法树)。AST 就是一个 JS 对象,精准描述了 DOM 结构、指令、文本插值等。
  2. 优化(Optimize): 标记静态节点(比如没有绑定任何动态数据的纯文本)。这一步为后续虚拟 DOM 的 diff 减负。
  3. 代码生成(Codegen): 从 AST 生成一个可执行的 render 函数,类似:
function render() {
    with(this) {
        return _c('div', { attrs: { id: 'app' } }, [
            _c('p', [_v(_s(message))]),
            _c('button', { on: { click: () => count++ } }, [_v('Click me')])
        ])
    }
}

注意:编译阶段不会把 {{ message }} 替换成具体值,也不会为每个指令绑定更新函数。它只产出 render 函数,真正的数据替换要到运行时。

##🎬 第四站:首次渲染 —— 从数据到真实 DOM 的“首秀”

🎼 乐章四:mountComponent 组件挂载阶段

created执行结束,开始执行 $mount 进入组件挂载阶段。

$mount 现在 datacomputedmethods 都已经可用,但 DOM 还不存在。你可以在 created 里发起异步请求、设置定时器,因为响应式数据已经 ready。

$mount 函数被调用,Vue 创建了一个渲染 Watcher

Vue.prototype.$mount = function (el, hydrating) {
    // ...
    return mountComponent(this, el, hydrating)
}
function mountComponent(vm, el, hydrating) {
    vm.$el = el;

    callHook$1(vm, 'beforeMount');
    // 创建更新函数
    const updateComponent = () => {
        vm._update(vm._render(), hydrating);  // render 生成 vnode,update 更新 DOM
    };
    // 创建渲染 Watcher !!!!!!!!!!!!!在这呢~
    new Watcher(vm, updateComponent, noop, {
        before() {
            if (vm._isMounted && !vm._isDestroyed) {
                callHook$1(vm, 'beforeUpdate');
            }
        }
    }, true)
    if (vm.$vnode == null) {
        vm._isMounted = true;
        callHook$1(vm, 'mounted');
    }
    return vm;
}

class Watcher {
    constructor(vm, expOrFn, cb, options, isRenderWatcher) {
        this.vm = vm;
        this.deps = [];          // 当前依赖的 Dep 列表
        this.newDeps = [];       // 新一轮收集的 Dep 列表
        this.depIds = new Set(); // 避免重复添加
        this.getter = expOrFn;   // 获取值的函数(渲染函数或表达式)

        this.value = this.lazy ? undefined : this.get();
    }

    get() {
        pushTarget(this);  // 将自己设为 Dep.target
        let value;
        try {
            value = this.getter.call(this.vm, this.vm);  // 执行 getter,触发依赖收集
        } finally {
            popTarget();      // 恢复上一个 Dep.target
            this.cleanupDeps(); // 清理不再需要的依赖
        }
        return value;
    }

    addDep(dep) {
        const id = dep.id;
        if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id);
            this.newDeps.push(dep);
            if (!this.depIds.has(id)) {
                dep.addSub(this);  // 双向绑定:Watcher 订阅 Dep
            }
        }
    }

    update() {
        if (this.lazy) {
            this.dirty = true;
        } else if (this.sync) {
            this.run();
        } else {
            queueWatcher(this);  // 异步队列更新
        }
    }
}

updateComponent 内部就是:vm._update(vm._render(), ...)。 渲染 Watcher 会立即执行一次,开启首次渲染之旅。

1️⃣ _render() —— 生成虚拟 DOM (VNode)

调用刚才生成的 render 函数。 在 render 执行过程中,this.messagethis.count 被读取 → 触发它们的 getter → 依赖收集开始!

  • 每个响应式属性的 Dep 会检查当前是否有活动的 Watcher(此时就是渲染 Watcher)。
  • 如果有,就把这个渲染 Watcher 添加到自己的订阅列表(subs)中。
// 伪代码:依赖收集
getter() {
  if (Dep.target) {
    dep.depend()  // 把 Dep.target(渲染 Watcher)加入 subs
  }
  return value
}

结果:messagecount 现在“认识”了渲染 Watcher。以后它们变了,就知道该通知谁。 render 最终返回一棵 VNode 树——一个轻量级的 JS 对象,描述了 DOM 结构。

2️⃣ _update() —— patch 挂载到真实 DOM

调用 __patch__ 函数,首次渲染时 oldVnode 是挂载点(真实 DOM 元素,比如 <div id="app">),vnode 是新 VNode。

patch 会递归地创建真实 DOM 元素,设置属性、事件监听(比如 @click 被绑定到真正的 click 事件),最后把生成的 DOM 插入到页面中。

页面终于显示了! 🎉 随后 mounted 钩子被调用,你可以在里面操作 DOM 了。

🔄 第五站:交互与响应式更新 —— “自动档”的魔法

用户点击了“Click me”按钮,count++ 被执行。

1️⃣ 数据变化

countsetter 被触发,内部调用 dep.notify()

2️⃣ 派发更新

dep.notify() 会遍历 subs 列表(里面目前有渲染 Watcher),调用每个 Watcher 的 update() 方法。

3️⃣ 异步调度

update() 不会立即重新渲染,而是调用 queueWatcher(this) 把渲染 Watcher 放入一个异步队列。 Vue 通过 nextTick(微任务或降级宏任务)来批量处理更新,避免同一个 Watcher 被重复添加(去重)。

4️⃣ 重新渲染与 Diff

在下一个 tick,队列被清空:

  • 渲染 Watcher 执行 run() → 再次调用 updateComponent。
  • 重新执行 render() 生成新 VNode(此时 count 已经变成新值,依赖收集会重新建立,旧依赖会被清理)。
  • 调用 _update() 执行 patch(oldVNode, newVNode)Diff 算法登场(Vue 2 双端比较 / Vue 3 快速 diff + 最长递增子序列):
  • 比较新旧 VNode 树,找出最小变化集。
  • 只更新变化的部分(比如按钮文本从 “Click me” 变成 “Click me (1)”),而不重新渲染整个列表。 最终真实 DOM 被高效更新,用户看到了新的数字。 随后 updated 钩子触发。

🗺️ 完整流程图

URL 输入
   ↓
DNS 解析 → TCP 连接
   ↓
HTML 加载 & 解析 JS
   ↓
new Vue() 
   ├─ 合并选项
   ├─ beforeCreate(inject → props → )
   ├─ initInjections → initState(methods → data → computed → watch)
   ├─── Observer 转换 data(响应式 + Dep)
   ├─── 初始化 computed / watch(创建 Watcher)
   ├─ created
   └─ $mount
        ├─ 编译模板 → render 函数(如果没提供)
        ├─ 创建渲染 Watcher(Vue 2) / Effect(Vue 3)
        │    ├─ 执行 _render() → 读取响应式数据 → 依赖收集(数据→Dep→Watcher) → 生成VNode
        │    └─ 执行 _update() → patch → 真实 DOM
        └─ mounted
   ↓
用户交互(修改数据)
   ├─ setter → dep.notify()
   ├─ 渲染 Watcher 被推入异步队列
   ├─ nextTick 执行队列
   │    ├─ 重新执行 _render() → 新 VNode
   │    └─ patch(oldVNode, newVNode) → Diff → 更新 DOM
   └─ updated

🧐 一些有趣的细节(常见疑问)

❓ “模板里没用到的数据,会不会也被依赖收集?”

不会。渲染 Watcher 只收集本次渲染实际访问到的数据。如果 v-if 为 false 导致某个分支从未进入,那分支里的数据就不会被收集。当条件变为 true 时,下一次渲染会自动订阅它们。

❓ “v-showv-if 在依赖收集上有什么不同?”

  • v-if:条件为 false 时,该分支根本不渲染 → 不读取内部数据 → 无依赖收集 → 内部数据变化不会触发更新。
  • v-show:只是 CSS 隐藏,DOM 一直存在 → 每次渲染都会读取内部数据 → 依赖始终存在 → 数据变化会触发重新渲染(即使看不见)。

❓ “Observer 在发布‑订阅里是什么角色?”

它是“装修工人”——在初始化时把普通数据改造成带 getter/setterDep 的响应式对象。它不直接参与发布或订阅,但它是整个系统能够运转的基础。

❓ “Vue 3 比 Vue 2 快在哪?”

  • Proxy 代替 Object.defineProperty,可监听属性添加/删除、数组索引等。
  • 编译优化:静态提升、补丁标记、块树 → 让 diff 跳过静态内容。
  • 快速 diff + 最长递增子序列 → 减少 DOM 移动次数。

🎯 总结:从 URL 到像素的“奇幻漂流”

阶段 核心角色 产出
资源加载 浏览器、HTTP HTML + JS
Vue 初始化 ObserverDepWatcher 响应式数据 + 实例
模板编译 Compiler render 函数
首次渲染 渲染 Watcherrenderpatch 真实 DOM
交互更新 setterDep.notify、调度器、patch + diff 最小化 DOM 更新

总结: 从输入 URL 到 Vue 项目渲染,整个链路是:

URL 输入 → 网络加载(HTML 加载) & 解析 JS → Vue实例初始化(响应式数据、编译)→ 首次渲染 Watcher → 执行 render 生成 VNode → patch 创建真实 DOM → 挂载完成 →用户交互 → 数据变化 → 响应式派发 → 重新渲染 → Diff 更新 DOM

这趟旅程中,Vue 的每一个设计都精妙地平衡了声明式编程的优雅与底层性能的极致。希望这次“共探”,能让你下次启动 Vue 项目时,看到的不只是一个页面,而是一整套精心编排的幕后舞剧。

Vue3 日历组件选型指南:五大主流方案深度解析

在 Vue3 项目开发中,日历组件是日程管理、预约系统、数据可视化等场景的核心组件。不同项目对日历的功能需求差异极大——有的只需基础日期选择,有的需要支持多日程展示、自定义节假日、拖拽调整等复杂功能。本文从「易用性、扩展性、性能」三个维度,深入分析 5 款主流 Vue3 日历组件,并提供选型建议,帮助开发者快速找到适配场景的最佳方案。

一、Vue3 Datepicker:轻量无依赖的基础款

Vue3 Datepicker 是一款纯 Vue3+TypeScript 开发的日历组件,主打轻量与无依赖特性。该组件体积仅约 5KB,却提供了日期范围选择、禁用日期、自定义格式等实用功能。其样式简洁,开发者可通过 CSS 轻松覆盖默认样式,同时完美适配移动端与 PC 端。得益于纯 Vue3 的实现方式,该组件对 Composition API 和 Options API 都有良好的兼容性。

安装命令:

npm install vue3-datepicker --save

使用示例:

<template>
  <div class="basic-calendar">
    <Datepicker
      v-model="selectedDate"
      :disabled-dates="disabledDates"
      format="YYYY-MM-DD"
      placeholder="选择日期"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import Datepicker from 'vue3-datepicker';
import 'vue3-datepicker/dist/index.css';

const selectedDate = ref<Date | null>(null);

const disabledDates = (date: Date) => {
  const day = date.getDay();
  return day === 0 || day === 6;
};
</script>

<style scoped>
.basic-calendar {
  width: 300px;
  margin: 20px;
}
</style>

适用场景:表单中的生日选择、订单日期筛选等轻量级日期选择需求,以及追求极小打包体积的项目。

二、Element Plus Calendar:生态集成的标准化选择

Element Plus Calendar 是饿了么团队出品的企业级日历组件,与 Element Plus 组件库深度集成,视觉风格统一。该组件支持月视图、周视图、日视图三种模式切换,提供日程数据绑定能力,开发者可自定义单元格内容展示。内置国际化功能、日期范围选择、禁用日期等基础能力,并提供完整的 TypeScript 类型定义,可与 Vue3+Vite 开发环境无缝配合。

安装命令:

npm install element-plus --save

使用示例:

<template>
  <div class="el-calendar-demo">
    <el-calendar v-model="currentDate">
      <template #date-cell="{ data }">
        <p :class="data.isSelected ? 'is-selected' : ''">
          {{ data.day.split('-').pop() }}
        </p>
        <span v-if="scheduleMap[data.day]" class="schedule-count">
          {{ scheduleMap[data.day] }}条日程
        </span>
      </template>
    </el-calendar>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { ElCalendar } from 'element-plus';
import 'element-plus/dist/index.css';

const currentDate = ref<Date>(new Date());

const scheduleMap = ref({
  '2026-02-06': 3,
  '2026-02-08': 1,
  '2026-02-10': 2,
});
</script>

<style scoped>
.is-selected {
  color: #409eff;
  font-weight: bold;
}
.schedule-count {
  font-size: 12px;
  color: #f56c6c;
}
</style>

适用场景:使用 Element Plus 组件库的中后台管理系统,需要快速实现标准化日历和日程功能的项目。

三、FullCalendar Vue3:复杂场景的全功能方案

FullCalendar Vue3 是基于业界知名的 FullCalendar 核心库封装的 Vue3 组件,专为复杂日程管理场景设计。该组件支持月视图、周视图、日视图、列表视图、时间轴视图等 10 余种视图类型,提供了日程拖拽、调整时长、重复日程设置、自定义事件渲染等丰富功能。组件兼容 Vue3 的组合式 API,可与 Pinia 或 Vuex 状态管理库无缝集成。此外,还支持 Google 日历和 iCal 导入,具备国际化与时区切换能力。

安装命令:

npm install @fullcalendar/vue3 @fullcalendar/core @fullcalendar/daygrid @fullcalendar/interaction

使用示例:

<template>
  <div class="full-calendar-demo">
    <FullCalendar
      :plugins="calendarPlugins"
      initialView="dayGridMonth"
      :events="calendarEvents"
      editable="true"
      selectable="true"
      @dateClick="handleDateClick"
      @eventClick="handleEventClick"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';

const calendarPlugins = ref([dayGridPlugin, interactionPlugin]);

const calendarEvents = ref([
  { title: '产品评审会', start: '2026-02-06', end: '2026-02-07', color: '#409eff' },
  { title: '版本发布', start: '2026-02-09', color: '#67c23a' },
]);

const handleDateClick = (info: any) => {
  alert(`选择了日期: ${info.dateStr}`);
};

const handleEventClick = (info: any) => {
  alert(`点击了日程: ${info.event.title}`);
};
</script>

<style scoped>
.full-calendar-demo {
  width: 90%;
  margin: 20px auto;
}
</style>

适用场景:企业 OA 系统、会议室预约、课程表管理等复杂日程管理场景,需支持拖拽操作、多视图切换、复杂事件配置的项目。

四、Vant4 Calendar:移动端友好的轻量选择

Vant4 Calendar 是有赞团队出品的移动端日历组件,专为移动端 H5 和小程序场景优化。该组件在交互设计上充分考虑移动端特性,支持滑动切换月份、手势操作等移动端常见交互方式。功能方面支持日期范围选择、快捷日期选择(如近 7 天、近 30 天)、自定义弹窗样式等实用能力。组件体积仅约 8KB,性能表现优异,支持按需引入,与 Vant4 组件库整体风格保持一致。

安装命令:

npm install vant --save

使用示例:

<template>
  <div class="vant-calendar-demo">
    <van-button @click="showCalendar = true">选择日期</van-button>
    <van-calendar
      v-model:show="showCalendar"
      v-model="selectedDate"
      type="range"
      :min-date="minDate"
      :max-date="maxDate"
      @confirm="handleConfirm"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { VanCalendar, VanButton } from 'vant';
import 'vant/lib/index.css';

const showCalendar = ref(false);
const selectedDate = ref<[Date, Date]>([new Date(), new Date()]);
const minDate = ref(new Date('2026-01-01'));
const maxDate = ref(new Date('2026-12-31'));

const handleConfirm = (dates: [Date, Date]) => {
  console.log('选择的日期范围:', dates);
  showCalendar.value = false;
};
</script>

适用场景:移动端 H5 页面、小程序项目,需轻量级、交互友好的日期选择功能。

五、Vue3 Simple Calendar:极简逻辑的定制基石

Vue3 Simple Calendar 是一款独特的日历组件,它不包含任何样式封装,仅提供核心日历逻辑。该组件基于 Vue3 Composition API 开发,体积仅 3KB,没有任何第三方依赖。开发者可以完全自定义 UI 和交互方式,组件只负责处理日历的基本逻辑,如月份切换、日期选中、日期渲染回调等。

安装命令:

npm install vue3-simple-calendar --save

使用示例:

<template>
  <div class="custom-calendar">
    <simple-calendar
      v-model="currentMonth"
      @date-click="handleDateClick"
    >
      <template #header="{ year, month, prevMonth, nextMonth }">
        <div class="calendar-header">
          <button @click="prevMonth">上一月</button>
          <h3>{{ year }}年{{ month }}月</h3>
          <button @click="nextMonth">下一月</button>
        </div>
      </template>
      <template #day="{ date, isToday, isWeekend }">
        <div
          class="day-cell"
          :class="{ today: isToday, weekend: isWeekend, selected: selectedDate === date }"
        >
          {{ date.getDate() }}
        </div>
      </template>
    </simple-calendar>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import SimpleCalendar from 'vue3-simple-calendar';

const currentMonth = ref<Date>(new Date());
const selectedDate = ref<Date | null>(null);

const handleDateClick = (date: Date) => {
  selectedDate.value = date;
  console.log('选中日期:', date);
};
</script>

<style scoped>
.custom-calendar {
  width: 350px;
  margin: 20px;
}
.calendar-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}
.day-cell {
  width: 50px;
  height: 50px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.today {
  background-color: #409eff;
  color: white;
  border-radius: 50%;
}
.weekend {
  color: #f56c6c;
}
.selected {
  border: 2px solid #67c23a;
  border-radius: 50%;
}
</style>

适用场景:需要品牌化设计日历 UI、特殊交互效果的项目,或仅需复用核心日历逻辑的定制化开发场景。

六、选型指南与核心原则

面对多种日历组件选择,开发者需要根据项目实际情况做出判断。以下是各组件的核心对比:

组件类型 核心优势 适用场景 打包体积
Vue3 Datepicker 轻量无依赖、易定制 基础日期选择、表单场景 ~5KB
Element Plus Calendar 大厂背书、生态集成 中后台标准化日程功能 ~15KB
FullCalendar Vue3 全功能、复杂交互 企业 OA、预约系统 ~100KB
Vant4 Calendar 移动端适配、交互友好 移动端 H5、小程序 ~8KB
Vue3 Simple Calendar 完全自定义、极简逻辑 个性化 UI、定制化交互 ~3KB

怎么在VS Code 调试vue2 源码

总结一下怎么在VS Code 调试vue2 源码

  • 克隆vue2源码到本地
  • 在源码根目录安装依赖 把项目跑起来 生成/dist/vue.js文件
  • 在/examples/ 下随便找个文件(引入的文件要是我们生成的vue.js) 打上断点
  • 安装Live Server插件 把我们打上断点的文件在浏览器打开
  • 在.vscode文件夹下配置launch.json
  • 点击VS Code的Run and Debug图标 就可以进入了

我们开始吧~

克隆vue2源码到本地

去Github克隆源码,克隆后我们用VS Code打开。

git clone https://github.com/vuejs/vue

image.png

在源码根目录安装依赖 把项目跑起来
pnpm i

image.png

image.png

把项目跑起来

npm/pnpm run dev

image.png

bundles /Users/gongzemin/Documents/GitHub/vue/src/platforms/web/entry-runtime-with-compiler.ts → dist/vue.js...

entry-runtime-with-compiler.ts 这个入口文件打包生成 dist/vue.js 这个最终可用的 Vue 文件

生成了dist文件夹 里面有vue.js

image.png

在examples/ 下随便找个文件 打上断点

我们找examples/classic/commits/app.js 在如图位置打上断点

image.png

commits/index.html 这个文件引入了vue.min.js, 我们刚才构建出来的是vue.js文件,我们把引入的文件改成vue.js

<script src="../../../dist/vue.js"></script>
安装Live Server插件 把我们打上断点的文件在浏览器打开

image.png

安装好插件后,打开文件的上下文菜单 可以看到Open with Live Server

image.png

这样我们就可以打开我们的examples/classic/commits/index.html 文件了 是用服务器打开的

image.png

在.vscode文件夹下配置launch.json

注意这里的URL是我们的要调试URL路径

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Run parser",
      "runtimeExecutable": "parser",
      "cwd": "${workspaceFolder}/packages/reactivity-transform/node_modules/@vue/compiler-core",
      "args": []
    },
    {
      "type": "chrome",
      "request": "launch",
      "name": "调试Vue源码",
      "url": "http://localhost:5501/examples/classic/commits/index.html",
      "webRoot": "${workspaceFolder}",
      "sourceMaps": true,
      "sourceMapPathOverrides": {
        "webpack:///./*": "${webRoot}/*",
        "webpack:///packages/*": "${webRoot}/packages/*",
        "*": "${webRoot}/*"
      },
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}
点击VS Code的Run and Debug图标

点击Run and Debug图标, 选择调试Vue源码(就是我们配置launch.json里面配置的name)

image.png 看到app.js 进入我们打的断点了

image.png

我们点击Step Into

image.png

就进入Vue()构造函数了

image.png

调试vue3源码方法也一样 参考这篇笔记

能够插入 DOM 的输入框

简易富文本编辑器

使用input、textarea 这种输入框会出现一个问题,就是无法在其中写入 DOM 结构,浏览器不会把 DOM 进行渲染,这样的话在某些情况下使用他们只会浪费时间,复制粘贴半天,发现没办法放 UI 内容,无敌了孩子。

如果你的内容需要很多操作可以选择去使用富文本编辑器,这里就说一下怎么写一个简单的富文本编辑器。

     <div
        id="editor"
        contenteditable="true" // 赋予容器可编辑的能力
        ref="editorRef"
      ></div>

只要是 DOM 能放的结构,他都可以。

他也有一些缺点,就是没有input简便,好写,而且它只有一部分 input 对应的方法, 比如以下常见方法:

  • input
  • paste
  • blur、focus
  • keydown、keyup

如何插入 DOM(组件) 和文本

插入 DOM

const textNode = document.createTextNode(featureData.description); // 创建文本
const placeholder = document.createElement('span'); // 创建节点
placeholder.contentEditable = false; // 不可编辑
// 变量记录文本节点
featureData.lastTextNode = textNode;
featureData.lastTagHolder = placeholder; 
// 在编辑器最前方进行插入
editor.insertBefore(textNode, editor.firstChild);
editor.insertBefore(placeholder, editor.firstChild);

在vue的程序里面想要在普通函数中动态创建、挂载、操作组件可以通过vue提供的createApp去创建vue的节点

const app = createApp({
    render: () =>
      h(Tag, {
        text: featureData.title, // 组件 props 
        bgColor: featureData.bgColor, // 组件 props 
        onClose: () => {
          featureData.lastApp?.unmount();
          featureData.lastApp = null;
          featureData.lastTextNode?.remove();
          featureData.lastTagHolder?.remove();
          featureData.lastTextNode = null;
          featureData.lastTagHolder = null;
        },
      }),
  });
  app.mount(placeholder);
  featureData.lastApp = app; // 记录app实例进行卸载

h 函数

用于创建虚拟节点,可以渲染多个/嵌套/动态结构。

  1. 渲染组件 vnode 时 children 参数需要通过插槽函数书写,可以通过设置props为null避免将插槽识别为props。
  2. 渲染为 html 的节点 children 可以随意文本或者数组传递多个节点。
function h(
  type: string | Component,
  props?: object | null,
  children?: Children | Slot | Slots   // 为组件时需要通过插槽函数
): VNode

h( 
    组件 / 标签名, 
    属性、props、事件, 
    子节点/内容            // 子节点不是插槽就可以省略 props 书写
)
// 多个节点
h(
    'div'
    null,
    [
        h('div','文字') 
    ]
)
// 动态结构
h('div', isShow ? h(Tag) : h('span', '无标签') )

// 组件插槽传递 vnode
h(Components,null,{default:()=>'你的内容'})// 默认插槽

// html节点
h('div',null,['文字', h('span', '内容')])

鼠标选中区域

可以通过选中区域对文本区域进行记录,选中区域内容、获取选区范围等等,可以用于加粗、添加标题。

// 创建鼠标选区
  const range = document.createRange();
  // 设定鼠标选中区域
  range.setStartAfter(textNode); // 在 textNode 后面开始
  range.setEndAfter(textNode);   // 在 textNode 后面结束
  // 获取选区管理
  const sel = window.getSelection();
  // 获取选中文字
  const selectedText = sel.toString()
  // 获取第一个选区
  const range = sel.getRangeAt(0)
  // 移除先前选区
  sel.removeAllRanges();
  // 记录当前鼠标选区
  sel.addRange(range);

VTJ.PRO 发布 v2.3.6:开放共享模版、优化发布流程,低代码开发体验再升级

摘要: 基于 Vue 3 的开源 AI 低代码平台 VTJ.PRO 于 2026 年 4 月 10 日正式发布 v2.3.6 版本。本次更新聚焦模版共享与发布体验,开放共享模版功能,整合发布操作链路,并优化了版本控制与自动化截图能力,进一步降低了项目复用与协作的门槛。

未命名.png


开放共享模版,构建可复用的组件生态

v2.3.6 最值得关注的变化是 开放共享模版功能。开发者现在可以将自己设计的页面、模块或完整应用打包为模版,并发布到共享空间供团队或社区复用。同时,发布模版的版本控制机制得到强化,解决了此前模版更新失败的问题,使模版的迭代和回滚更加可靠。

  • 发布模版后,系统会自动在开发项目中创建对应的模版引用页面,实现“一次发布,处处引用”。
  • 模版共享结合原有的 AI 能力(设计稿转代码、自然语言生成页面),可大幅提升团队内部标准化组件的沉淀效率。

发布操作统一化,支持自动截图

为了减少多入口切换的认知负担,新版本将 发布应用、发布模版、项目出码 三个核心操作按钮整合至同一界面,开发者无需在不同菜单间跳转即可完成完整的交付流程。

此外,发布应用现已支持自动生成截图。系统会在发布时自动捕获当前应用界面的关键视图,方便在版本记录、发布日志或团队协作中快速识别应用状态。

默认公开与取消自动启动页,更贴合实际开发习惯

  • 创建应用时,访问权限默认为“公开”。这一调整降低了团队内部或开源项目中的分享门槛,同时也保持了随时可改为私有的灵活性。
  • 取消创建应用时自动新增启动页。此前新建应用会自动生成一个示例启动页,部分开发者反馈会带来额外的删除操作。新版本不再自动生成启动页,应用创建后直接进入空白设计状态,更符合从零开始的开发直觉。

开发者体验:从“可用”到“好用的低代码”

VTJ.PRO 一直强调 “降低复杂度,不降低自由度” ,v2.3.6 的更新再次印证了这一理念——通过优化发布链路和模版共享能力,让团队协作中的资产复用更加自然,同时保持对 Vue 源码的完全控制。

目前,VTJ.PRO 已在 Gitee 收获 9.9K Star,荣获 Gitee 2025 年度“大前端 Top3”。项目基于 Vue 3 + TypeScript + Vite,深度集成 ElementPlus、ECharts 等主流库,并已接入 DeepSeek、Qwen、Gemini、GPT 等 10+ 款大模型。

快速体验与更新方式


关于 VTJ.PRO
VTJ.PRO 是一款开源、基于 Vue 3 的 AI 低代码开发平台,支持可视化设计与手写代码双向转换,并提供私有化部署、多端输出(Web、H5、UniApp)、版本管理与企业级协作能力。项目始终保持“源码透明、无黑盒锁定”,是面向专业开发者的低代码解决方案。

11.png

Vue 迁移 React 实战:VuReact 一键自动化转换方案

一、核心关键词盘点

在 Vue 转 React 的技术迁移场景中,以下核心关键词是开发者必须聚焦的核心,也是本次方案落地的关键抓手:

  • 核心诉求:Vue 3 迁移 React 18+、自动化转换、减少手动重写成本、保留 TypeScript 类型、响应式系统适配
  • 核心工具:VuReact(编译核心 @vureact/compiler-core + 运行时 @vureact/runtime-core@vureact/router
  • 核心能力:智能编译、一键命令行转换、Scoped 样式适配、Composition API 转 React Hook、渐进式迁移
  • 核心痛点:手动改写易出错、响应式系统差异、生命周期不兼容、Scoped 样式迁移、混合开发模式适配

vureact_hero_demo.gif

二、痛点拆解与优化方案

痛点 1:手动迁移成本高、易出错

现状分析

传统 Vue 转 React 需逐行改写组件、模板、响应式逻辑,大型项目耗时数月,且易因语法差异引入 Bug。

优化方案:VuReact 一键自动化编译

通过 VuReact 实现零手动改写的自动化转换,核心步骤如下:

  1. 安装核心依赖
npm install -D @vureact/compiler-core
  1. 配置转换规则 创建 vureact.config.js,精准控制输入/输出/排除规则:
import { defineConfig } from '@vureact/compiler-core';

export default defineConfig({
  input: 'src', // 待迁移的 Vue 源码目录
  exclude: ['src/main.ts'], // 排除 Vue 入口文件
  output: {
    outDir: 'react-app', // React 代码输出目录
  },
});
  1. 执行一键转换
# 完整编译(生产环境)
npx vureact build
# 实时编译(开发调试)
npx vureact watch

痛点 2:Vue 响应式系统与 React Hook 不兼容

现状分析

Vue 的 ref/computed/watch 与 React 的 Hook 模式差异大,手动转换易破坏响应式逻辑。

优化方案:响应式语法自动适配

VuReact 内置专属运行时 Hook,无缝转换 Vue 响应式语法:

Vue 3 原语法 React 转换后语法
ref(0) useVRef(0)
computed(() => {}) useComputed(() => {})
watch(source, callback) useWatch(source, callback)

实战示例

<!-- Vue 原代码 -->
<script setup lang="ts">
// @vr-name: Demo
import { ref, computed, watch } from 'vue';
const price = ref(100);
const quantity = ref(2);
const total = computed(() => price.value * quantity.value);
watch(quantity, (newVal) => console.log('数量变化:', newVal));
</script>
// VuReact 自动转换后的 React 代码:Demo.tsx
import { useVRef, useComputed, useWatch } from '@vureact/runtime-core';

const Demo =  memo(() => {
  const price = useVRef(100);
  const quantity = useVRef(2);
  const total = useComputed(() => price.value * quantity.value);
  useWatch(quantity, (newVal) => console.log('数量变化:', newVal));
});

export default Demo;

痛点 3:Vue Scoped 样式迁移后失效

现状分析

Vue 的 Scoped 样式通过 data-v-hash 隔离,React 无原生支持,手动迁移易导致样式污染。

优化方案:Scoped 样式自动模块化

VuReact 编译时自动生成 CSS Module,零运行时开销实现样式隔离:

<!-- Vue 原代码 -->
<template>
  <div class="container"><h1>标题</h1></div>
</template>
<style scoped>
.container { padding: 20px; background: #f5f5f5; }
h1 { color: #333; }
</style>
// 自动生成的 React 代码
import $style from './Component-abc123.module.css';

const Component = () => {
  return (
    <div className={$style.container} data-css-abc123>
      <h1 data-css-abc123>标题</h1>
    </div>
  );
};
/* 自动生成的 CSS Module 文件 */
.container[data-css-abc123] {
  padding: 20px;
  background: #f5f5f5;
}
h1[data-css-abc123] {
  color: #333;
}

痛点 4:大型项目无法一次性迁移

现状分析

企业级项目直接全量迁移风险高,需支持 Vue/React 混合开发、按模块渐进迁移。

优化方案:渐进式迁移策略

  1. 按目录精准迁移
# 仅迁移组件目录
npx vureact build --input src/components
# 排除遗留代码目录
npx vureact build --exclude "src/legacy/**/*"
  1. 混合开发模式配置
export default defineConfig({
  input: 'src',
  exclude: [
    'src/legacy', // 保留未迁移的 Vue 代码
    'src/main.ts', // 保留 Vue 入口
  ],
  output: { outDir: 'react-app' },
});

痛点 5:工程化配置迁移繁琐

现状分析

迁移后需重新配置 React 项目的依赖、构建工具(Vite/Webpack),耗时且易遗漏。

优化方案:全自动工程化输出

  1. 自动生成依赖清单
{
  "name": "react-app",
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "@vureact/runtime-core": "^1.0.0",
    "@vureact/router": "^2.0.1"
  },
  "devDependencies": {
    "typescript": "~5.8.3",
    "@eslint/js": "^9.25.0",
    "@types/react": "^19.1.2",
    "@types/react-dom": "^19.1.2",
    "@vitejs/plugin-react": "^6.0.1",
    "eslint": "^9.25.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.19",
    "globals": "^16.0.0",
    "typescript-eslint": "^8.30.1",
    "vite": "^8.0.0"
  }
}
  1. 自动生成构建配置(以 Vite 为例)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
})

三、完整迁移流程(开箱即用)

# 1. 安装 VuReact
npm install -D @vureact/compiler-core

# 2. 快速创建配置文件
echo "import { defineConfig } from '@vureact/compiler-core';
export default defineConfig({
  input: 'src',
  exclude: ['src/main.ts'],
  output: { outDir: 'react-app' },
});" > vureact.config.js

# 3. 执行迁移编译
npx vureact build

四、核心支持能力汇总

特性 VuReact 支持情况
Vue 3 <script setup> ✅ 完整支持
TypeScript 类型保留 ✅ 零丢失
模板指令(v-if/v-for) ✅ 自动转 JSX
生命周期(onMounted/onUnmounted) ✅ 转专属 Hook
Scoped 样式 ✅ 转 CSS Module
混合开发模式 ✅ 支持
渐进式迁移 ✅ 按目录/文件控制

五、总结

VuReact 作为 Vue 转 React 的一站式自动化工具,核心价值在于:

  1. 降成本:一行命令替代手动重写,迁移效率提升 90%+;
  2. 低风险:保留原有业务逻辑、TypeScript 类型,减少 Bug 引入;
  3. 高灵活:支持渐进式迁移、混合开发,适配大型项目场景;
  4. 全兼容:覆盖响应式、样式、生命周期、模板等全维度语法转换。

无论是中小型组件库迁移,还是大型企业级 Vue 应用升级 React 架构,VuReact 都能实现“无痛迁移”,让前端技术栈升级不再是技术债务,而是高效的架构迭代。

推荐阅读

Vue3 代码编写规范 | 避坑指南+团队协作标准

一、Vue3 通用基础规范(必看!统一编码底线)

1.1 编码格式规范:避免格式混乱,提升代码可读性

  • 缩进:统一使用4个空格缩进(禁止使用Tab),确保不同编辑器渲染一致。
  • 换行:每个独立代码块之间空1行,逻辑相关的代码块紧密排列,提升可读性。
  • 分号:语句结尾统一添加分号,避免因自动分号插入(ASI)导致的语法歧义。
  • 引号:模板内属性使用双引号(""),Script中字符串优先使用单引号(''),特殊场景(如嵌套引号)可灵活切换。
  • 注释:关键逻辑、复杂业务代码必须添加注释,注释需简洁明了,说明“为什么做”而非“做了什么”;组件开头可添加类注释,说明组件功能、Props、使用场景。

1.2 命名规范:一眼看懂用途,降低协作成本

核心原则:JS/TS领域遵循camelCase(小驼峰)/PascalCase(大驼峰),HTML领域使用kebab-case(连字符),保持项目内命名一致性,提升代码可读性与协作效率。

  • 变量/函数:使用camelCase,首字母小写,动词开头命名函数(如handleClick、fetchData),名词开头命名变量(如userInfo、goodsList)。
  • 常量:使用UPPER_SNAKE_CASE(全大写下划线分隔),如const API_BASE_URL = 'api.example.com'。
  • 类/组件:使用PascalCase,首字母大写,组件名需为多个单词(根组件App除外),避免与HTML原生元素冲突,如UserProfile、GoodsCard而非Todo、Button。
  • 自定义指令:使用kebab-case,如v-focus、v-scroll-to,符合HTML属性命名规范。

二、Vue3 单文件组件(SFC)规范(核心重点!避坑关键)

2.1 组件结构规范:固定结构,避免渲染异常

单文件组件(.vue)内部顺序固定为:template → script → style,每个部分独立成块,结构清晰;template内最多包含一个顶级元素,避免多根节点导致的渲染异常。

<!-- 正确示例 -->
<template>
    <div class="user-profile">
        <!-- 组件内容 -->
    </div>
</template>

<script setup>
// 逻辑代码
</script>

<style scoped>
// 样式代码
</style>

2.2 Template 规范:高效渲染,减少性能损耗

  • 指令使用:v-bind、v-on可使用简写(:、@),v-slot使用#简写;指令顺序统一为:v-for → v-if → v-bind → v-on,如<div v-for="item in list" :key="item.id" v-if="item.visible" @click="handleClick">
  • v-for 要求:必须搭配key,key值需为唯一标识(如id),禁止使用index作为key;避免在v-for内使用v-if,可通过计算属性过滤数据后再渲染,提升性能。
  • 组件引用:模板中使用组件时,优先使用PascalCase标签(如),明确区分原生HTML元素;DOM模板中必须使用kebab-case(如),因HTML不区分大小写。
  • 属性绑定:多个属性分行书写,每个属性占一行,提升可读性;布尔属性直接写属性名,如而非。

2.3 Script 规范:简洁高效,符合Vue3最佳实践

2.3.1 语法选择:优先<script setup>,拒绝混合语法

优先使用<script setup>语法(Vue3推荐),简洁高效;复杂组件(如需要生命周期钩子、Props验证、 emits定义)可结合Options API,但同一项目内语法需统一,禁止混合使用。

2.3.2 导入顺序:规范排序,提升代码可维护性

导入语句按以下顺序排列,不同类别之间空1行,提升可读性:

  1. Vue内置API(如ref、computed、watch);
  2. 第三方库(如Pinia、Axios、Element Plus);
  3. 项目内部组件(如子组件、基础组件);
  4. 工具函数、常量、样式文件;
  5. API接口请求函数。
<script setup>
// 1. Vue内置API
import { ref, computed, watch } from 'vue';
// 2. 第三方库
import { useUserStore } from 'pinia';
import axios from 'axios';
// 3. 内部组件
import BaseButton from './BaseButton.vue';
import UserCard from '@/components/UserCard.vue';
// 4. 工具函数/常量
import { formatDate } from '@/utils/format';
import { API_BASE_URL } from '@/constants';
// 5. API接口
import { fetchUserInfo } from '@/api/user';
</script>

2.3.3 Props 规范:严谨定义,避免传参异常

  • 命名:Props定义使用camelCase(如userName),模板中传递时使用kebab-case(如user-name),Vue会自动完成转换。
  • 定义:Props需详细定义,至少指定类型;必填项标注required: true,可选值通过validator验证,提升组件可维护性与容错性。
// 正确示例
const props = defineProps({
    // 基础类型定义
    userId: {
        type: Number,
        required: true,
        validator: (value) => value > 0 // 验证值为正整数
    },
    // 布尔类型,推荐前缀is
    isDisabled: {
        type: Boolean,
        default: false
    },
    // 数组/对象类型,默认值需用函数返回,避免引用共享
    goodsList: {
        type: Array,
        default: () => []
    },
    userInfo: {
        type: Object,
        default: () => ({
            name: '',
            age: 0
        })
    }
});

2.3.4 Emits 规范:明确声明,避免事件混乱

  • 命名:定义时使用camelCase(如updateValue),模板中监听时使用kebab-case(如@update-value),符合HTML属性命名习惯。
  • 定义:通过defineEmits明确声明组件触发的事件,禁止隐式触发事件;事件参数需清晰,避免传递过多参数,复杂参数建议封装为对象。
// 正确示例
const emit = defineEmits(['updateValue', 'deleteItem']);

// 触发事件(传递单个参数)
const handleValueChange = (value) => {
    emit('updateValue', value);
};

// 触发事件(传递复杂参数,封装为对象)
const handleDelete = (id, name) => {
    emit('deleteItem', { id, name });
};

2.3.5 异步逻辑规范:优雅处理,避免报错中断

  • 优先使用async/await语法,禁止使用Promise链式调用(then/catch),代码更易读且便于调试。
  • 所有async/await必须包裹try/catch,或在调用时用.catch()捕获错误,避免控制台报错和逻辑中断;错误处理需友好,可结合UI提示反馈给用户。
  • 高频触发的异步请求(如搜索输入框)必须加防抖,避免无效请求,推荐用组合式函数useDebounce封装复用。
// 正确示例(async/await + try/catch)
const fetchUser = async () => {
    try {
        const res = await fetchUserInfo(); // 调用异步接口
        return res.data;
    } catch (err) {
        console.error('获取用户信息失败:', err);
        ElMessage.error('加载失败,请重试');
        throw err; // 如需上层处理,可重新抛出错误
    }
};

// 错误示例(Promise链式调用)
const fetchUser = () => {
    return fetchUserInfo()
        .then(res => res.data)
        .catch(err => {
            console.error('获取用户信息失败:', err);
            ElMessage.error('加载失败,请重试');
            throw err;
        });
};

2.3.6 TypeScript 规范:强类型约束,减少类型报错

  • 禁止滥用any类型:除非明确兼容所有类型(如第三方库无类型声明),否则必须用具体类型、unknown或泛型;若用any,需加注释说明原因。
  • 接口(interface)与类型别名(type)区分:定义对象/类的结构用interface(支持扩展、实现);定义联合类型、交叉类型或简单类型别名用type。
  • Props/Emits 类型:使用TypeScript时,优先通过泛型定义Props和Emits类型,提升类型安全性。
// 正确示例(interface定义对象结构)
interface Goods {
    id: number;
    name: string;
    price: number;
    stock: number;
}
const goods: Goods = { id: 1, name: '手机', price: 5999, stock: 100 };

// 正确示例(type定义联合类型)
type GoodsCategory = 'electronics' | 'clothes' | 'food';

// Props类型定义
interface Props {
    userId: number;
    isDisabled?: boolean;
}
const props = defineProps<Props>();

// Emits类型定义
const emit = defineEmits<{
    (e: 'updateValue', value: string): void;
    (e: 'deleteItem', params: { id: number; name: string }): void;
}>();

2.4 Style 规范:避免污染,提升样式复用性

  • 作用域:组件样式优先使用scoped(如),避免样式污染;全局样式统一放在src/styles目录下,禁止在组件内写全局样式(除非特殊需求)。
  • 命名:样式类名使用kebab-case,与组件名、功能对应,如.user-profile、goods-card;避免使用无意义的类名(如box1、content2)。
  • 样式顺序:按“布局 → 尺寸 → 样式 → 交互”的顺序编写,如position → width → background → hover。
  • 复用:公共样式(如颜色、字体、间距)提取为变量,统一管理;重复使用的样式封装为Mixin或自定义样式类,提升复用性。

三、Vue3 组件设计规范(高复用+低耦合,团队必守)

3.1 组件拆分原则:拒绝大组件,提升可维护性

  • 单一职责:一个组件只负责一个功能,避免“大组件”(代码超过500行),复杂功能拆分为多个子组件,如将用户列表拆分为UserList(列表容器)、UserItem(列表项)、UserSearch(搜索框)。
  • 高复用低耦合:可复用组件(如按钮、输入框)提取为基础组件(放在src/components/base目录),组件间通过Props传递数据、Emits触发事件,禁止直接操作父/子组件数据。
  • 命名区分:基础组件统一前缀Base(如BaseButton、BaseInput),业务组件按功能命名(如OrderList、PaymentForm),布局组件前缀Layout(如LayoutHeader、LayoutSidebar)。

3.2 组件通信规范:清晰传参,避免数据混乱

  • 父子组件:父传子用Props,子传父用Emits,禁止子组件直接修改Props(单向数据流);复杂数据可通过v-model双向绑定(Vue3支持多v-model)。
  • 跨层级组件:优先使用Pinia状态管理,或使用provide/inject(适用于深层组件通信,需明确注入类型),禁止使用EventBus(易造成事件混乱)。
  • 同级组件:通过父组件中转(子传父 → 父传另一个子),或使用Pinia共享状态,避免直接通信。

四、Vue3 Pinia 状态管理规范(替代Vuex,简洁高效)

4.1 Store 设计原则:模块化拆分,避免冗余

  • 模块化:按业务模块拆分Store(如userStore、cartStore、goodsStore),避免单一Store过大;Store命名统一前缀use(如useUserStore),使用camelCase命名法。
  • 状态划分:State(状态)、Getters(计算属性)、Actions(异步/同步操作)分离,禁止在Getters中修改State,禁止在组件中直接修改Store的State(需通过Actions)。

4.2 状态操作规范:规范调用,避免状态异常

// stores/user.ts 正确示例
import { defineStore } from 'pinia';
import { fetchUserInfo } from '@/api/user';

export const useUserStore = defineStore('user', () => {
    // State:定义状态,使用ref/reactive
    const userInfo = ref({
        id: 0,
        name: '',
        avatar: ''
    });
    const isLogin = ref(false);

    // Getters:计算属性,依赖State,只读
    const userNickname = computed(() => userInfo.value.name || '未知用户');

    // Actions:处理同步/异步操作,修改State
    const setUserInfo = (info) => {
        userInfo.value = info;
        isLogin.value = true;
    };

    const logout = () => {
        userInfo.value = { id: 0, name: '', avatar: '' };
        isLogin.value = false;
    };

    // 异步Action,使用async/await
    const loadUserInfo = async (userId) => {
        try {
            const res = await fetchUserInfo(userId);
            setUserInfo(res.data);
        } catch (err) {
            console.error('加载用户信息失败:', err);
            throw err;
        }
    };

    return { userInfo, isLogin, userNickname, setUserInfo, logout, loadUserInfo };
});

五、Vue3 Vue Router 路由规范(优化体验,避免路由踩坑)

  • 路由命名:路由name使用kebab-case(如user-profile),与组件名、路径对应,提升可读性;路由path使用kebab-case(如/user/profile),符合URL命名规范。
  • 路由懒加载:所有路由组件均使用懒加载(() => import('组件路径')),减少首屏加载时间;基础组件无需懒加载。
  • 路由守卫:全局守卫用于权限控制(如登录验证),路由独享守卫用于单个路由的特殊控制,组件内守卫用于组件内的生命周期控制;避免在守卫中写复杂业务逻辑。
  • 参数传递:路径参数(params)用于必填参数(如/user/:id),查询参数(query)用于可选参数(如/list?page=1&size=10);接收参数时需做类型校验。
// router/index.ts 正确示例
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
    {
        path: '/',
        name: 'home',
        component: () => import('@/views/Home.vue'),
        meta: { title: '首页', requiresAuth: false }
    },
    {
        path: '/user/:id',
        name: 'user-profile',
        component: () => import('@/views/UserProfile.vue'),
        meta: { title: '用户详情', requiresAuth: true },
        props: true // 自动将params转为Props传递给组件
    },
    {
        path: '/404',
        name: '404',
        component: () => import('@/views/404.vue')
    },
    {
        path: '/:pathMatch(.*)*',
        redirect: '/404' // 路由匹配失败,重定向到404
    }
];

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes
});

// 全局前置守卫:登录验证
router.beforeEach((to, from, next) => {
    const userStore = useUserStore();
    if (to.meta.requiresAuth && !userStore.isLogin) {
        next('/login');
    } else {
        document.title = to.meta.title || 'Vue3 项目';
        next();
    }
});

export default router;

六、Vue3 工程化与协作规范(团队高效协作必备)

6.1 文件目录规范:结构清晰,便于维护

项目目录结构清晰,按功能模块划分,便于维护和协作,推荐目录结构如下:

src/
├── assets/          // 静态资源(图片、字体、图标等),命名使用kebab-case
│   ├── images/
│   ├── fonts/
│   └── icons/
├── components/      // 公共组件
│   ├── base/        // 基础组件(BaseButton、BaseInput等)
│   ├── layout/      // 布局组件(LayoutHeader、LayoutSidebar等)
│   └── business/    // 业务组件(OrderList、GoodsCard等)
├── views/           // 页面视图组件,命名使用PascalCase
│   ├── Home.vue
│   ├── UserProfile.vue
│   └── Order/
│       ├── OrderList.vue
│       └── OrderDetail.vue
├── stores/          // Pinia状态管理,命名使用useXXXStore.ts
│   ├── useUserStore.ts
│   └── useCartStore.ts
├── router/          // 路由配置
│   └── index.ts
├── api/             // API接口封装,按模块划分
│   ├── user.ts
│   └── goods.ts
├── utils/           // 工具函数,命名使用camelCase
│   ├── format.ts
│   └── request.ts
├── constants/       // 常量定义
│   └── index.ts
├── styles/          // 全局样式
│   ├── index.scss
│   └── variables.scss
├── composables/     // 组合式函数,复用逻辑
│   └── useDebounce.ts
└── App.vue          // 根组件

6.2 代码提交规范(Git Commit):清晰可追溯,便于审查

采用Conventional Commits标准,提交信息清晰,便于代码审查和版本回溯,格式为:(): 。

  • type(提交类型):feat(新功能)、fix(Bug修复)、docs(文档变更)、style(代码样式调整,不影响逻辑)、refactor(重构,不修复Bug也不增加功能)、test(测试相关)、chore(构建/工具变更)。
  • scope(范围):指定提交影响的模块(如user、router、goods),无明确范围可省略。
  • subject(描述):简洁明了,说明提交内容,首字母小写,结尾不加句号。
// 示例
feat(user): add password reset UI
fix(router): handle 404 redirect
chore(deps): upgrade axios to 1.2.0
docs: update component usage documentation

6.3 代码校验规范:统一格式,减少冲突

  • 工具配置:项目必须集成ESLint、Prettier,统一代码格式;安装依赖:npm install -D eslint prettier eslint-plugin-vue @typescript-eslint/parser eslint-config-prettier husky lint-staged。
  • 自动校验:配置pre-commit钩子(husky + lint-staged),提交代码时自动校验格式,不符合规范的代码禁止提交;开发过程中使用编辑器插件(如ESLint、Prettier)实时校验。
  • ESLint配置:继承vue3-recommended规范,结合项目需求调整规则,禁止禁用必要的校验规则(如禁止滥用any、禁止Props修改)。

七、Vue3 性能与安全规范(优化体验+规避风险)

7.1 性能优化规范:提速降耗,提升用户体验

  • 响应式优化:避免过度使用reactive,简单数据使用ref;大数据列表使用v-virtual-scroller(虚拟滚动),减少DOM渲染数量。
  • 计算属性与监听:computed用于依赖状态的计算(缓存结果),watch用于监听状态变化并执行副作用(如请求接口);避免在watch中写复杂逻辑,避免监听过多状态。
  • 资源优化:静态资源(图片)压缩,使用CDN加载;路由懒加载、组件懒加载;避免重复请求(添加请求缓存、防抖节流)。
  • DOM优化:减少DOM操作,避免在模板中使用复杂表达式;使用v-show替代v-if(频繁切换场景),v-if替代v-show(一次性渲染场景)。

7.2 安全规范:规避漏洞,保障项目稳定

  • XSS防护:避免直接插入HTML(如v-html),若必须使用,需对内容进行过滤;禁止使用eval、with等危险语法。
  • 接口安全:请求接口时添加token验证;敏感数据(如密码)加密后传输;接口返回数据需做类型校验,避免恶意数据导致的报错。
  • 依赖安全:定期更新项目依赖,避免使用存在安全漏洞的依赖包;安装依赖前检查依赖安全性(如使用npm audit)。

八、Vue3 补充规范(细节拉满,避免踩坑)

  • 兼容性:兼容主流浏览器(Chrome、Edge、Firefox最新版本),如需兼容旧浏览器(如IE11),需添加相应的polyfill。
  • 可维护性:代码书写简洁,避免冗余(如重复代码封装为函数/组件);注释清晰,便于后续维护和他人理解。
  • 一致性:项目内所有代码严格遵循本规范,团队成员需统一认知;新增规范需团队讨论确认后补充,避免个人风格差异导致的代码混乱。
  • 废弃代码:禁止保留无用代码(如注释掉的代码、未使用的变量/函数/组件),提交代码前删除废弃内容,保持代码整洁。
❌