Skip to content

Vue 3 基础入门

1. Vue 3 简介

1.1 为什么选择 Vue 3

Vue 3 核心改进:
├── 性能提升    │  Proxy 响应式,Virtual DOM 重写,编译时优化
├── 更好的 TS 支持 │  全部用 TS 重写,类型推导更准确
├── 组合式 API    │  逻辑复用更好,代码组织更灵活
├── 更小的体积    │  Tree-shaking 优化,核心 10KB 左右
├── 更强的扩展性  │  自定义渲染器,依赖注入改进
└── 新增特性      │  Teleport、Suspense、Fragments

1.2 搭建项目

bash
# 使用 npm
npm create vue@latest my-vue-app

# 使用 vite (推荐,更快)
npm create vite@latest my-vue-app -- --template vue

# 进入项目
cd my-vue-app
npm install
npm run dev

1.3 项目结构

my-vue-app/
├── public/           # 静态资源
├── src/
│   ├── assets/       # 资源文件
│   ├── components/   # 组件
│   ├── views/        # 页面
│   ├── router/       # 路由
│   ├── stores/       # Pinia 状态管理
│   ├── composables/  # 组合式函数
│   ├── App.vue       # 根组件
│   └── main.js       # 入口文件
├── index.html
├── vite.config.js
└── package.json

1.4 入口文件

javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 安装插件
app.use(router)
app.use(pinia)
app.mount('#app')

// 全局配置
app.config.errorHandler = (err, instance, info) => {
  console.error('全局错误:', err, info)
}

2. 模板语法

2.1 插值

vue
<template>
  <!-- 文本插值 -->
  <span>{{ message }}</span>
  
  <!-- 原始 HTML (慎用,XSS 风险) -->
  <div v-html="rawHtml"></div>
  
  <!-- 属性绑定 -->
  <div :class="{ active: isActive }"></div>
  
  <!-- JavaScript 表达式 -->
  <span>{{ count + 1 }}</span>
  <span>{{ ok ? 'YES' : 'NO' }}</span>
</template>

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

const message = ref('Hello Vue 3')
const rawHtml = ref('<strong>粗体文字</strong>')
const isActive = ref(true)
const count = ref(0)
const ok = ref(true)
</script>

2.2 指令

vue
<template>
  <!-- v-bind 绑定属性 -->
  <img :src="imageUrl" :alt="title" />
  <button :disabled="isDisabled">点击</button>
  
  <!-- v-on 绑定事件 -->
  <button @click="handleClick">点击</button>
  <form @submit.prevent="handleSubmit">表单</form>
  <div @click.stop="handleClick">阻止冒泡</div>
  
  <!-- v-model 双向绑定 -->
  <input v-model="text" />
  <input v-model.number="age" type="number" />
  
  <!-- v-if / v-show -->
  <div v-if="show">条件渲染</div>
  <div v-show="show">display 切换</div>
  
  <!-- v-for 列表渲染 -->
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
  
  <!-- v-once 只渲染一次 -->
  <span v-once>{{ message }}</span>
  
  <!-- v-cloak 编译完成前隐藏 -->
  <div v-cloak>{{ message }}</div>
</template>

2.3 事件修饰符

vue
<template>
  <!-- 阻止默认行为 -->
  <form @submit.prevent="onSubmit">...</form>
  
  <!-- 阻止冒泡 -->
  <div @click.stop="onClick">...</div>
  
  <!-- 只触发一次 -->
  <button @click.once="doOnce">只执行一次</button>
  
  <!-- 捕获模式 -->
  <div @click.capture="handleCapture">捕获阶段触发</div>
  
  <!-- 键盘事件 -->
  <input @keyup.enter="submit" />
  <input @keyup.ctrl.enter="submit" />
  
  <!-- 鼠标事件修饰符 -->
  <button @click.left="leftClick">左键</button>
  <button @click.right="rightClick">右键</button>
  <button @click.middle="middleClick">中键</button>
</template>

3. 响应式基础

3.1 ref 和 reactive

javascript
import { ref, reactive, toRefs, toRef } from 'vue'

// ref: 基础类型响应式
const count = ref(0)
console.log(count.value)  // 0
count.value++

// ref: 对象类型(自动深层响应式)
const obj = ref({ name: '张三', age: 25 })
obj.value.name = '李四'

// reactive: 对象/数组响应式(深层)
const state = reactive({
  count: 0,
  user: { name: '张三' },
  items: [1, 2, 3]
})
state.count++
state.user.name = '李四'
state.items.push(4)

// toRefs: 解构响应式对象
const { count, name } = toRefs(state)
// 保持响应式连接
count.value = 100  // state.count 也会变

// toRef: 单个属性保持响应式
const age = toRef(state, 'age')

3.2 computed 计算属性

javascript
import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// 只读计算属性
const fullName = computed(() => {
  return firstName.value + lastName.value
})

// 可写计算属性
const fullNameWritable = computed({
  get() {
    return firstName.value + lastName.value
  },
  set(value) {
    firstName.value = value.slice(0, 1)
    lastName.value = value.slice(1)
  }
})

// 访问
console.log(fullName.value)  // '张三'
fullNameWritable.value = '李四'
console.log(firstName.value) // '李'

3.3 watch 监听

javascript
import { ref, reactive, watch, watchEffect } from 'vue'

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

// 监听单个 ref
watch(count, (newVal, oldVal) => {
  console.log(`count 从 ${oldVal} 变成 ${newVal}`)
})

// 监听多个
watch([count, () => user.name], ([newCount, newName], [oldCount, oldName]) => {
  console.log('count 或 name 变化了')
})

// 监听 reactive 对象(需要用函数)
watch(() => user.age, (newAge, oldAge) => {
  console.log(`年龄从 ${oldAge} 变成 ${newAge}`)
}, { immediate: true, deep: true })

// watchEffect: 副作用自动追踪
watchEffect(() => {
  // 自动追踪 user.name 和 user.age
  console.log(`${user.name} 的年龄是 ${user.age}`)
})

// 停止监听
const stop = watch(count, () => { ... })
stop()  // 停止监听

4. 组合式 API

4.1 setup 基础

vue
<script setup>
// setup 是编译器处理的语法糖
// 所有导入的组件、变量都会自动暴露给模板

import { ref, computed, onMounted } from 'vue'
import MyComponent from './MyComponent.vue'

const message = ref('Hello')
const count = ref(0)

const doubled = computed(() => count.value * 2)

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

// 生命周期钩子
onMounted(() => {
  console.log('组件挂载完成')
})
</script>

<template>
  <h1>{{ message }}</h1>
  <button @click="increment">+1</button>
  <p>count × 2 = {{ doubled }}</p>
  <MyComponent />
</template>

4.2 生命周期钩子

javascript
import { 
  onMounted, 
  onUpdated, 
  onUnmounted,
  onBeforeMount,
  onBeforeUpdate,
  onBeforeUnmount,
  onActivated,
  onDeactivated,
  onErrorCaptured
} from 'vue'

export default {
  setup() {
    // 创建阶段
    onBeforeMount(() => {
      console.log('即将挂载')
    })
    
    onMounted(() => {
      console.log('已挂载')
    })
    
    // 更新阶段
    onBeforeUpdate(() => {
      console.log('即将更新')
    })
    
    onUpdated(() => {
      console.log('已更新')
    })
    
    // 卸载阶段
    onBeforeUnmount(() => {
      console.log('即将卸载')
    })
    
    onUnmounted(() => {
      console.log('已卸载')
    })
    
    // Keep-Alive 相关
    onActivated(() => {
      console.log('激活')
    })
    
    onDeactivated(() => {
      console.log('停用')
    })
    
    // 错误捕获
    onErrorCaptured((err, instance, info) => {
      console.error('捕获到错误:', err, info)
    })
  }
}

4.3 依赖注入

javascript
// 父组件 provide
import { provide, ref } from 'vue'

export default {
  setup() {
    const theme = ref('dark')
    const updateTheme = (newTheme) => {
      theme.value = newTheme
    }
    
    // 提供给所有后代
    provide('theme', theme)
    provide('updateTheme', updateTheme)
    
    // 或者提供静态值
    provide('appName', 'My Vue App')
  }
}

// 子组件 inject
import { inject } from 'vue'

export default {
  setup() {
    // 注入
    const theme = inject('theme')
    const updateTheme = inject('updateTheme')
    const appName = inject('appName', 'Default App')  // 默认值
    
    // 注入是响应式的
    console.log(theme.value)  // 'dark'
    
    return { theme, updateTheme, appName }
  }
}

4.4 组合式函数 (Composables)

javascript
// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const double = computed(() => count.value * 2)
  
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  function reset() {
    count.value = initialValue
  }
  
  return {
    count,
    double,
    increment,
    decrement,
    reset
  }
}

// 使用
import { useCounter } from '@/composables/useCounter'

export default {
  setup() {
    const { count, increment, decrement } = useCounter(10)
    return { count, increment, decrement }
  }
}

4.5 常用工具函数

javascript
import { 
  // 判断类型
  isRef,        // 是否是 ref
  isReactive,   // 是否是 reactive
  isProxy,      // 是否是 proxy
  isReadonly,   // 是否是 readonly
  
  // 转换
  toRefs,       // reactive 转 ref
  toRef,        // 单个属性转 ref
  toValue,      // ref/reactive 转原始值
  
  // 高级
  shallowRef,    // 浅层响应式(.value 变化才触发)
  shallowReactive, // 浅层 reactive
  readonly,     // 只读响应式
  triggerRef,   // 手动触发 shallowRef 更新
  
  // nextTick
  nextTick      // 等待 DOM 更新
} from 'vue'

// 示例
import { shallowRef, triggerRef, nextTick } from 'vue'

const state = shallowRef({ count: 0 })
// 改变嵌套属性不会触发更新
state.value.count = 100  // 不会更新视图!

// 需要手动触发
state.value.count = 100
triggerRef(state)

// nextTick 等待更新
async function updateAndWait() {
  count.value++
  await nextTick()
  console.log('DOM 已更新')
}

5. 组件基础

5.1 组件通信

vue
<!-- Parent.vue -->
<template>
  <!-- 父传子: props -->
  <ChildComponent 
    :title="title"
    :items="items"
    @update="handleUpdate"
  />
  
  <!-- 父传子: provide/inject -->
  <MiddleComponent />
</template>

<script setup>
import { ref, provide } from 'vue'
import ChildComponent from './ChildComponent.vue'

const title = ref('标题')
const items = ref(['a', 'b', 'c'])

provide('sharedData', {
  message: '共享数据'
})

function handleUpdate(newValue) {
  console.log('子组件更新:', newValue)
}
</script>
vue
<!-- Child.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <ul>
      <li v-for="item in items" :key="item">{{ item }}</li>
    </ul>
    
    <!-- 子传父: emit -->
    <button @click="notifyParent">通知父组件</button>
    
    <!-- 访问父组件 -->
    <button @click="handleClick">直接调用父组件方法</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

// 定义 props
const props = defineProps({
  title: {
    type: String,
    required: true,
    default: '默认标题'
  },
  items: {
    type: Array,
    default: () => []
  }
})

// 定义 emit
const emit = defineEmits(['update', 'click'])

function notifyParent() {
  emit('update', 'new value')
}

// 访问父组件方法
function handleClick() {
  // 模板中用 $emit,脚本中用 context.emit
}

// 注入
const shared = inject('sharedData')
console.log(shared.message)
</script>

5.2 v-model 在组件上

vue
<!-- Parent.vue -->
<template>
  <!-- 默认: modelValue + update:modelValue -->
  <CustomInput v-model="text" />
  
  <!-- 多个 v-model -->
  <CustomInput 
    v-model="text"
    v-model:title="title"
  />
</template>

<script setup>
import CustomInput from './CustomInput.vue'
const text = ref('')
const title = ref('')
</script>
vue
<!-- CustomInput.vue -->
<template>
  <!-- 方式1: 普通 input -->
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
  
  <!-- 方式2: 简写 -->
  <input v-model="modelValue" />
  
  <!-- 命名 v-model -->
  <input :value="title" @input="$emit('update:title', $event.target.value)" />
</template>

<script setup>
defineProps(['modelValue', 'title'])
defineEmits(['update:modelValue', 'update:title'])
</script>

5.3 插槽

vue
<!-- Parent.vue -->
<template>
  <Layout>
    <!-- 默认插槽 -->
    <template #default>
      <p>主要内容</p>
    </template>
    
    <!-- 命名插槽 -->
    <template #header>
      <h1>页面标题</h1>
    </template>
    
    <!-- 作用域插槽 -->
    <template #footer="{ data }">
      <p>页脚 - {{ data }}</p>
    </template>
  </Layout>
</template>
vue
<!-- Layout.vue -->
<template>
  <div class="layout">
    <header>
      <slot name="header">默认标题</slot>
    </header>
    
    <main>
      <slot>默认内容</slot>
    </main>
    
    <footer>
      <slot name="footer" :data="footerData">默认页脚</slot>
    </footer>
  </div>
</template>

<script setup>
const footerData = ref('版权信息')
</script>

6. 常用特性

6.1 Teleport

vue
<!-- 将内容传送到任意 DOM 位置 -->
<template>
  <button @click="showModal = true">打开弹窗</button>
  
  <Teleport to="body">
    <div v-if="showModal" class="modal">
      <div class="modal-content">
        <h2>弹窗内容</h2>
        <button @click="showModal = false">关闭</button>
      </div>
    </div>
  </Teleport>
  
  <!-- 传送到指定 ID -->
  <Teleport to="#modal-container">
    <div>另一个弹窗</div>
  </Teleport>
  
  <!-- 禁用 (调试用) -->
  <Teleport to="body" :disabled="true">
    <div>临时禁用</div>
  </Teleport>
</template>

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

6.2 Suspense

vue
<!-- 异步组件加载状态 -->
<template>
  <Suspense>
    <!-- 主要内容: 异步组件 -->
    <template #default>
      <AsyncUserProfile />
    </template>
    
    <!-- 加载中的显示 -->
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

<script setup>
// 异步组件
const UserProfile = defineAsyncComponent(() => 
  import('./UserProfile.vue')
)

// 或者在 setup 中直接 await
import { ref, defineAsyncComponent, onMounted } from 'vue'

const UserProfile = defineAsyncComponent({
  loader: () => import('./UserProfile.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})
</script>

6.3 片段 (Fragments)

vue
<!-- Vue 3 组件可以有多个根元素 -->
<template>
  <div class="header">头部</div>
  <div class="main">主体</div>
  <div class="footer">底部</div>
</template>

<!-- 带有特性的片段 -->
<template>
  <!-- attributes 自动应用到内部第一个元素 -->
  <h1>多根组件</h1>
  <article>文章内容</article>
  <!-- 等同于 -->
  <div v--bind="$attrs">attributes 自动应用到这里</div>
</template>

<script setup>
// 禁用 inheritAttrs
defineOptions({
  inheritAttrs: false
})
</script>

7. 面试要点

7.1 Vue 2 vs Vue 3 响应式对比

Vue 2: Object.defineProperty
- 无法监听新增/删除属性 (需用 Vue.set/delete)
- 无法监听数组索引变化 (需用 $set)
- 需要遍历所有属性,性能差

Vue 3: Proxy
- 深层响应式,自动追踪
- 新增/删除属性自动响应
- 数组索引变化自动响应
- 性能更好

7.2 ref vs reactive 怎么选

ref:
- 适合基本类型
- 适合解构赋值后保持响应式
- 模板中使用自动解包 (.value 可省略)
- 返回可变对象 { value: xxx }

reactive:
- 适合对象/数组
- 不适合解构(会丢失响应式)
- 返回 Proxy,性能略好

最佳实践:
- 基本类型用 ref
- 对象/数组用 reactive
- 解构用 toRefs + ref
- 保持清晰,不要混用

7.3 computed vs watch 区别

computed:
- 用于派生计算
- 自动追踪依赖
- 结果会被缓存
- 同步返回
- 适合派生状态

watch:
- 用于副作用
- 手动指定监听目标
- 不缓存
- 可以是异步
- 适合执行副作用(请求、DOM 操作)

7.4 组合式 API 优势

1. 更好的逻辑复用
   - mixins 的缺陷: 属性来源不清、命名冲突
   - composables: 显式导入、清晰的 API

2. 更好的代码组织
   - 相关逻辑放一起(而非按 options 分散)
   - 更适合大型项目

3. 更好的 TypeScript 支持
   - 类型推导更准确
   - IDE 支持更好

基于 MIT 许可发布