Vue3基础知识汇总与实战指南 - 教程

一、Vue3 核心特性与环境搭建

1.1 Vue3 核心升级点

Vue3 在语法、性能、扩展性上实现全面升级,核心变化包括:

  • 组合式 API:按业务逻辑聚合代码,替代 Vue2 选项式 API 的分散写法

  • 响应式重构:基于 Proxy 实现,支持数组索引、对象新增属性的响应式追踪

  • 多根节点组件:无需外层包裹 div,减少 DOM 层级

  • 新组件支持:Teleport、Suspense、TransitionGroup 等增强组件能力

  • TypeScript 原生支持:类型推导更完善,开发体验提升

1.2 开发环境搭建

方式 1:Vite(推荐,极速构建)
# 创建项目
npm create vite@latest vue3-demo -- --template vue
# 进入项目
cd vue3-demo
# 安装依赖
npm install
# 启动开发服务
npm run dev
方式 2:Vue CLI
# 安装CLI(需Node.js 14.18+)
npm install -g @vue/cli
# 创建项目
vue create vue3-demo
# 选择Vue 3选项
# 启动服务
cd vue3-demo && npm run serve
项目结构核心文件
vue3-demo/
├── src/
│   ├── main.js       # 入口文件(替换Vue2的new Vue())
│   ├── App.vue       # 根组件(支持多根节点)
│   └── components/   # 组件目录
入口文件差异(Vue2 vs Vue3)
// Vue2入口
import Vue from 'vue'
import App from './App.vue'
new Vue({ el: '#app', render: h => h(App) })
// Vue3入口(main.js)
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

二、组合式 API 全解析:语法与实战

2.1 setup 语法糖(核心入口)

Vue3.2 + 推荐的简化写法,无需 return 即可暴露变量 / 方法到模板,自动注册组件。

<!-- 基础用法 -->
  <template>
    <button @click="count++">{{ count }}</button>
      </template>
        <script setup>
          // 直接定义响应式变量
          import { ref } from 'vue'
          const count = ref(0)
          </script>

2.2 核心 API 用法

2.2.1 状态定义:ref 与 reactive
<script setup>
  import { ref, reactive, toRefs } from 'vue'
  // 1. 基础类型响应式(ref)
  const age = ref(20)
  // 修改值需通过.value
  age.value = 21
  // 2. 对象类型响应式(reactive)
  const user = reactive({
  name: '张三',
  address: { city: '北京' }
  })
  // 直接修改属性
  user.name = '李四'
  // 3. 解构响应式对象(toRefs)
  const { name, address } = toRefs(user)
  </script>
2.2.2 计算属性:computed
<script setup>
  import { ref, computed } from 'vue'
  const score = ref(85)
  // 只读计算属性
  const level = computed(() => {
  return score.value >= 90 ? '优秀' : '良好'
  })
  // 可写计算属性
  const doubleScore = computed({
  get() { return score.value * 2 },
  set(val) { score.value = val / 2 }
  })
  doubleScore.value = 180 // 触发set,score变为90
  </script>
2.2.3 监听器:watch 与 watchEffect
<script setup>
  import { ref, watch, watchEffect } from 'vue'
  const count = ref(0)
  const user = ref({ name: '张三' })
  // 1. 监听单个基础类型
  watch(count, (newVal, oldVal) => {
  console.log(`${oldVal}变到${newVal}`)
  }, { immediate: true }) // 立即执行
  // 2. 监听对象属性
  watch(() => user.value.name, (newName) => {
  console.log('用户名变更:', newName)
  })
  // 3. 自动追踪依赖(watchEffect)
  watchEffect(() => {
  console.log('count值:', count.value)
  })
  </script>

2.3 生命周期钩子

Vue3 生命周期需显式导入,setup 替代了 Vue2 的 beforeCreate 和 created。

<script setup>
  import { onMounted, onBeforeUnmount } from 'vue'
  // 组件挂载后执行
  onMounted(() => {
  console.log('DOM已挂载')
  })
  // 组件卸载前清理
  let timer = null
  onMounted(() => {
  timer = setInterval(() => console.log('计时'), 1000)
  })
  onBeforeUnmount(() => {
  clearInterval(timer)
  })
  // 支持多个同名钩子顺序执行
  onMounted(() => {
  console.log('第二个mounted钩子')
  })
  </script>

2.4 逻辑复用:自定义 Hooks

替代 Vue2 的 mixin,解决命名冲突和依赖模糊问题。

// hooks/useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'
export default function useMousePosition() {
const x = ref(0)
const y = ref(0)
function updatePosition(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', updatePosition)
})
onUnmounted(() => {
window.removeEventListener('mousemove', updatePosition)
})
return { x, y }
}
<!-- 组件中使用 -->
  <template>
    鼠标位置:{{x}}, {{y}}
    </template>
      <script setup>
        import useMousePosition from './hooks/useMousePosition'
        const { x, y } = useMousePosition()
        </script>

三、响应式系统:原理与 API 用法

3.1 响应式原理对比

特性Vue2(Object.defineProperty)Vue3(Proxy)
追踪范围仅对象已定义属性全属性(含新增 / 删除)
数组支持需重写 7 种方法(push/pop 等)原生支持数组索引 / 长度修改
嵌套响应递归定义需额外处理自动深度代理
性能初始化慢(遍历所有属性)懒代理(访问时才代理)

3.2 响应式工具 API

3.2.1 只读代理:readonly
<script setup>
  import { reactive, readonly } from 'vue'
  const user = reactive({ name: '张三' })
  const readOnlyUser = readonly(user)
  // 修改会警告且无效
  readOnlyUser.name = '李四' // 控制台警告
  </script>
3.2.2 响应式转换:toRef 与 toRefs
<script setup>
  import { reactive, toRef, toRefs } from 'vue'
  const user = reactive({ name: '张三', age: 20 })
  // 单个属性转换为ref
  const nameRef = toRef(user, 'name')
  // 所有属性转换为ref
  const { name, age } = toRefs(user)
  </script>
3.2.3 非响应式标记:markRaw
<script setup>
  import { reactive, markRaw } from 'vue'
  // 标记后对象不再响应式
  const rawObj = markRaw({ id: 1 })
  const state = reactive({
  data: rawObj
  })
  state.data.id = 2 // 不会触发更新
  </script>

四、组件进阶:新特性与通信方案

4.1 组件新特性

4.1.1 多根节点组件(Fragment)
<!-- Vue3支持直接写多个根节点 -->
  <template>
    <header>头部</header>
      <main>主体</main>
        <footer>底部</footer>
          </template>
4.1.2 Teleport(传送门)

将组件内容渲染到指定 DOM 位置,解决层级样式问题(如弹窗)。

<template>
  <button @click="show = true">打开弹窗</button>
    <teleport to="body">
      <div class="dialog" v-if="show">
        <h3>弹窗标题</h3>
          <button @click="show = false">关闭</button>
            </div>
              </teleport>
                </template>
                  <script setup>
                    import { ref } from 'vue'
                    const show = ref(false)
                    </script>
                      <style scoped>
                        .dialog {
                        position: fixed;
                        top: 50%;
                        left: 50%;
                        transform: translate(-50%, -50%);
                        }
                        </style>
4.1.3 Suspense(异步组件加载)

等待异步组件加载时显示兜底内容,优化用户体验。

<!-- 父组件 -->
  <template>
    <suspense>
      <!-- 默认插槽:异步组件加载完成后显示 -->
        <template #default>
          <AsyncComponent />
            </template>
              <!-- 兜底插槽:加载中显示 -->
                <template #fallback>
                  <div>加载中...</div>
                    </template>
                      </suspense>
                        </template>
                          <script setup>
                            // 动态导入异步组件
                            const AsyncComponent = defineAsyncComponent(() =>
                            import('./AsyncComponent.vue')
                            )
                            </script>
                              <!-- AsyncComponent.vue(含异步逻辑) -->
                                <script setup>
                                  // setup支持async/await
                                  const fetchData = async () => {
                                  const res = await fetch('/api/data')
                                  return res.json()
                                  }
                                  const data = await fetchData()
                                  </script>

4.2 组件通信方案汇总

4.2.1 父子通信:props 与 emit
<!-- 子组件 Child.vue -->
  <template>
    <button @click="handleClick">点击</button>
      </template>
        <script setup>
          // 定义接收的props
          const props = defineProps({
          title: { type: String, required: true }
          })
          // 定义触发的事件
          const emit = defineEmits(['change'])
          const handleClick = () => {
          emit('change', '传递给父组件的值')
          }
          </script>
            <!-- 父组件 Parent.vue -->
              <template>
                <Child title="父传子标题" @change="handleChange" />
                  </template>
                    <script setup>
                      const handleChange = (val) => {
                      console.log('子传父的值:', val)
                      }
                      </script>
4.2.2 跨级通信:provide 与 inject
<!-- 祖先组件 -->
  <script setup>
    import { provide, ref } from 'vue'
    const theme = ref('light')
    // 提供数据
    provide('theme', theme)
    // 提供修改方法
    provide('updateTheme', (newTheme) => {
    theme.value = newTheme
    })
    </script>
      <!-- 后代组件 -->
        <script setup>
          import { inject } from 'vue'
          // 注入数据和方法
          const theme = inject('theme')
          const updateTheme = inject('updateTheme')
          // 调用方法修改祖先组件数据
          const switchTheme = () => {
          updateTheme(theme.value === 'light' ? 'dark' : 'light')
          }
          </script>
4.2.3 兄弟通信:mitt 事件总线
# 安装mitt
npm install mitt
// utils/eventBus.js
import mitt from 'mitt'
export const eventBus = mitt()
<!-- 组件A(发送方) -->
  <script setup>
    import { eventBus } from './utils/eventBus'
    const sendMsg = () => {
    eventBus.emit('msg', '来自A的消息')
    }
    </script>
      <!-- 组件B(接收方) -->
        <script setup>
          import { eventBus } from './utils/eventBus'
          import { onMounted, onUnmounted } from 'vue'
          onMounted(() => {
          // 监听事件
          eventBus.on('msg', (val) => {
          console.log('接收消息:', val)
          })
          })
          onUnmounted(() => {
          // 解绑事件
          eventBus.off('msg')
          })
          </script>

五、实战案例:TodoList 全流程实现

5.1 项目结构

src/
├── components/
│   ├── TodoInput.vue    # 输入框组件
│   ├── TodoList.vue     # 列表组件
│   └── TodoItem.vue     # 列表项组件
├── hooks/
│   └── useTodoStore.js  # 状态管理Hook
├── App.vue              # 根组件
└── main.js              # 入口文件

5.2 状态管理:useTodoStore.js

import { ref, computed } from 'vue'
export default function useTodoStore() {
// 本地存储初始化
const todos = ref(JSON.parse(localStorage.getItem('todos') || '[]'))
// 计算属性:未完成数量
const unDoneCount = computed(() => {
return todos.value.filter(todo => !todo.done).length
})
// 方法:添加任务
const addTodo = (text) => {
if (!text.trim()) return
todos.value.push({
id: Date.now(),
text,
done: false
})
}
// 方法:切换任务状态
const toggleTodo = (id) => {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
// 方法:删除任务
const deleteTodo = (id) => {
todos.value = todos.value.filter(t => t.id !== id)
}
// 监听todos变化,同步到本地存储
watch(todos, (newTodos) => {
localStorage.setItem('todos', JSON.stringify(newTodos))
}, { deep: true })
return { todos, unDoneCount, addTodo, toggleTodo, deleteTodo }
}

5.3 组件实现

TodoInput.vue
<template>
  <div class="input-container">
    <input
    v-model="text"
    @keyup.enter="handleAdd"
    placeholder="请输入任务"
    />
    <button @click="handleAdd">添加</button>
      </div>
        </template>
          <script setup>
            import { ref, defineEmits } from 'vue'
            const text = ref('')
            const emit = defineEmits(['add'])
            const handleAdd = () => {
            emit('add', text.value)
            text.value = '' // 清空输入框
            }
            </script>
TodoItem.vue
<template>
  <li :class="{ done: todo.done }">
    <input
    type="checkbox"
    :checked="todo.done"
    @change="$emit('toggle', todo.id)"
    />
    <span>{{ todo.text }}</span>
      <button @click="$emit('delete', todo.id)">删除</button>
        </li>
          </template>
            <script setup>
              defineProps({
              todo: {
              type: Object,
              required: true,
              properties: {
              id: Number,
              text: String,
              done: Boolean
              }
              }
              })
              defineEmits(['toggle', 'delete'])
              </script>
                <style scoped>
                  .done span {
                  text-decoration: line-through;
                  color: #999;
                  }
                  </style>
TodoList.vue
<template>
  <ul class="todo-list">
    <TodoItem
    v-for="todo in todos"
    :key="todo.id"
    :todo="todo"
    @toggle="toggleTodo"
    @delete="deleteTodo"
    />
    </ul>
      </template>
        <script setup>
          import TodoItem from './TodoItem.vue'
          defineProps({
          todos: { type: Array, required: true },
          toggleTodo: { type: Function, required: true },
          deleteTodo: { type: Function, required: true }
          })
          </script>
App.vue(根组件整合)
<template>
  <div class="app">
    <h1>TodoList(剩余{{ unDoneCount }}项)</h1>
      <TodoInput @add="addTodo" />
        <TodoList
        :todos="todos"
        :toggleTodo="toggleTodo"
        :deleteTodo="deleteTodo"
        />
        </div>
          </template>
            <script setup>
              import TodoInput from './components/TodoInput.vue'
              import TodoList from './components/TodoList.vue'
              import useTodoStore from './hooks/useTodoStore'
              // 导入状态管理逻辑
              const { todos, unDoneCount, addTodo, toggleTodo, deleteTodo } = useTodoStore()
              </script>

六、避坑指南与性能优化

6.1 常见问题与解决方案

问题场景原因分析解决方案
ref 对象修改不更新 DOM直接赋值替换了 ref 引用改用.value 修改属性(如 refObj.value = 10)
异步组件报错async setup 未用 Suspense 包裹父组件添加 Suspense 组件兜底
provide/inject 数据不响应传递的是非响应式值用 ref/reactive 包装后再 provide
toRefs 解构后失去响应直接解构 reactive 对象必须通过 toRefs 转换后再解构
组件卸载后定时器仍运行未在 onBeforeUnmount 清理定时器需在卸载前 clearInterval

6.2 性能优化技巧

6.2.1 减少不必要的渲染
<!-- 1. 组件缓存:KeepAlive -->
  <template>
    <KeepAlive>
      <router-view /> <!-- 缓存路由组件 -->
        </KeepAlive>
          </template>
            <!-- 2. 计算属性缓存:避免重复计算 -->
              <script setup>
                import { computed } from 'vue'
                // 复杂计算会被缓存,仅依赖变化时重新计算
                const complexResult = computed(() => {
                return heavyCalculation(state.data)
                })
                </script>
                  <!-- 3. 事件处理函数缓存:避免每次渲染创建新函数 -->
                    <script setup>
                      import { useCallback } from 'vue'
                      // 缓存函数,仅依赖变化时重新创建
                      const handleClick = useCallback((id) => {
                      console.log('点击了', id)
                      }, []) // 空依赖数组:始终返回同一函数
                      </script>
6.2.2 大型列表优化
<!-- 使用vue-virtual-scroller实现虚拟滚动 -->
  <template>
    <RecycleScroller
    class="scroller"
    :items="largeList"
    :item-size="50"
    >
    <template v-slot="{ item }">
      <div class="list-item">{{ item.content }}</div>
        </template>
          </RecycleScroller>
            </template>
              <script setup>
                import { RecycleScroller } from 'vue-virtual-scroller'
                import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
                // 模拟10万条数据
                const largeList = Array(100000).fill(0).map((_, i) => ({
                id: i,
                content: `列表项 ${i + 1}`
                }))
                </script>
posted @ 2025-11-30 10:40  gccbuaa  阅读(135)  评论(0)    收藏  举报