Vue基础核心

Vue核心概念

一、Why Vue?

我们在学习了HTML、CSS、Javascript(前端开发三驾马车)以后,就可以上手开发网页了!

在很早期的网站可以,因为此时的网站十分简单,也许就是几段文字加几张图片,使用js写一点点交互特效。

但在现代前端开发中,情况远比此复杂:

传统开发方式的痛点

  1. DOM操作性能问题。在代码中直接使用Javascript,调用DOM Api 操作HTML元素。

    ini 复制代码
    // 原生JS更新列表, 每次修改DOM都会触发浏览器的重绘重排
    const list = document.getElementById("list");
    list.innerHTML = "";
    data.forEach(item => {
        const li = document.createElement("li"); // 创建元素
        li.textContent = item.name;
        list.appendChild(li); // 插入DOM,触发重排
    });

    问题:复杂页面中频繁的DOM操作会导致性能瓶颈。

  2. 数据与视图不同步。在每次需要页面元素值发生变化时,需要手动的使用javascript去调用DOM api,才能完成修改以及网页的正确显示。

    ini 复制代码
    // 原生JS:需要手动更新视图
    let count = 0;
    const button = document.getElementById('btn');
    const display = document.getElementById('count');
    ​
    button.addEventListener('click', () => {
      count++;
      display.textContent = count; // 必须手动更新!
    });

    问题:数据变化后容易忘记更新DOM,导致界面显示错误。

  3. 代码组织管理工作繁重。同一个元素的html、css、javascript 相关的代码却分布在不同的文件中,当工程有上千个元素模块时,不同的元素的处理代码分布在上千个不同的文件夹中,这对于管理开发工程来说是可怕的。

    xml 复制代码
    <!-- HTML文件 -->
    <div class="modal" id="modal1"></div>
    ​
    <!-- CSS文件 -->
    <style>
    .modal { /* 样式代码 */ }
    </style>
    ​
    <!-- JS文件 -->
    <script>
    document.getElementById('modal1') // 操作逻辑
    </script>

    问题:一个功能的相关代码分散在不同文件中,难以维护。

  4. 代码复用困难。直接编写html元素,再插入js、css文件,很多可以重复使用的"盒子"是不能很好的复用。这会导致代码冗余并且重复工作。

    xml 复制代码
    <!-- 每次需要模态框都要复制一遍 -->
    <div class="modal">
      <div class="modal-content">
        <!-- 重复的HTML结构 -->
      </div>
    </div>

    问题:相同功能需要重复编写代码。

为了解决这些问题,涌现了许多优秀的前端开发框架:Jquery、Angular、React、Vue等。

Vue的解决方案

问题1解决:声明式渲染 + 虚拟DOM

xml 复制代码
<template>
  <!-- Vue模板语法 -->
  <li v-for="item in items">{{ item.name }}</li>
</template>

编写的vue代码不直接"增删查改"DOM元素,vue提供了一套模板语法,可以很便捷的编写网页元素。vue会在使用vite等工具构建的过程中,将这些模板代码编译成为js代码。当网页触发html元素的"增删查改"时,不是直接调用DOM api去操作DOM元素,而是使用diff算法对比新旧差异,后决定怎么操作DOM元素,更加科学高效。

原理

  1. 【构建时(编译阶段)】

    1. vite通过vue编译器SFC文件中的<template>部分内容编译成为渲染函数(render function) (这个过程中会标记静态节点,即不会随数据变化的节点,避免后续更新时重复对比。Vue3的提升)
    2. 渲染函数、脚本逻辑、样式(按需注入)打包为浏览器可以执行的JS/CSS文件。
  2. 【运行时(浏览器运行阶段)】

    1. 【首次渲染】 执行打包好的代码,创建Vue实例/组件。 运行渲染函数生成虚拟DOM(VNode)vue虚拟DOM转化为真实DOM并挂载到页面元素中。
    2. 【数据更新】 当数据发生变化时,vue响应式系统触发重新运行渲染函数,生成新的虚拟DOM,此时vue会运行patch算法对比新旧差异,最小化、最优的批量更改真实DOM

    旧虚拟DOM vs 新虚拟DOM

    Diff算法(对比差异)

    仅更新有变化的部分

    高效DOM更新

【数据更新渲染】

问题2解决:响应式数据绑定

数据驱动UI变化,状态视图同步。编写的vue代码不手动改变DOM元素内容。需要改变的变量的值,在模板代码中声明响应式变量后,变量值发生改变,无需手动更新HTML中的DOM内容,元素标签内容可以直接发生变化。我们在做好数据绑定后,只需要关心数据的变化。

xml 复制代码
// Vue:数据变化自动更新视图
<template>
  <p>{{ count }}</p>
  <button @click="increment">+1</button>
</template>
​
<script setup>
import { ref } from 'vue'
const count = ref(0) // 响应式数据
​
const increment = () => {
  count.value++ // 数据变化,视图自动更新!
}
</script>
// 在template的<p></p>标签中,显示的数字是定义的一个变量,js中直接处理变量的变化,不用专门单独的编写js代码来更改html视图的内容。

MVVM模型

rust 复制代码
View(模板) <--> ViewModel(Vue实例) <--> Model(数据)
     ↓                    ↓                       ↓
  HTML DOM            响应式系统               JavaScript 
  用户界面            自动同步机制               数据对象
javascript 复制代码
vue的响应式系统有三个核心角色:【ref/reactive】,【渲染函数】,【依赖收集器】
​
- ref/reactive: 定义响应式变量,vue使用Proxy/Object.defineProperty定义的变量。
- 渲染函数:render function,使用了某个响应式变量的渲染函数。
- 依赖收集器:收集并存储所有依赖了某个响应式变量的渲染函数的集合。
​
这里响应式变量的工作方式同上,也分为【首次渲染】与【数据更新渲染】
- 首次渲染时,所有使用到响应式变量的渲染函数在读取该变量时,都会触发该变量的proxy get回调,此时会在返回该变量值的同时将该渲染函数存储到该响应式变量的依赖收集器中。
- 当响应式数据发生更新,会触发proxy set回调,此时会给变量设置新值的,同时会将依赖收集器中的所有渲染函数依次运行一遍,从而生成新的虚拟DOM,运行patch算法对比新旧,批量更新真实DOM。
​
// Vue2采取的是使用Object.defineProperty的方式来设置变量的set/get来实现这一过程
// Vue3则是采用的ES6推出的Proxy对象来劫持对象的get/set过程
const count = new Proxy(原始数据, {
    // 拦截读取数据的操作
    get(target, key) {
        // 核心:收集依赖 -> 记录"当前渲染函数依赖了这个key(count)"
        // 1. 找到当前正在执行的渲染函数(称为"当前活跃依赖")
        const activeEffect = 正在执行的渲染函数;
        // 2. 把这个渲染函数添加到count的依赖收集器(Dep)中
        depMap.set(key, activeEffect);
        // 3. 返回原始值,保证代码能够正常运行
        return target[key]
    },
    // 拦截"修改数据"的操作
    set(target, key, value) {
        // 1. 先更新原始数据的值
        target[key] = value;
        // 2. 找到这个key对应的所有依赖(比如count对应的渲染函数)
        const effects = depMap.get(key);
        // 3. 批量渲染函数 -> 生成新的虚拟DOM
        effects.forEach(effect, () => {
            effect();
        });
    }
})

问题3、4解决:组件化开发

网页中每一个区块元素的功能够可以圈定为一个"盒子",这个"盒子"就是组件,每一个组件都可以被引入插入到其他组件的树形结构当中,实现模块的复用。单文件组件(SFC),就是将同一个组件的HTML、Javascript、CSS代码书写到同一个文件中。

每个组件都有自己完整的独立的作用域。组件内部的setup数据、函数,scoped样式都是组件私有的,和外部隔离。

xml 复制代码
<!-- Modal.vue - 单文件组件 -->
<template>
  <!-- HTML模板 -->
  <div class="modal">
    <slot></slot>
  </div>
</template>
​
<script setup>
// JavaScript逻辑
const props = defineProps(['title'])
</script>
​
<style scoped>
/* 组件私有样式 */
.modal { border: 1px solid #ccc; }
</style>
​
​
​
<!-- 复用组件 -->
<template>
  <Modal title="提示">内容</Modal>
  <Modal title="确认">确定吗?</Modal>
</template>
javascript 复制代码
// 子组件 Son.vue在被Vite/Webpack编译后会成为一个构造对象,不是传统的面向对象的构造函数,
export default {
    // 组件名称
    _name: "Son",
    // <template> 中的内容编译为render()函数
    render() {},
    // <script setup> 中的内容编译到setup()函数中
    setup() {},
    // <style scoped> 样式文件
    _scopedId: "",
    // 编译后的样式(会自动注入页面)
    styles:[]
}
​
​
// 父组件 Father.vue
import Son from "......" // 这里import的就是子组件编译后的构造对象,借用了ESM的模块的引用(内存地址)能力,而非简单的复制构造对象
<Son /> // 在父组件的<template>中使用一次<Son />, 就是使用createVNode(Son)创建一个Son组件的实例,多次使用就会创建多个独立的实例,他们之间的数据和状态时互不影响的。
scss 复制代码
内存中:
┌─────────────────────────────────┐
│  Son.vue 编译后的模块对象(单例) │
│  { render, setup, ... }         │ <-- 所有 import 都指向这里
└─────────────────────────────────┘
           ↑            ↑            ↑
           │引用        │引用        │引用
┌──────────┴───┐ ┌─────┴─────┐ ┌───┴──────────┐
│ 组件实例 1    │ │ 组件实例 2 │ │ 组件实例 3   │
│ (独立数据)    │ │ (独立数据) │ │ (独立数据)   │
└──────────────┘ └───────────┘ └──────────────┘

二、"盒子"理念:理解组件

组件,这是现代前端框架都会提及到的一个基本概念。

什么是组件?

组件 ------ 就是由一个或多个HTML元素组成的一个一个的模块。就像JS函数,它的内部对于外部是不可以直接访问的,可以接收来自外部的传入的参数,也可以通过expose "暴露" 一些内部的状态供外部调用。

csharp 复制代码
┌─────────────────────────────────┐
│          Vue 组件               │
│                                 │
│  输入:                          │
│  ├── Props (属性)               │
│  ├── Attrs(属性)               │
│  ├── Slots (插槽)               │
│  └── Inject (注入)              │
│                                 │
│  内部:                          │
│  ├── 响应式数据 (ref, reactive) │
│  ├── 计算属性 (computed)        │
│  ├── 生命周期钩子                │
│  ├── 侦听器 (watch)             │
│  └── 方法 (methods)             │
│                                 │
│  输出:                          │
│  ├── Emit (触发事件)            │
│  ├── Expose (暴露方法)          │
│  └── Provide (提供数据)         │
└─────────────────────────────────┘

想象你在搭积木:每个积木块(组件)都有特定的形状和功能,你可以用它们组合成各种结构(页面)。

组件的特点

  • 封装性:内部状态和逻辑对外部不可见,只暴露接口。
  • 可复用性:一次编写,多处使用。
  • 可组合性:小组件可以组合成更大的组件。
  • 独立性:每个组件有自己的状态和逻辑。
css 复制代码
App(应用根组件)
├── Header(头部组件)
├── Main(内容区)
│   ├── Sidebar(侧边栏)
│   ├── Content(主要内容)
│   │   ├── Article(文章组件)
│   │   └── Comments(评论组件)
│   └── Pagination(分页组件)
└── Footer(底部组件)

比喻理解

  • 整个应用像一棵大树,App是树干
  • 其他组件是树枝和树叶
  • 数据像树液,在组件间流动

这种结构让代码组织清晰,易于理解和维护。我们在日常开发中大部分时间就是在开发这些绿色方块,组织它们之间的引用关系,以及其中的数据状态处理。

三、模板语法与响应式数据

1、模板语法(文本插值 & 指令)

在没有输入、输出的情况下,一个组件仅使用Vue的模板语言,就可以构成一个基本的、完整的组件。

  • 文本插值:{{}}
  • 指令:带有v-前缀的特殊attribute。

我们可以使用Vue的模板语法描述UI,声明式地将数据渲染到DOM中。

xml 复制代码
<template>
  <!-- 1. 文本插值 {{}} 语法 -->
  <p>计数器: {{ count }}</p>
  
  <!-- 2. 属性绑定 v-bind 简写为 : -->
  <button :disabled="isDisabled">点击我</button>
  
  <!-- 3. 事件绑定 v-on 简写为 @ -->
  <button @click="handleClick">增加</button>
  
  <!-- 4. 条件渲染 v-if -->
  <p v-if="count > 10">计数大于10了!</p>
  <p v-show="count > 10">计数大于10了!</p>
  
  <!-- 5. 列表渲染 v-for -->
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
  <!-- 6. 双向绑定 v-model -->
  <input v-model="message" placeholder="输入文本">
  <p>输入的内容: {{ message }}</p>
  <!-- v-model 展开书写 -->
  <input
    :value="message"
    @input="event => message = event.target.value"
    placeholder="输入文本"
  >
  <p>输入的内容: {{ message }}</p>
​
  <!-- 7. 其他指令 -->
  <!-- v-text:设置元素的 textContent 属性 -->
  <span v-text="msg"></span>
  <!-- 等同于 -->
  <span>{{msg}}</span>
​
  <!-- v-html:渲染HTML -->
  <div v-html="htmlContent"></div>
​
  <!-- v-once:仅渲染元素和组件一次,并跳过之后的更新 -->
  <div v-once>{{ initialValue }}</div>  
​
  <!-- v-memo:缓存一个模板的子树。在元素和组件上都可以使用。 -->
  <!-- 当组件重新渲染,如果 valueA 和 valueB 都保持不变,这个 <div> 及其子项的所有更新都将被跳过。-->
  <div v-memo="[valueA, valueB]"> ... </div>
​
  <!-- v-pre:跳过编译 -->
  <div v-pre>{{ 这里的内容不会被编译 }}</div>
​
  <!-- v-cloak:只在没有构建步骤的环境下需要使用 -->
  <div v-cloak>{{ message }}</div>
</template>
​
<script setup>
import { ref } from 'vue'
​
const count = ref(0)
const isDisabled = ref(false)
const items = ref([
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' }
])
​
const handleClick = () => {
  count.value++
  isDisabled.value = count.value > 5
}
</script>

2、响应式数据

Vue组件中的响应式数据可以分为两个部分:组件内部,全局。

① 组件内部响应式数据

这种响应式数据是在组件内部定义以及使用,在组件外部不能够直接访问到,是组件内部的 "变化" 。由vue框架原生提供的 ref, reactive, computed等api定义并使用。

xml 复制代码
<template>
  <div>
    <p>点击次数: {{ count }}</p>
    <p>用户信息: {{ user.name }} - {{ user.age }}岁</p>
    <p>明年年龄: {{ nextYearAge }}</p>
    <button @click="increment">增加</button>
  </div>
</template>
​
<script setup>
import { ref, reactive, computed } from "vue"
​
// ref: 用于基本类型(数字、字符串、布尔值),也可以用于引用类型(对象、数组)
const count = ref(0)
​
// reactive: 用于对象类型
// reactive响应式数据存在一些限制:只能对象、不能整体替换(失去响应性)、结构操作不友好(失去响应性)。
// 由于这些限制的存在,Vue官方更加推荐使用ref()作为声明响应式状态的主要API。
// 90%的场景使用ref。reactive适用于表单数据、一组相关的状态集合、不需要整体替换的对象。
const user = reactive({
  name: "张三",
  age: 25
})
​
// computed: 计算属性,当依赖的数据变化时自动重新计算
const nextYearAge = computed(() => user.age + 1)
​
// 方法
const increment = () => {
  count.value++  // ref需要.value访问
  user.age++     // reactive直接访问属性
}
</script>

ref家族:不同场景的选择

php 复制代码
import { ref, shallowRef, toRef, toRefs } from 'vue'
// ref、reactive默认都是深度响应监听

// 1. ref - 深度响应式(默认)
const deepObj = ref({ count: 0 })
deepObj.value.count = 1 // 触发响应式更新

// 2. shallowRef - 浅层响应式(只响应.value变化)
const shallowObj = shallowRef({ count: 0 })
shallowObj.value = { count: 1 } // ✅ 触发更新
shallowObj.value.count = 2      // ❌ 不会触发更新!

// 由于reactive对于解构操作不友好:将reactive响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,将丢失响应性连接,解决办法使用toRef、toRefs
// 这样解构后的数据与原reative对象中的属性是保持一致,同步更改的

// 3. toRef - 将响应式对象的属性转为ref
const state = reactive({ count: 0 })
const countRef = toRef(state, 'count')
countRef.value++ // 会同步修改state.count

// 4. toRefs - 将响应式对象的所有属性转为ref
const { count, name } = toRefs(state)
// 可以在解构后保持响应性

computed: 计算属性值会基于其响应式依赖被缓存。

javascript 复制代码
<template>
	<p>{{ computedFunc() }} </p>
	<p>{{ computedParam }} </p>
</template>
<script setup>
	import { reactive, computed } from "vue"
	
    const author = reactive({
        book: ["1", "2", "3"]
    })
    
    const computedParam = computed(() => author.book?.length)
                                   
    const computedFunc = () => author.book?.length
</script>

// computed的懒执行:计算属性【只有在被第一次访问时才会执行】。

// 计算属性只有在其 【响应式依赖】 更新时才会重新计算。(这里的computedParam只有在author.book.length发生变化时才会更新,即便组件重渲染也不会发生更新。)
// 相比之下,方法调用【总是】会在重渲染发生时再次执行函数。(这里的computedFunc在每次重渲染时都会运行一次)

// 所以【能用computed就不用方法】:利用缓存提升性能。
xml 复制代码
- computed的 getter不应该有副作用。
	计算属性的getter应只做计算而没有任何副作用。(非常重要)
    【不要改变其他状态、在getter中做异步请求或者更改DOM!】
- 默认只读,可写需提供setter。避免在setter中直接修改计算属性值。
	computed({ get(){}, set(val) {} }) + v-model:双向绑定的计算属性。
	不要使用computed的setter去直接改变计算属性本身。
    更新计算属性应该通过【更新所依赖的源数据】以触发新的计算。
    // computed结合v-model双向绑定计算属性
    <template>
      <!-- 输入框显示「元」,但实际存的是「分」 -->
      <input v-model="priceInYuan" placeholder="输入价格(元)" />
      <p>实际存储(分):{{ priceInFen }}</p>
    </template>

    <script setup>
    import { ref, computed } from 'vue'
    const priceInFen = ref(1000) // 后端存的是分

    // 计算属性做单位转换
    const priceInYuan = computed({
      get() {
        return (priceInFen.value / 100).toFixed(2) // 显示元
      },
      set(newVal) {
        priceInFen.value = Math.round(parseFloat(newVal) * 100) // 输入元,转成分存
      }
    })
    </script>

computed自动依赖追踪:Vue会自动追踪 getter 中访问的所有响应式数据,只有这些数据变化时才会重新计算。

csharp 复制代码
1、作为依赖响应式数据的内容(类似于UI的render函数),响应式变量ref依赖源发生变化时,会同步发生更新变化。
此时computed的某个重计算的effect会被响应式数据ref的依赖收集器 收集,以便在响应式数据被触发setter时,触发运算以更新数据。

2、作为响应式变量(类似于响应式数据ref),computed是一种【特殊的,具有懒加载缓存性质的ref变量】。
此时computed在被 依赖它的render函数读取(触发getter)时,会将render函数收集到依赖收集器中,在computed的set dirty为true时,触发render函数的effects以更新UI。
rust 复制代码
初始化:
    1. 模板代码中<template>编译为render函数,将<script>中的内容编译为js逻辑代码,
        【此时,computed 初始化为一个特殊 ref 对象,
        	内部包含:value(缓存值)、dirty=true、dep(自己的依赖收集器)】
    -----> 
    2. 代码初始化运行,render()函数运行以生成虚拟DOM,读取到computed变量,
    【a、触发 computed 的 getter,computed 的 dep 收集当前 render() 为依赖;
        b、校验 dirty=true,执行 computed 的 getter 函数;
        c、读取所依赖的 ref 变量 → ref 的 dep 收集 computed 的 effect 为依赖;
        d、计算结果存入 value(缓存),将 dirty 设为 false;
        e、返回 value 给 render()】
        
更新时:
    1. 当相关响应式ref变量发生变化,ref 的 dep 运行所有收集的 effects(包括 computed 的 effect)。
        【此时,运行computed effect,不会直接运算,仅将computed dirty属性设置为true。】 
        -----> 
    2. dirty被设置为true后,直接通过自己的 dep 通知所有收集的 render() 重执行,
        【注意:这里和 computed 的 setter 完全无关】 
        -----> 
    3. 相关render() 重执行过程中读取 computed 值,此时 dirty=true,触发 computed 重运算
        【重新读取 ref 变量,计算新结果存入 value,dirty 设为 false,返回新值给 render()】

【为什么说这个dirty变量是「懒计算 + 缓存」的关键】
「懒计算」:源数据变化时不立刻重算,只有当 computed 被再次读取时才重算;
「缓存」:computed相关ref依赖变化,导致的render()重渲染,会读取computed变量,并且computed的dirty为true,所以会重计算值,这个和普通的变量没有区别,即都会在render()运行时,重新计算更新。
如果是其他的非computed相关ref依赖引起的render()重渲染,虽然也会读取computed变量, 但是因为此时computed的dirty为false,则不会重运算值,使用原值(缓存值)。此处dirty的存在,使代码免掉了多余的计算,也就是「缓存」。

【computed的「懒计算」,这种"先标脏再通知"比"直接更新",到底提升了什么性能?】
- 简单场景下没有差异(微乎其微)。
- 如果依赖computed数据计算逻辑非常复杂、元素一开始是v-if=false不展示,就不用在相关ref变量变化是立即执行。
- computed 提升的不只是性能,更是代码的可维护性、可扩展性和可读性。
- computed(90% 的场景优先选),用 watch(剩下的 10% 场景)

关键点

  • ref:用于基本类型,通过.value访问
  • reactive:用于对象,直接访问属性
  • computed:基于依赖缓存,只有依赖变化时才重新计算
② 全局响应式数据

全局状态管理(Pinia)

是整个工程中的响应式数据,当多个组件需要共享数据时,使用Pinia。工程中的每一个组件都可以通过对应的pinia提供的方法访问到,是全局的 "变化"。由vue的配套工具包------pinia提供储存和读取。

什么时候使用Pinia?

  • 多个组件需要共享数据
  • 需要持久化的用户状态
  • 复杂的状态管理逻辑
javascript 复制代码
// 定义
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    user: null
  }),
  actions: {
    increment() {
      this.count++
    },
    async fetchUser() {
      const response = await fetch('/api/user')
      this.user = await response.json()
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2,
    userInitial: (state) => state.user?.name.charAt(0)
  }
})
xml 复制代码
<!-- 在组件中使用 -->
<script setup>
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()

// 访问状态
console.log(store.count)

// 调用action
store.increment()

// 使用getter
console.log(store.doubleCount)
</script>

类似于Pinia的这种全局状态管理工具 可以理解为一个Vue工程的强力插件,但它并不属于Vue框架语言本身,这里就不对它的细节深入展开了。

四、组件基础:深入了解"盒子"

了解了最基本的组件构成成分------模板语法与响应式状态之后,进一步的了解组件概念中 "祖先"组件、父级组件、兄弟组件、子级组件、"后代"组件之间的交互------组件的输入与输出,数据传输链路。

组件就像一个"黑盒",有明确的输入、输出和内部机制:

csharp 复制代码
┌─────────────────────────────────────────────────────────┐
│                     Vue3 组件(SFC)                     │
│                                                          │
│  【输入】(外部 → 组件)                                  │
│  ├── Props:声明式接收父组件数据(只读)                  │
│  ├── Attrs:未声明的 HTML 属性(自动透传)                │
│  ├── Slots:默认/具名/作用域插槽(内容+数据输入)           │
│  └── Inject:注入祖先组件 Provide 的数据         		  │
│                                                          │
│  【内部黑盒】(组件私有)                                  │
│  ├── 自定义 Hooks:逻辑复用                               │
│  ├── 响应式数据:ref / reactive                          │
│  ├── 模板引用:ref(访问 DOM/子组件)                     │
│  ├── 计算属性:computed(缓存派生值)                     │
│  ├── 生命周期钩子:onMounted / onUpdated 等              |
│  ├── 侦听器:watch / watchEffect(监听数据变化)          │
│  └── 函数定义:function			                     │
│  └── 样式封装:scoped / CSS Modules                      │
│                                                         │
│  【输出】(组件 → 外部)                                  │
│  ├── Expose:暴露方法/属性给父组件(通过 ref 调用)        │
│  ├── Provide:提供数据给后代组件(跨层级)(组件 → 内部)   │
│  ├── Emit:触发事件(子 → 父通信)                        │
│  └── v-model:Props + Emit 的语法糖(双向绑定)           │
└─────────────────────────────────────────────────────────┘

输入部分

1. Props(父传子):显式声明接收的组件属性
xml 复制代码
<!-- 子组件 UserCard.vue -->
<template>
  <div class="user-card">
    <h3>{{ user.name }}</h3>
    <p>年龄: {{ user.age }}</p>
    <p v-if="showEmail">邮箱: {{ user.email }}</p>
    <button v-if="showDelete" @click="$emit('delete')">删除</button>
  </div>
</template>

<script setup>
import { defineProps, withDefaults } from "vue"
// 方式1:数组形式(简单但不推荐)
defineProps(['user', 'showDelete'])

// 方式2:对象形式(推荐,可定义类型和默认值)
defineProps({
  user: {
    type: Object,
    required: true,
    // default: () => ({ name: 'john' }),  // 在写了required: true时,再写default是错误的写法,Vue会警告
  },
  showDelete: {
    type: Boolean,
    default: false
  },
  showEmail: {
    type: Boolean,
    default: true
  }
});
// 方式3:withDefaults(推荐,可定义类型和 为"可选的 prop"提供默认值。默认值。需此时<script lang="ts">)
interface User {
    name: string;
    age?: number;
}
interface PropsType {
	user?: User;
    showDelete?: boolean;
    showEmail: boolean;
}
withDefaults(defineProps<PropsType>(), {
    user: () => ({ name: 'Unknown', age: 0 }), // 在使用 withDefaults 时,默认值的可变引用类型 (如数组或对象) 应该在函数中进行包装,以避免意外修改和外部副作用。
    showDelete: false,
    // showEmail: true  //showEmail为必填,不写default值,同上
})
</script>

<!-- 父组件中使用 -->
<template>
  <UserCard
    :user="currentUser" 
    :show-delete="true"
  />
</template>
ini 复制代码
props关键点:
1、单向数据流。从父组件流向子组件,为避免数据流向混乱,不允许在子组件中修改props。如果props中存在对象、数组等引用类型,在子组件中虽然可以更改其属性值, 但是应该【有意识的避免这种操作】。
** 2、子组件里的 props 对象,本身就是响应式的(浅响应式,shallowReactive),并且正是通过 set 触发子组件的重新渲染。 
3、defineProps的几种写法,withDefaults的写法,props的解构写法(会丢失响应性 需要搭配toRef、toRefs,3.5版本以后不会丢失响应性)

4、Props 本身不是独立的响应式源,它是「父组件响应式源数据」在子组件中的「响应式代理引用」。
- 父组件初始化:	【<UserCard :user="currentUser" />】
- 子组件初始化:	【const props = defineProps(['user'])】
	
5、父组件重渲染 → 新 props 数据通过赋值写入子组件的响应式 props 对象 → 响应式系统检测到变化 → 触发子组件重新渲染。
	- (子组件自己没有能力去"获取"到props新值。它只能被动地等待父组件在未来某次重渲染时,把此刻的新值传给它。
		只有当父组件因为某些原因重渲染时,才会把最新props传给子组件,更新子组件的 props。)
	- 这里<UserCard :user="{name: 'john'}" />, 父组件重新渲染,子组件也一定会重新渲染。因为每次父渲染都会创建新字面量,子组件 Props 永远变化,导致不必要的重渲染。(「高频坑」)
		
	**【父渲染才传新 Props,子渲染看 Props 变没变。】**
scss 复制代码
// 父组件重渲染后,子组件会不会重渲染,取决于 Vue 对 Props 做的「浅比较」结果
// Vue 的判断流程(伪代码)

// 父组件重渲染时,对子组件做 update
function updateChildComponent(child, newProps) {
  // 浅比较只是优化判断
  if (hasPropsChanged(child.props, newProps)) {
    // 赋值给响应式 props 对象,这个操作会触发子组件的 render effect
    child.props.name = newProps.name
    child.props.count = newProps.count
    // 子组件的重渲染由响应式系统自动完成,这里不需要显式调用
  }
  // 如果没变,跳过赋值,子组件响应式系统不会触发,因此不重渲染
}
javascript 复制代码
// <UserCard :user="{name: 'john'}" />, 父组件重新渲染,子组件也一定会重新渲染。因为每次父渲染都会创建新字面量,子组件 Props 永远变化,导致不必要的重渲染。(「高频坑」)

// 实测 类似于{name: 'john'}、{count: 1}这种「完全静态的对象字面量」是不会触发渲染的。

----------*****--------------------
// 如果对象字面量中包裹了响应式数据 {count: countRef},会触发组件渲染
// 只要对象字面量内引用了任何组件上下文的变量,哪怕它是非响应式的常量,都不会被提升。
	// 如{count: myCount}不会静态提升。(此处myCount是一个普通const变量)
----------*****--------------------

// 【结论】:在 Vue 3 中,只有那些【非纯静态】的对象字面量(包含变量)才会导致每次创建新对象,引发不必要的子组件重渲染。【只要对象字面量引用了任何组件上下文的变量,或者你手动每次替换对象,子组件就会每次都重渲染。】

<!-- 父组件 -->
<template>
  <!-- ❌ 这里的 {count: 1} 是完全静态的,没有响应式变量 -->
  <Son :countt="{count: 1}" />
</template>

// ✅ 静态提升:把完全静态的对象字面量提升到 render 函数外面,只创建一次
const _hoisted_1 = { count: 1 }

function render(_ctx, _cache) {
  // 父组件重渲染时,永远复用 _hoisted_1,引用地址没变
  return h(Son, { countt: _hoisted_1 })
}
2. 透传Attribute($attrs)

封装通用组件、组件库、高阶组件的核心利器。

在子组件中没有通过defineProps``defineEmits显式声明的props属性、v-on事件,但是在父组件引用子组件的时候通过模板语法往下传递了的属性,都会被"装进" $attrs中,在子组件中被访问到。

xml 复制代码
<template>
	<MyButton :class="styleGivenByFather" @click="fatherClickFunc" ></MyButton>
</template>

// MyButton
 <!-- 1. 属性默认自动添加到子组件根元素上 -->
 <!-- 【class, style】特殊,会直接添加到后面。 -->
 <!-- 【type, placeholder, id等】普通属性,会直接覆盖子组件的。 -->
<template>
	<button class="btn-style styleGivenByFather">点击</button>
</template>

 <!-- 2. 事件同时触发 先执行sonClickFunc,后执行fatherClickFunc -->
<template>
	<button class="btn-style" @click="sonClickFunc">点击</button>
</template>

 <!-- 3. 非单根组件,且存在$attrs,不会默认继承$attrs,且vue会报警告。需要手动自定义绑定$attrs -->
 <!-- 单根组件,根元素仍然会自动继承 $attrs,哪怕内部某个元素已经用了 v-bind="$attrs" -->
<template>
	<p>显示文本</p>
	<button class="btn-style" @click="sonClickFunc" v-bind="$attrs">点击</button>
</template>

 <!-- 4. $attrs会往后代组件 透传, grandSon组件中可以通过$attrs访问。 -->
 <!-- 【前提】当前组件没有"消费"某个Attribute。
				- 即没有声明为props、emits,
				- 没有v-bind="$attrs"绑定到元素上,
				- 没有defineOptions({inheritAttrs: false; }) -->
<template>
	<grandSon />
</template>

 <!-- 5. 使用defineOptions({ inheritAttrs: false; }) 关自动继承$attrs -->
<template>
	<grandSon />
</template>
<script setup>
import { defineOptions } from "vue"

defineOptions({ inheritAttrs: false; })
</script>

 <!-- 6. 使用useAttrs() 在<script>中使用透传的 Attribute 数据 -->
 <!-- useAttrs()、:type="$attrs.type" 都只属于读取$attrs,不属于消费$attrs。
		读取操作都不是消费,只有 v-bind="$attrs" 或 defineProps["type"] 属性才是消费 -->
<template>
	<grandSon />
</template>
<script setup>
import { useAttrs } from "vue"

const attrs = useAttrs();
</script>

 <!-- 7. $attrs对象中的属性不具备响应性(vue出于性能考虑) -->
 <!-- 实际使用过程中,vue3.5+版本,$attrs更类似于一个proxy对象,内部属性(仅属性为响应式时)的变化能够引起组件重渲染 -->
 <!-- watch无法监听attr,但是能够监听到其中响应式属性 attr.xxx的变化 -->
3. 插槽(Slot)
xml 复制代码
<!-- 子组件 Modal.vue -->
<template>
  <div class="modal">
     <!-- 通过$slot 与 v-if来决定是否渲染 -->
    <div v-if="$slots.header" class="modal-header">
      <slot name="header">
        默认标题 <!-- 默认内容 -->
      </slot>
    </div>
    
    <div class="modal-body">
      <slot></slot> <!-- 默认插槽 -->
    </div>
    
    <div class="modal-footer">
      <slot name="footer" :text="greetingMessage" :count="1" :close="handleClose"> <!-- 作用域插槽 -->
        <button @click="$emit('close')">关闭</button>
      </slot>
    </div>
  </div>
</template>

<!-- 父组件使用 -->
<template>
  <Modal @close="isModalOpen = false">
    <!-- 1. 具名插槽 -->
    <!-- v-slot:header、v-slot:default。 -->
    <!-- v-slot简写为#   #header、#default。 -->
    <template #header>
      <h2>自定义标题</h2>
    </template>
    
    <!-- 2. 默认插槽。 所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容 -->
    <p>这是模态框的内容...</p>
	<p>And another one.</p>
    <template #default> <!-- 不会合并!显式 #default 会完全覆盖隐式默认插槽内容 -->
      <p>自定义标题<p>
    </template>

    <!-- 3.动态插槽名。v-slot:[dynamicSlotName] -->
    <template #[dynamicSlotName]>
      <p>自定义标题<p>
    </template>

    <!--4. 插槽作用域:
			插槽内容无法访问子组件的数据。
			Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。 -->
    
    <!-- 5. 作用域插槽 - 具名插槽 -->
    <!-- 【注意】插槽上的 【name】 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。
				因此最终 这里的从slotProps获得数据是 { close, text, count } -->
    <template #footer="{ close, text, count }">
      <button @click="save">{{ text }}</button>
      <button @click="close">{{ count }}</button>
    </template>
  </Modal>

 <!-- 6. 作用域插槽 - 默认写法 -->
 <!-- 如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template> 标签。(如上,#default) -->
 <!-- 如果存在具名插槽,不能够在组件上直接写v-slot="slotProps"来获取默认插槽数据,将导致编译错误 -->
 <!-- v-slot="slotProps" 默认绑定的是默认插槽的属性,不是 footer 的 -->
  <Modal v-slot="slotProps">
    {{ slotProps.dddd }} {{ slotProps.aaa }}
  </Modal>
</template>

<!-- 7. 作用域的透传(多层组件) -->
<!-- 规则:如果需要把作用域从孙组件透传给父组件,子组件必须手动接收并重新传递。-->
<!-- 子组件(中间层) -->
<template>
  <GrandSon>
    <!-- ✅ 手动接收孙组件的作用域,再传递给父组件 -->
    <template #default="{ grandSonItem }">
      <slot name="item" :item="grandSonItem"></slot>
    </template>
  </GrandSon>
</template>
4. Provide/Inject(跨层级传递)

数据在跨层级组件传递时,如果通过props一层一层的传递会很麻烦。

provideinject 可以帮助我们解决这一问题 1。一个父组件相对于其所有的后代组件,会作为依赖提供者 。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。

xml 复制代码
<!-- 祖先组件 -->
<script setup>
import { provide, ref, readonly } from 'vue'

const theme = ref('dark')

// 提供数据给所有后代组件, readonly确保提供的数据不能被注入方的组件更改
provide('theme', readonly(theme))
provide('theme2', "sss")

// 提供方法
provide('changeTheme', (newTheme) => {
  theme.value = newTheme
})
</script>

<!-- 后代组件(任何层级) -->
<script setup>
import { inject } from 'vue'

// 注入数据,注入的是theme ref对象整个,不会自动解包
const theme = inject('theme')
const changeTheme = inject('changeTheme')

// 使用默认值
const config = inject('config', { color: 'blue' })
</script>

输出部分

1. 暴露组件方法(Expose)
xml 复制代码
<!-- 子组件 Counter.vue -->
<script setup>
import { ref } from 'vue'

const count = ref(0)

const increment = () => {
  count.value++
}

const reset = () => {
  count.value = 0
}

// 暴露方法给父组件
defineExpose({
  increment,
  reset,
  count
})
</script>

<!-- 父组件 -->
<template>
  <Counter ref="counterRef" />
  <button @click="resetCounter">重置子组件</button>
</template>

<script setup>
import { ref } from 'vue'
import Counter from './Counter.vue'

const counterRef = ref()

const resetCounter = () => {
  // 调用子组件暴露的方法
  counterRef.value.reset()
  console.log('当前计数:', counterRef.value.count)
}
</script>
2. 触发事件(Emit)
xml 复制代码
<!-- 子组件 -->
<script setup lang="ts">
interface UpdateParam {
    id: number
    name: string
}
    
 // 写法一、运行时
// const emit = defineEmits(['update', 'delete', 'custom-event'])

// 写法二、基于选项
const emit = defineEmits({
    delete:() => {
		// 返回 'true' 或者 'false',表明验证听过或失败
        return true   // 必须显式 return 布尔值
    },
    update:(value: UpdateParam) => {
        // 返回 'true' 或者 'false',表明验证听过或失败
    	return value.id > 0   // 举例:返回 true/false
    },
    customEvent: (param:string) => {
        // 返回 'true' 或者 'false',表明验证听过或失败
    	return param.length > 0
    },
    add: (id: number, name: string, flag: boolean) => {
        // 返回 'true' 或者 'false',表明验证听过或失败
    	return Boolean(name)
    }
})

// 写法三、基于类型
const emit = defineEmits<{
    (e: 'update', obj:UpdateParam): void,
    (e: 'delete'): void,
    (e: 'custom-event', param:string): void, // 在 <script setup> 中,emit 的事件名推荐使用 camelCase,父组件监听时使用 kebab-case。所以在这里其实不推荐携程custom-event,而是customEvent
    (e: 'add', id: number, name: string, flag: boolean): void
}>({
	// 注意:纯类型声明无法做运行时校验,它只提供编译时的类型提示。如果你需要校验数据格式,需配合写法二的验证对象。【如果需要运行时校验】
    delete:() => {
		// 返回 'true' 或者 'false',表明验证听过或失败
        return true   // 必须显式 return 布尔值
    },
})
    
// 写法四、3.+ 可选的、更简洁的语法
// 同样没有运行时校验能力,仅用于类型推导。
const emit = defineEmits<{
    update: [param: UpdateParam],
    delete: [],
    customEvent: [param: string]
}>()

const handleClick = () => {
  // 触发无参数事件
  emit('delete')
  
  // 触发带参数事件
  emit('update', { id: 1, name: '新名称' })
  
  // 触发自定义事件
  emit('custom-event', '自定义数据')
}
</script>

内部机制

生命周期钩子
xml 复制代码
<script setup>
import { 
  onBeforeMount, // 组件挂载前
  onMounted,     // 组件挂载后
  onBeforeUpdate, // 组件更新前
  onUpdated,      // 组件更新后
  onBeforeUnmount, // 组件卸载前
  onUnmounted,     // 组件卸载后
  onErrorCaptured  // 捕获子组件错误
} from 'vue'

// 此时组件尚未渲染到DOM
// 访问不到DOM,可以修改数据
onBeforeMount(() => {
  console.log('组件即将挂载')
})

// 此时组件已渲染到DOM
// 可以:访问DOM、发起网络请求、设置定时器
onMounted(() => {
  console.log('组件已挂载')
})

// 数据变化,即将重新渲染
// 可以:获取更新前的DOM状态
onBeforeUpdate(() => {
  console.log('数据即将更新')
})

// 组件已重新渲染
// 可以:操作更新后的DOM
// 注意:避免在此修改数据,可能导致无限循环!
onUpdated(() => {
  console.log('数据已更新')
})

// 组件即将被销毁
// 可以:清除定时器、取消事件监听、取消订阅、清理资源
onBeforeUnmount(() => {
  console.log('组件即将卸载')
})

// 组件已被销毁
// 可以:最终的清理工作
onUnmounted(() => {
  console.log('组件已卸载')
})
    
onErrorCaptured((err, instance, info) => {
  console.log('捕获到子组件错误:', err)
  return false // 阻止错误继续向上传播
})
</script>

(----------------------------------todo: 添加一个组件加载完成的整个生命周期过程,父-子-孙三代组件间生命周期的先后顺序,响应式状态改变同组件更新加载之间的关系--------------)

侦听器(Watch)
xml 复制代码
<script setup>
import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const user = ref({ name: '张三', age: 25 })

// 1. 监听单个ref
watch(count, (newValue, oldValue) => {
  console.log(`count从${oldValue}变为${newValue}`)
})

// 2. 监听对象中的某个属性
watch(
  () => user.value.age,
  (newAge, oldAge) => {
    console.log(`年龄从${oldAge}变为${newAge}`)
  }
)

// 3. 监听多个数据源
watch([count, () => user.value.name], ([newCount, newName]) => {
  console.log(`count: ${newCount}, name: ${newName}`)
})
    
// 4. 深度监听对象
watch(
  user,
  (newUser, oldUser) => {
    console.log('用户信息变化了', newUser)
  },
  { deep: true }  // 深度监听
)
    
// 5. 立即执行
watch(
  count,
  (newValue) => {
    console.log('立即执行:', newValue)
  },
  { immediate: true }
)
// 6. watchEffect:自动追踪依赖
watchEffect(() => {
  console.log(`count: ${count.value}, age: ${user.value.age}`)
  // 自动追踪count和user.value.age
})
    
// 7. watchPostEffect - DOM更新后执行
watchPostEffect(() => {
  // 在DOM更新后执行
  const el = document.getElementById('myElement')
  console.log('DOM已更新', el?.offsetHeight)
})
    
// 8. 停止监听
const stop = watchEffect(() => { /* ... */ })
// 当不再需要时
stop()
</script>

(----------------------------------todo: watch的变量发生变化触发的组件重新渲染??--------------------------------------------------)

五、新人常见误区 & 经典错误

渲染性能相关误区

误区1:每次数据变化都会导致整个组件重渲染

xml 复制代码
<template>
  <div>{{ count }}</div>
  <button @click="changeUnused">修改未使用的数据</button>
</template>

<script setup>
const count = ref(0)
const unusedData = ref(0) // 不在模板中使用

const changeUnused = () => {
  unusedData.value++ // ❌ 不会触发重渲染
  count.value++      // ✅ 会触发重渲染
}
</script>

误区2:所有响应式数据修改都会立即触发更新

ini 复制代码
<script setup>
const count = ref(0)

// Vue会批量处理更新
const updateMultiple = () => {
  count.value = 1
  count.value = 2
  count.value = 3
  // 实际上只会触发一次重渲染
}
</script>

误区3:在循环中大量修改响应式数据会导致性能问题

ini 复制代码
<script setup>
const bigArray = ref([])

const badPractice = () => {
  for (let i = 0; i < 10000; i++) {
    bigArray.value.push(i) // ❌ 每次push都会触发响应式更新
  }
}

const goodPractice = () => {
  const tempArray = []
  for (let i = 0; i < 10000; i++) {
    tempArray.push(i)
  }
  bigArray.value = tempArray // ✅ 一次性更新
}
</script>

经典错误

  1. 错误:直接修改props
xml 复制代码
<!-- ❌ 错误做法 -->
<script setup>
const props = defineProps(['user'])

const updateUser = () => {
  props.user.name = '新名字'  // 错误!props是只读的
}
</script>

<!-- ✅ 正确做法 -->
<script setup>
const props = defineProps(['user'])
const emit = defineEmits(['update:user'])

const updateUser = () => {
  // 创建新对象,触发更新
  const newUser = { ...props.user, name: '新名字' }
  emit('update:user', newUser)
}
</script>
  1. 错误:忘记ref的.value
xml 复制代码
<script setup>
const count = ref(0)

// ❌ 错误
const increment = () => {
  count++  // 应该用 count.value++
}

// ✅ 正确
const increment = () => {
  count.value++
}
</script>
  1. 错误:在setup中直接访问DOM
xml 复制代码
<script setup>
import { onMounted } from 'vue'

// ❌ 错误:setup阶段DOM还未渲染
const element = document.getElementById('myElement')

// ✅ 正确:在onMounted中访问
onMounted(() => {
  const element = document.getElementById('myElement')
})
</script>
  1. 错误:循环中不使用key
xml 复制代码
<!-- ❌ 错误:可能导致性能问题和状态混乱 -->
<div v-for="item in items">
  {{ item.name }}
</div>

<!-- ✅ 正确:始终使用唯一的key -->
<div v-for="item in items" :key="item.id">
  {{ item.name }}
</div>
  1. 错误:过度使用响应式数据
php 复制代码
<script setup>
// ❌ 过度使用:所有数据都响应式
const config = reactive({
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
})

// ✅ 优化:静态数据不需要响应式
const API_CONFIG = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
}

// 只有需要变化的数据才用响应式
const requestState = reactive({
  loading: false,
  error: null,
  data: null
})
</script>

六. 最佳实践

组件通信方式选择

场景 推荐方式 示例
父 → 子 Props <Child :title="title" />
子 → 父 Events @update="handleUpdate"
兄弟组件 状态提升 通过共同的父组件传递
跨层级 Provide/Inject provide('theme', theme)
任意组件 Pinia const store = useStore()
父访问子 Template Refs ref="childRef"
复杂表单 v-model + emits :modelValue="value" @update:modelValue="..."

优化组件性能

xml 复制代码
<script setup>
import { ref, computed, watch } from 'vue'

// 1. 使用计算属性缓存计算结果
const expensiveResult = computed(() => {
  return someArray.value.reduce((sum, item) => {
    // 复杂计算...
    return sum + item.value
  }, 0)
})

// 2. 避免不必要的响应式
const staticConfig = { // 不需要响应式
  apiUrl: 'https://api.example.com',
  timeout: 5000
}

// 3. 合理使用shallowRef
const largeObject = shallowRef({ /* 大对象 */ })
// 如果只需要替换整个对象,不需要深度响应

// 4. 组件懒加载
import { defineAsyncComponent } from 'vue'
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// 5. 使用v-show替代v-if(当频繁切换时)
<Modal v-show="isModalOpen" /> <!-- 只切换display -->
<Modal v-if="isModalOpen" />   <!-- 会销毁和重建组件 -->

// 6. 使用KeepAlive缓存组件状态
<KeepAlive>
  <component :is="currentComponent" />
</KeepAlive>
</script>

vue SFC组件书写规范

<script>标签内结构顺序

在Vue3的组合式风格中,<script>标签内的内容代码的组织顺序应遵循 "从外到内、从静态到动态、从输入到输出" 的逻辑,以提高代码的可读性和可维护性。应该按照以下方式安排:

  • 外部依赖引入 。(import语句, 依次为:组件库 / 工具库引入,@符号 src 绝对路径全局引入,./ 当前组件下相对路径引入 )
  • TypeScript类型定义
  • 组件输入defineProps、defineEmits、defineOptions、defineAsyncComponent(动态组件)等编译宏。
  • 上下文属性 :隐式上下文:useAttrs(如果需要)、useSlots(如果需要)。跨层级通信:provide /inject
  • 常量定义:(constants:组件内不会发生改变的常量、枚举值,但可能依赖于导入的数据,如URL、API、配置项等)。
  • 组合式 API 状态 。(Compontional,依次为:useStore、useRoute、useRouter、useCssModule、ref、shallowRef、markRaw、reactive)。
  • 计算属性computed
  • 生命周期钩子 (按顺序组织:onMounted、onUpdated、onUnmounted等)。
  • 侦听器watch、watchEffect)。
  • 自定义函数
  • 自定义指令(如果需要)。
  • defineExpose(如果需要)。
typescript 复制代码
<script setup>
// 1. 依赖引入
// 组件库、工具库引入
import { defineProps, defineEmit, defineAsyncComponent, useAttrs, provide, ref, shallowRef, markRaw, reactive, toRefs, createApp,  onMounted } from 'vue';
import { useRouter } from "vue-router";
import { ElNotification, ElForm } from "element-plus";
import { get } from "lodash-es";
// @符号(src)绝对路径全局引入
import { useGlobalStore } from "@/store";
// 类型定义(优先使用 `import type`)
import type { User } from '@/types/user'
// ./ 当前组件下项目路径引入
import UserDialog from "./components/UserDialog.vue";
    
// 2. TypeScript类型定义
interface Props {
  userId: number
  theme?: 'light' | 'dark'
}
type Emits = {
  (e: 'update:theme', value: 'light' | 'dark'): void
  (e: 'error', message: string): void
}
type FetchStatus = 'idle' | 'loading' | 'success' | 'error'

// 3. 组件输入
const props = defineProps<Props>(); // 或使用useAttr()获取父组件传递过来的属性及属性值
const emit = defineEmit<Emits>();
defineOptions({
  name: 'UserProfile',
  inheritAttrs: false
})
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue')); // 动态组件
    
// 上下文依赖
const attrs = useAttrs();
const slots = useSlots();
const injectedVal = inject('key', defaultVal);
    
// 5. 常量定义
const API_ENDPOINT = '/api/user'
const MAX_RETRIES = 3
const STATUS = {
  IDLE: 'idle',
  LOADING: 'loading'
} as const

// 6. 组合式API 状态(依次useStore、useRoute、useRouter、useCssModule、ref、shallowRef、markRaw、reactive)
const globalStore = useGlobalStore();
const route = useRoute();
const router = useRouter();
const style = useCssModule('scopedStyle'); // css相关
const refState = ref({ / define ref state / });
    
const state = reactive({ / define reactive state / });
const heavyObject = markRaw({ ... }); // 避免被代理
const list = shallowRef([]); // 浅层响应
// 7. 计算属性
const computedValue = computed(() => / compute a value /);

// 8. 生命周期钩子
onMounted(() => {/ run when component is mounted /});
onUpdated(() => console.log('组件更新'));
onUnmounted(() => clearTimer());
onActivated(() => console.log('组件激活')); // KeepAlive 相关钩子
onDeactivated(() => console.log('组件停用'));
onServerPrefetch(async () => await fetchData()) // SSR 钩子(如需要)
    
// 9. 侦听器 watch
watch(/ watch a value /);
watchEffect(/ watch a value /);    

// 10. 自定义函数 
const increment = () => {/ use props, state, computedValue, watcher, etc. /}
function getString() {} // 若getString()在ref初始化、watch {immedate: true}中使用到,则可利用函数的变量声明提升来规避使用先于声明的问题

// 11. 自定义指令
const vFocus = {
  mounted: (el) => el.focus()
}

// 12. provide、defineExopse(如果需要)
provide('key', someVal);
defineExopse({/ expose consts, ref, reacive, function, etc. /})
</script>

: 当const 函数名 = () => {}定义的函数在ref初始化定义watch {immediate: true}中被引用(即被初始化依赖)时,函数定义要坚持使用const的方式定义(不修改为function 函数名(){}的方式定义),则可不按规范定义应将自定义函数写在最后,可提前到ref变量定义watch {immediate: true}之前。

关键原则:

  • 初始化依赖优先 :若函数被ref的初始值、watch的立即回调直接依赖,则需强制提前函数定义
  • 无依赖函数保持一致:不涉及初始化逻辑的函数仍然放在最后。

学习路线建议

  1. 第一阶段:基础

    • 模板语法、数据绑定
    • 响应式数据 (ref, reactive)
    • 条件渲染、列表渲染
  2. 第二阶段:组件

    • Props/Events
    • 插槽 (Slots)
    • 组件生命周期
  3. 第三阶段:高级特性

    • Provide/Inject
    • 自定义指令
    • 过渡动画
  4. 第四阶段:生态系统

    • Vue Router (路由)
    • Pinia (状态管理)
    • 组件库使用 (Element Plus, Ant Design Vue等)

总结

Vue的强大之处在于它的渐进式设计易学性。你可以:

  1. 从一个简单的计数器开始
  2. 逐渐添加组件和路由
  3. 最后引入状态管理

记住这些核心原则:

  • 数据驱动视图:关注数据变化,而不是DOM操作
  • 组件化思维:将UI拆分为独立、可复用的组件
  • 单向数据流:Props向下,Events向上
  • 声明式编程:告诉Vue"你想要什么",而不是"如何做"

从简单的组件开始,慢慢构建更复杂的应用。遇到问题时,记住 Vue的官方文档 和社区是你最好的朋友。

最后的小提示:不要试图一次性掌握所有概念。先从最常用的特性开始(模板语法、响应式数据、组件通信),在实践中逐步深入学习其他高级特性。

相关推荐
弱鸡前端1 小时前
纯前端实现pdf从生成到下载
前端
明月_清风1 小时前
TanStack + Cloudflare 边缘实战:从 0 到 1 构建全栈应用
前端·全栈
东风破_1 小时前
你天天用的 Python dict,90% 的人没搞懂这三个坑
前端
前端Hardy1 小时前
21.8 万周下载!这个 React 表格组件,10 行代码就能跑起来
前端·javascript·后端
lichenyang4531 小时前
# 鸿蒙 ArkTS 聊天 Demo 功能复盘:真实 SSE、多轮会话、暂停输出、历史记录与防崩溃修复 > 项目:`harmony-chat-demo`
前端
陈_杨1 小时前
鸿蒙APP开发-带你走进胶片录的拍摄记录管理
前端·javascript
陈_杨1 小时前
鸿蒙APP开发-带你走进胶片录的相机控制
前端·javascript
陈_杨1 小时前
鸿蒙APP开发-带你走进节流战的Canvas图表
前端·javascript
陈_杨1 小时前
鸿蒙APP开发-带你走进光绘记的拍摄规划
前端·javascript