Vue3 快速上手学习笔记

基于《Vue3快速上手》整理,适合初学者的 Vue3 入门指南

目标:从零开始掌握 Vue3 核心语法,能独立搭建项目


目录

  1. Vue3 简介
  2. 创建 Vue3 工程
  3. Vue3 核心语法
  4. 路由
  5. Pinia 状态管理
  6. 组件通信

一、Vue3 简介

1.1 发布背景

  • 发布时间:2020年9月18日
  • 版本号:3.0,代号 “One Piece”
  • 开发规模:4800+ 次提交、40+ 个 RFC、600+ 次 PR、300+ 贡献者

1.2 主要优势

特性 说明
性能提升 打包大小减少 41%,初次渲染快 55%,更新渲染快 133%,内存减少 54%
源码升级 使用 Proxy 代替 defineProperty 实现响应式;重写虚拟 DOM 和 Tree-Shaking
拥抱 TypeScript 更好的 TS 支持
Composition API 全新的组合式 API,代码组织更灵活

1.3 新特性概览

Composition API(组合式 API)

  • setup
  • refreactive
  • computedwatch

新的内置组件

  • Fragment - 片段
  • Teleport - 传送
  • Suspense - 异步加载

其他变化

  • 新的生命周期钩子
  • data 选项必须声明为函数
  • 移除 keyCode 作为 v-on 修饰符

二、创建 Vue3 工程

2.1 基于 Vite 创建(推荐)

Vite 是新一代前端构建工具,优势:

  • 轻量快速的热重载(HMR)
  • 对 TypeScript、JSX、CSS 开箱即用
  • 真正的按需编译
# 创建命令
npm create vue@latest

# 具体配置选项
# √ Project name: vue3_test
# √ Add TypeScript? Yes
# √ Add JSX Support? No
# √ Add Vue Router? No
# √ Add Pinia? No
# √ Add Vitest? No
# √ Add ESLint? Yes
# √ Add Prettier? No

2.2 项目结构特点

项目根目录
├── index.html          # 入口文件(在项目最外层)
├── src/
│   ├── main.ts         # 应用入口
│   ├── App.vue         # 根组件
│   └── components/     # 组件目录
└── vite.config.ts      # Vite 配置

2.3 创建应用实例

Vue3 通过 createApp 函数创建应用:

// main.ts
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

2.4 Vue3 兼容 Vue2 语法

Vue3 向下兼容 Vue2 语法,且模板中可以没有根标签

<!-- Vue3 模板可以没有根标签 -->
<template>
  <h1>标题</h1>
  <p>段落</p>
</template>

三、Vue3 核心语法

3.1 Options API vs Composition API

Options API(Vue2 风格)

<script>
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  computed: {
    double() {
      return this.count * 2
    }
  }
}
</script>

缺点:数据、方法、计算属性分散在不同选项中,维护困难。

Composition(组合式) API(Vue3 推荐)

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

// 数据
const count = ref(0)

// 计算属性
const double = computed(() => count.value * 2)

// 方法
function increment() {
  count.value++
}
</script>

优点:相关功能的代码组织在一起,逻辑更清晰,便于复用。

3.2 setup 函数

基本用法

setup 是 Vue3 的核心配置项,是 Composition API 的"舞台":

<script>
export default {
  name: 'Person',
  setup() {
    // 数据
    let name = '张三'
    let age = 18
    
    // 方法
    function showInfo() {
      alert(`我叫${name},今年${age}岁`)
    }
    
    // 返回的对象,模板中可以直接使用
    return { name, age, showInfo }
  }
}
</script>
setup 特点
  1. 返回对象:对象中的属性和方法可直接在模板中使用
  2. this 为 undefined:setup 中不能通过 this 访问组件实例
  3. 执行时机:在 beforeCreate 之前执行
setup 语法糖(推荐写法)
<script setup lang="ts" name="Person">
// 直接写代码,无需 return
import { ref } from 'vue'

const name = ref('张三')
const age = ref(18)

function showInfo() {
  alert(`我叫${name.value},今年${age.value}岁`)
}
</script>

语法糖优势

  • 无需手动 return
  • 无需写 export default
  • 更简洁的代码

3.3 ref - 创建响应式数据(基本类型)

作用

定义响应式变量,适用于基本类型数据。

语法
import { ref } from 'vue'

let xxx = ref(初始值)
示例
<template>
  <div class="person">
    <h2>姓名:{{ name }}</h2>
    <h2>年龄:{{ age }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">年龄+1</button>
  </div>
</template>

<script setup lang="ts" name="Person">
import { ref } from 'vue'

// 定义响应式数据
let name = ref('张三')
let age = ref(18)

// 方法
function changeName() {
  // JS 中操作 ref 需要 .value
  name.value = '李四'
}

function changeAge() {
  age.value += 1
}
</script>
重要注意点
场景 写法
模板中使用 {{ name }}(不需要 .value)
JS 中读取/修改 name.value(需要 .value)
ref 返回值 RefImpl 实例对象,.value 才是响应式的

3.4 reactive - 创建响应式数据(对象类型)

作用

定义响应式对象,适用于对象类型数据(对象、数组)。

语法
import { reactive } from 'vue'

let 响应式对象 = reactive(源对象)
示例
<template>
  <div class="person">
    <h2>汽车:{{ car.brand }} - {{ car.price }}万</h2>
    <h2>游戏列表:</h2>
    <ul>
      <li v-for="g in games" :key="g.id">{{ g.name }}</li>
    </ul>
    <button @click="changeCar">修改汽车</button>
    <button @click="changeGame">修改游戏</button>
  </div>
</template>

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

// 对象类型数据
let car = reactive({ brand: '奔驰', price: 100 })

// 数组类型数据
let games = reactive([
  { id: '01', name: '英雄联盟' },
  { id: '02', name: '王者荣耀' },
  { id: '03', name: '原神' }
])

function changeCar() {
  car.price += 10  // 直接修改,无需 .value
}

function changeGame() {
  games[0].name = '流星蝴蝶剑'
}
</script>
特点
  • reactive 定义的数据是深层次响应式的
  • 修改时不需要 .value
  • 重新分配对象会失去响应式(可用 Object.assign 解决)

3.5 ref vs reactive 对比

特性 ref reactive
数据类型 基本类型、对象类型 仅对象类型
访问方式 需要 .value 直接访问
深层响应 对象内部自动用 reactive 自动深层响应
重新赋值 可以 会失去响应式

使用原则

  1. 基本类型 → 用 ref
  2. 对象类型,层级不深 → refreactive 都可以
  3. 对象类型,层级较深 → 推荐 reactive

3.6 toRefs 与 toRef

作用:将响应式对象的属性转换为 ref,保持响应式。

<script setup lang="ts" name="Person">
import { reactive, toRefs, toRef } from 'vue'

let person = reactive({
  name: '张三',
  age: 18,
  gender: '男'
})

// toRefs:批量转换
let { name, gender } = toRefs(person)

// toRef:单个转换
let age = toRef(person, 'age')

function changeName() {
  name.value += '~'  // 修改会同步到 person
}
</script>

3.7 computed 计算属性

作用:根据已有数据计算出新数据。

<template>
  <div>
    姓:<input type="text" v-model="firstName"> <br>
    名:<input type="text" v-model="lastName"> <br>
    全名:<span>{{ fullName }}</span> <br>
    <button @click="changeFullName">修改全名</button>
  </div>
</template>

<script setup lang="ts" name="App">
import { ref, computed } from 'vue'

let firstName = ref('zhang')
let lastName = ref('san')

// 计算属性 - 可读可写
let fullName = computed({
  get() {
    return firstName.value + '-' + lastName.value
  },
  set(val) {
    const [first, last] = val.split('-')
    firstName.value = first
    lastName.value = last
  }
})

function changeFullName() {
  fullName.value = 'li-si'
}
</script>

3.8 watch 监视

作用:监视数据变化。

情况一:监视 ref 定义的基本类型
import { ref, watch } from 'vue'

let sum = ref(0)

// 直接写数据名
const stopWatch = watch(sum, (newValue, oldValue) => {
  console.log('sum变化了', newValue, oldValue)
  if (newValue >= 10) {
    stopWatch()  // 停止监视
  }
})
情况二:监视 ref 定义的对象类型
let person = ref({
  name: '张三',
  age: 18
})

// 需要开启深度监视
watch(person, (newValue, oldValue) => {
  console.log('person变化了', newValue, oldValue)
}, { deep: true })
情况三:监视 reactive 定义的对象
let person = reactive({ name: '张三', age: 18 })

// 默认开启深度监视
watch(person, (newValue, oldValue) => {
  console.log('person变化了')
})
情况四:监视对象中的某个属性
// 属性是基本类型,写成函数式
watch(() => person.name, (newValue, oldValue) => {
  console.log('name变化了')
})

// 属性是对象类型,推荐也写成函数式
watch(() => person.car, (newValue, oldValue) => {
  console.log('car变化了')
}, { deep: true })
情况五:监视多个数据
watch([() => person.name, person.car], (newValue, oldValue) => {
  console.log('多个数据变化了')
}, { deep: true })

3.9 watchEffect

作用:自动追踪依赖,无需明确指出监视哪些数据。

import { ref, watchEffect } from 'vue'

let temp = ref(0)
let height = ref(0)

// 自动追踪 temp 和 height
const stopWatch = watchEffect(() => {
  if (temp.value >= 50 || height.value >= 20) {
    console.log('联系服务器')
  }
  
  // 条件满足时停止监视
  if (temp.value === 100 || height.value === 50) {
    stopWatch()
  }
})

watch vs watchEffect

  • watch:明确指定监视的数据
  • watchEffect:自动追踪函数中用到的响应式数据

3.10 标签的 ref 属性

作用:获取 DOM 元素或组件实例。

获取 DOM 元素
<template>
  <div>
    <h1 ref="title1">节点飞思</h1>
    <h2 ref="title2">前端</h2>
    <button @click="showLog">打印内容</button>
  </div>
</template>

<script setup lang="ts" name="Person">
import { ref } from 'vue'

let title1 = ref()
let title2 = ref()

function showLog() {
  console.log(title1.value)  // DOM 元素
  console.log(title1.value.innerText)
}
</script>
获取组件实例
<!-- 父组件 -->
<template>
  <Person ref="ren"/>
  <button @click="test">测试</button>
</template>

<script setup lang="ts" name="App">
import Person from './Person.vue'
import { ref } from 'vue'

let ren = ref()

function test() {
  console.log(ren.value.name)
  console.log(ren.value.age)
}
</script>
<!-- 子组件 Person.vue -->
<script setup lang="ts" name="Person">
import { ref, defineExpose } from 'vue'

let name = ref('张三')
let age = ref(18)

// 必须暴露才能被父组件访问
defineExpose({ name, age })
</script>

3.11 props 父传子

<!-- 父组件 -->
<template>
  <Person :list="persons"/>
</template>

<script setup lang="ts" name="App">
import { reactive } from 'vue'
import Person from './Person.vue'

type Persons = Array<{ id: string; name: string; age: number }>

let persons = reactive<Persons>([
  { id: '001', name: '张三', age: 18 },
  { id: '002', name: '李四', age: 19 }
])
</script>
<!-- 子组件 -->
<template>
  <ul>
    <li v-for="item in list" :key="item.id">
      {{ item.name }}--{{ item.age }}
    </li>
  </ul>
</template>

<script setup lang="ts" name="Person">
// 仅接收
// defineProps(['list'])

// 接收 + 限制类型
// defineProps<{ list: Persons }>()

// 接收 + 限制类型 + 默认值
let props = withDefaults(defineProps<{ list?: Persons }>(), {
  list: () => [{ id: '001', name: '小猪佩奇', age: 18 }]
})
</script>

3.12 生命周期

Vue3 生命周期钩子:

阶段 Vue2 Vue3
创建 beforeCreate / created setup
挂载 beforeMount / mounted onBeforeMount / onMounted
更新 beforeUpdate / updated onBeforeUpdate / onUpdated
卸载 beforeDestroy / destroyed onBeforeUnmount / onUnmounted
<script setup lang="ts" name="Person">
import { 
  ref, 
  onBeforeMount, 
  onMounted, 
  onBeforeUpdate, 
  onUpdated,
  onBeforeUnmount,
  onUnmounted 
} from 'vue'

let sum = ref(0)

console.log('setup')

onBeforeMount(() => console.log('挂载之前'))
onMounted(() => console.log('挂载完毕'))
onBeforeUpdate(() => console.log('更新之前'))
onUpdated(() => console.log('更新完毕'))
onBeforeUnmount(() => console.log('卸载之前'))
onUnmounted(() => console.log('卸载完毕'))
</script>

3.13 自定义 Hook

作用:封装复用逻辑,类似于 Vue2 的 mixin。

// hooks/useSum.ts
import { ref, onMounted } from 'vue'

export default function() {
  let sum = ref(0)
  
  const increment = () => { sum.value += 1 }
  const decrement = () => { sum.value -= 1 }
  
  onMounted(() => increment())
  
  return { sum, increment, decrement }
}
// hooks/useDog.ts
import { reactive, onMounted } from 'vue'
import axios from 'axios'

export default function() {
  let dogList = reactive<string[]>([])
  
  async function getDog() {
    try {
      let { data } = await axios.get('https://dog.ceo/api/breeds/image/random')
      dogList.push(data.message)
    } catch (error) {
      console.log(error)
    }
  }
  
  onMounted(() => getDog())
  
  return { dogList, getDog }
}
<!-- 组件中使用 -->
<script setup lang="ts">
import useSum from './hooks/useSum'
import useDog from './hooks/useDog'

let { sum, increment, decrement } = useSum()
let { dogList, getDog } = useDog()
</script>

四、路由

4.1 基本使用

安装 vue-router 4:

npm install vue-router@4

配置路由:

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/pages/Home.vue'
import News from '@/pages/News.vue'
import About from '@/pages/About.vue'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/home', component: Home },
    { path: '/news', component: News },
    { path: '/about', component: About }
  ]
})

export default router

注册路由:

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')

使用路由:

<template>
  <div class="app">
    <!-- 导航区 -->
    <div class="navigate">
      <RouterLink to="/home" active-class="active">首页</RouterLink>
      <RouterLink to="/news" active-class="active">新闻</RouterLink>
      <RouterLink to="/about" active-class="active">关于</RouterLink>
    </div>
    
    <!-- 展示区 -->
    <div class="main-content">
      <RouterView></RouterView>
    </div>
  </div>
</template>

<script setup lang="ts" name="App">
import { RouterLink, RouterView } from 'vue-router'
</script>

4.2 路由器工作模式

模式 优点 缺点
history URL 美观,不带 # 需要服务端配合处理路径
hash 兼容性好,无需服务端处理 URL 带 #,SEO 较差
// history 模式
const router = createRouter({
  history: createWebHistory(),
  routes: [...]
})

// hash 模式
const router = createRouter({
  history: createWebHashHistory(),
  routes: [...]
})

4.3 路由传参

query 参数
<!-- 传递 -->
<RouterLink :to="{
  path: '/news/detail',
  query: {
    id: news.id,
    title: news.title
  }
}">
  {{ news.title }}
</RouterLink>

<!-- 接收 -->
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.query.id)
console.log(route.query.title)
</script>
params 参数
<!-- 传递 -->
<RouterLink :to="{
  name: 'xiang',
  params: {
    id: news.id,
    title: news.title
  }
}">
  {{ news.title }}
</RouterLink>

<!-- 路由规则需要占位 -->
{ name: 'xiang', path: 'detail/:id/:title', component: Detail }

<!-- 接收 -->
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.id)
</script>

4.4 编程式导航

<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

// 跳转
function goHome() {
  router.push('/home')
}

// 带参数跳转
function goDetail(id) {
  router.push({
    name: 'detail',
    params: { id }
  })
}

// 替换当前记录
function replaceHome() {
  router.replace('/home')
}
</script>

五、Pinia 状态管理

Pinia 是什么?
Pinia 是 Vue 官方推荐的新一代状态管理库,用来统一管理 Vue 项目里的全局数据。

作用:

  • 统一管理全局数据:
    多个页面、多个组件都要用的数据(比如用户信息、token、主题、语言),不用层层传递,直接存在 Pinia 里。
  • 数据响应式,一处改、处处更新:
    在任何页面修改数据,所有用到这个数据的地方自动同步更新,不用手动刷新。

5.1 安装与配置

npm install pinia
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

5.2 定义 Store

// store/count.ts
import { defineStore } from 'pinia'

// 选项式写法
export const useCountStore = defineStore('count', {
  // 状态
  state() {
    return {
      sum: 6,
      school: '飞思'
    }
  },
  // 计算
  getters: {
    bigSum: (state) => state.sum * 10,
    upperSchool(): string {
      return this.school.toUpperCase()
    }
  },
  // 函数方法
  actions: {
    increment(value: number) {
      if (this.sum < 10) {
        this.sum += value
      }
    }
  }
})

5.3 组合式写法(推荐

// store/talk.ts
import { defineStore } from 'pinia'
import { reactive } from 'vue'
import axios from 'axios'

export const useTalkStore = defineStore('talk', () => {
  // state
  const talkList = reactive([])
  
  // action
  async function getATalk() {
    let { data } = await axios.get('https://api.uomg.com/api/rand.qinghua')
    talkList.unshift(data.content)
  }
  
  return { talkList, getATalk }
})

5.4 组件中使用

<template>
  <div class="count">
    <h2>当前求和为:{{ sum }}</h2>
    <h2>学校:{{ school }}</h2>
    <h2>大十倍:{{ bigSum }}</h2>
    <button @click="increment(1)">+1</button>
  </div>
</template>

<script setup lang="ts" name="Count">
import { useCountStore } from '@/store/count'
import { storeToRefs } from 'pinia'

const countStore = useCountStore()

// 使用 storeToRefs 保持响应式
const { sum, school, bigSum } = storeToRefs(countStore)

// 方法可以直接解构
const { increment } = countStore
</script>

5.5 修改数据的三种方式

// 方式一:直接修改
countStore.sum = 666

// 方式二:批量修改
countStore.$patch({
  sum: 999,
  school: '飞思'
})

// 方式三:通过 action 修改(推荐)
// store 中定义 action,组件中调用
countStore.increment(10)

六、组件通信

6.1 props(父 ↔ 子)

<!-- 父组件 -->
<template>
  <Child :car="car" :getToy="getToy"/>
</template>

<script setup>
import { ref } from 'vue'
const car = ref('奔驰')
const toy = ref()

function getToy(value) {
  toy.value = value
}
</script>
<!-- 子组件 -->
<template>
  <h4>父给我的车:{{ car }}</h4>
  <button @click="getToy(toy)">玩具给父亲</button>
</template>

<script setup>
import { ref } from 'vue'
const toy = ref('奥特曼')
defineProps(['car', 'getToy'])
</script>

6.2 自定义事件(子 → 父)

<!-- 父组件 -->
<Child @send-toy="handleToy"/>

<script setup>
function handleToy(toy) {
  console.log('收到玩具:', toy)
}
</script>
<!-- 子组件 -->
<script setup>
const emit = defineEmits(['send-toy'])

function send() {
  emit('send-toy', '奥特曼')
}
</script>

6.3 mitt(任意组件间)

npm install mitt
// utils/emitter.ts
import mitt from 'mitt'
const emitter = mitt()
export default emitter
<!-- 接收方 -->
<script setup>
import emitter from '@/utils/emitter'
import { onUnmounted } from 'vue'

emitter.on('send-toy', (value) => {
  console.log('收到:', value)
})

onUnmounted(() => {
  emitter.off('send-toy')
})
</script>
<!-- 发送方 -->
<script setup>
import emitter from '@/utils/emitter'

function send() {
  emitter.emit('send-toy', '奥特曼')
}
</script>

6.4 v-model(父子双向)

<!-- 父组件 -->
<template>
  <JdfsInput v-model="userName"/>
</template>

<script setup>
import { ref } from 'vue'
const userName = ref('张三')
</script>
<!-- 子组件 -->
<template>
  <input 
    type="text" 
    :value="modelValue" 
    @input="emit('update:modelValue', $event.target.value)"
  >
</template>

<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

6.5 provide / inject(祖孙通信)

<!-- 祖先组件 -->
<script setup>
import { ref, reactive, provide } from 'vue'

let money = ref(100)
let car = reactive({ brand: '奔驰', price: 100 })

function updateMoney(value) {
  money.value += value
}

// 提供数据
provide('moneyContext', { money, updateMoney })
provide('car', car)
</script>
<!-- 孙组件 -->
<script setup>
import { inject } from 'vue'

// 注入数据
let { money, updateMoney } = inject('moneyContext')
let car = inject('car')
</script>

6.6 插槽 Slot

默认插槽
<!-- 父组件 -->
<Category title="今日热门游戏">
  <ul>
    <li v-for="g in games" :key="g.id">{{ g.name }}</li>
  </ul>
</Category>
<!-- 子组件 -->
<template>
  <div class="item">
    <h3>{{ title }}</h3>
    <slot></slot>  <!-- 默认插槽 -->
  </div>
</template>
具名插槽
<!-- 父组件 -->
<Category title="今日热门游戏">
  <template v-slot:s1>
    <ul>...</ul>
  </template>
  <template #s2>
    <a href="">更多</a>
  </template>
</Category>
<!-- 子组件 -->
<template>
  <div class="item">
    <slot name="s1"></slot>
    <slot name="s2"></slot>
  </div>
</template>
作用域插槽
<!-- 父组件 -->
<Game v-slot="params">
  <ul>
    <li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
  </ul>
</Game>
<!-- 子组件 -->
<template>
  <div class="category">
    <h2>今日游戏榜单</h2>
    <slot :games="games" a="哈哈"></slot>
  </div>
</template>

<script setup>
import { reactive } from 'vue'
let games = reactive([
  { id: '01', name: '英雄联盟' },
  { id: '02', name: '王者荣耀' }
])
</script>

总结

Vue3 学习路线

  1. 基础阶段:掌握 setuprefreactivecomputedwatch
  2. 进阶阶段:学习生命周期、组件通信、自定义 Hook
  3. 工程化:掌握路由(vue-router)、状态管理(Pinia)
  4. 实战:用 Vue3 + TypeScript 搭建项目

关键记忆点

  • ref → 基本类型,需要 .value
  • reactive → 对象类型,不需要 .value
  • setup → Vue3 的核心,Composition API 的舞台
  • script setup → 推荐写法,更简洁
  • Pinia → 替代 Vuex,更简单
  • mitt → 替代事件总线

更多推荐