适合读者:会一点 HTML、CSS、JavaScript,想从零开始入门 Vue3,但不想一开始就被一堆概念绕晕。

这篇文章不准备把 Vue3 的所有知识点都塞进来,而是带你按真实学习顺序走一遍:

  1. 先知道 Vue3 是什么;
  2. 再创建一个能运行的项目;
  3. 然后认识 .vue 单文件组件;
  4. 接着学习 script setuprefreactivecomputedwatch
  5. 最后用一个 Todo 案例把组件拆分、propsemit 串起来。

学完之后,你应该能看懂一个 Vue3 入门项目的基本结构,也知道后面该继续学什么。

一、Vue3 到底是什么

Vue 是一个用于构建用户界面的 JavaScript 框架。简单说,它主要帮我们解决三件事:

  • 页面怎么根据数据自动渲染;
  • 数据变了,页面怎么自动更新;
  • 一个复杂页面怎么拆成多个可复用组件。

可以先用一张图建立整体印象:

新手先记住一句话:

Vue3 的核心,就是用响应式数据驱动页面,再用组件把页面拆开。

二、准备环境并创建项目

Vue 官方推荐使用 create-vue 来创建基于 Vite 的 Vue 项目。当前官方快速上手文档要求 Node.js 版本为 ^20.19.0 || >=22.12.0

先检查 Node 版本:

node -v

创建项目:

npm create vue@latest

创建过程中会出现一些选项,新手可以这样选:

选项建议
Project name自己起一个项目名
TypeScript先选 No,入门更轻
JSXNo
Vue Router入门第一篇可以 No
Pinia入门第一篇可以 No
Vitest / E2ENo
ESLint / Prettier可以先 No,后面再学
Vue DevTools可以 Yes,也可以先 No

进入项目并启动:

cd 项目名
npm install
npm run dev

启动成功后,终端会给你一个本地地址,例如:

http://localhost:5173/

在浏览器打开它,就能看到 Vue 项目页面。

整个流程可以理解为:

三、认识 Vue 项目目录

创建后的项目里,新手最先关注这几个文件就够了:

src
├─ main.js
├─ App.vue
└─ components
   └─ HelloWorld.vue

它们的作用:

文件作用
main.js项目入口,负责创建 Vue 应用并挂载到页面
App.vue根组件,项目启动后最先渲染的组件
components放可复用组件的目录

main.js 常见内容类似这样:

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

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

这段代码的意思是:

  • vue 里导入 createApp
  • 导入根组件 App.vue
  • 创建应用;
  • 挂载到 index.html 里的 id="app" 节点上。

四、认识 .vue 单文件组件

Vue 项目里最常见的文件是 .vue 文件,它叫单文件组件。

一个 .vue 文件通常由三部分组成:

<script setup>
// 写 JavaScript 逻辑
</script>

<template>
  <!-- 写页面结构 -->
</template>

<style scoped>
/* 写当前组件样式 */
</style>

三块分别对应:

区域作用
<script setup>写数据、方法、导入组件等逻辑
<template>写页面结构
<style scoped>写样式,scoped 表示样式只影响当前组件

先写一个最小计数器:

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

const count = ref(0)

function add() {
  count.value++
}
</script>

<template>
  <button @click="add">当前数量:{{ count }}</button>
</template>

这里已经出现了 Vue3 入门最重要的几个东西:

  • ref():定义响应式数据;
  • count.value++:在脚本中修改 ref 的值;
  • {{ count }}:在模板中显示数据;
  • @click="add":点击按钮时执行 add 方法。

五、为什么 Vue3 常用 script setup

Vue 组件有两种常见写法:

  • 选项式 API;
  • 组合式 API。

选项式 API 按选项分类组织代码:

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    add() {
      this.count++
    }
  }
}
</script>

组合式 API 更偏向按功能逻辑组织代码:

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

const count = ref(0)

function add() {
  count.value++
}
</script>

在 Vue3 新项目里,常见写法就是:

<script setup>
// 组合式 API 逻辑
</script>

script setup 的好处是:

  • 写法更短;
  • 顶层变量和函数可以直接在模板中使用;
  • 导入的组件可以直接在模板中使用;
  • 和 TypeScript 配合更自然;
  • 更适合后面封装自定义 hook。

新手先记住:

学 Vue3 新项目,重点掌握组合式 API 和 <script setup>

六、响应式数据:refreactive

Vue 的响应式可以理解为:

数据变了,页面会自动跟着变。

1. 使用 ref 定义简单数据

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

const username = ref('')

function changeName() {
  username.value = '小明'
}
</script>

<template>
  <p>用户名:{{ username }}</p>
  <button @click="changeName">修改用户名</button>
</template>

注意:

  • 脚本中读取或修改 ref,要写 .value
  • 模板中使用 ref,通常不写 .value

2. 使用 reactive 定义对象

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

const form = reactive({
  username: '',
  password: ''
})

function login() {
  console.log(form.username, form.password)
}
</script>

<template>
  <input v-model="form.username" placeholder="用户名">
  <input v-model="form.password" placeholder="密码">
  <button @click="login">登录</button>
</template>

reactive 适合对象、数组这类复杂数据。

3. refreactive 怎么选

场景推荐
数字、字符串、布尔值ref
表单输入值refreactive
普通对象reactiveref
数组reactiveref
需要整体替换对象ref
需要解构对象属性reactive + toRefs

简单记忆:

  • 简单值用 ref
  • 对象数组可用 reactive
  • 不确定怎么选,先用 ref
  • reactive 解构后容易丢响应式,需要配合 toRefs

七、用 Todo 案例串起核心语法

下面写一个简单 Todo 案例,包含:

  • 输入任务;
  • 添加任务;
  • 完成任务;
  • 删除任务;
  • 统计未完成数量;
  • 监听任务变化。

先在 App.vue 中写单组件版:

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

const newTitle = ref('')

const todos = ref([
  { id: 1, title: '学习 script setup', done: true },
  { id: 2, title: '学习 ref 和 reactive', done: false }
])

const unfinishedCount = computed(() => {
  return todos.value.filter(todo => !todo.done).length
})

function addTodo() {
  const title = newTitle.value.trim()

  if (!title) return

  todos.value.push({
    id: Date.now(),
    title,
    done: false
  })

  newTitle.value = ''
}

function toggleTodo(id) {
  const todo = todos.value.find(item => item.id === id)
  if (todo) {
    todo.done = !todo.done
  }
}

function removeTodo(id) {
  todos.value = todos.value.filter(item => item.id !== id)
}

watch(todos, (newValue) => {
  console.log('任务列表变化了:', newValue)
}, {
  deep: true
})
</script>

<template>
  <main class="page">
    <h1>Vue3 Todo 入门案例</h1>

    <section class="todo-input">
      <input
        v-model="newTitle"
        placeholder="请输入任务"
        @keyup.enter="addTodo"
      >
      <button @click="addTodo">添加</button>
    </section>

    <p>未完成任务:{{ unfinishedCount }}</p>

    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <label>
          <input
            type="checkbox"
            :checked="todo.done"
            @change="toggleTodo(todo.id)"
          >
          <span :class="{ done: todo.done }">{{ todo.title }}</span>
        </label>

        <button @click="removeTodo(todo.id)">删除</button>
      </li>
    </ul>
  </main>
</template>

<style scoped>
.page {
  max-width: 640px;
  margin: 40px auto;
  font-family: Arial, sans-serif;
}

.todo-input {
  display: flex;
  gap: 8px;
}

input {
  padding: 8px;
}

button {
  padding: 8px 12px;
}

li {
  display: flex;
  justify-content: space-between;
  margin: 8px 0;
}

.done {
  color: #999;
  text-decoration: line-through;
}
</style>

这个案例里,几个核心 API 的作用很清楚:

API在案例中的作用
ref保存输入框内容和任务列表
computed根据任务列表计算未完成数量
watch监听任务列表变化
v-model表单输入和数据双向绑定
v-for渲染任务列表
:class根据任务状态动态添加样式
@click绑定点击事件

数据流可以这样理解:

八、computedwatch 的区别

新手容易混淆 computedwatch

1. computed 用来得到一个新数据

例如未完成任务数量:

const unfinishedCount = computed(() => {
  return todos.value.filter(todo => !todo.done).length
})

它的特点:

  • 依赖的数据不变,就会使用缓存;
  • 适合根据已有数据计算出一个新结果;
  • 不建议在 computed 里发请求、改状态、操作 DOM。

2. watch 用来监听变化并做事情

例如监听任务列表变化:

watch(todos, (newValue) => {
  console.log('任务变化了:', newValue)
}, {
  deep: true
})

它的特点:

  • 可以明确监听某个数据;
  • 数据变化后执行回调;
  • 适合发请求、保存本地缓存、打印日志等副作用。

一句话区分:

computed 关心“算出什么值”,watch 关心“变化后做什么事”。

九、把 Todo 拆成组件

单组件版能运行,但真实项目里不会把所有代码都塞进 App.vue

可以拆成这样:

src
├─ App.vue
└─ components
   ├─ TodoInput.vue
   ├─ TodoList.vue
   └─ TodoItem.vue

组件关系:

flowchart LR
  A[App.vue 父组件] -- props: todos --> B[TodoList.vue]
  B -- props: todo --> C[TodoItem.vue]
  C -- emit: toggle/remove --> B
  B -- emit: toggle/remove --> A
  A -- 更新 todos --> B

1. TodoInput.vue

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

const emit = defineEmits(['add'])

const title = ref('')

function submit() {
  const value = title.value.trim()

  if (!value) return

  emit('add', value)
  title.value = ''
}
</script>

<template>
  <div class="todo-input">
    <input
      v-model="title"
      placeholder="请输入任务"
      @keyup.enter="submit"
    >
    <button @click="submit">添加</button>
  </div>
</template>

这里的重点是:

  • 子组件内部维护输入框内容;
  • 添加时通过 emit('add', value) 通知父组件;
  • 真正修改任务列表的逻辑仍然放在父组件。

2. TodoItem.vue

<script setup>
const props = defineProps({
  todo: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['toggle', 'remove'])
</script>

<template>
  <li>
    <label>
      <input
        type="checkbox"
        :checked="props.todo.done"
        @change="emit('toggle', props.todo.id)"
      >
      <span :class="{ done: props.todo.done }">
        {{ props.todo.title }}
      </span>
    </label>

    <button @click="emit('remove', props.todo.id)">删除</button>
  </li>
</template>

这里的重点是:

  • props 接收父组件传来的任务;
  • 子组件不直接修改父组件数据;
  • 通过 emit 把操作通知出去。

3. TodoList.vue

<script setup>
import TodoItem from './TodoItem.vue'

const props = defineProps({
  todos: {
    type: Array,
    default: () => []
  }
})

const emit = defineEmits(['toggle', 'remove'])
</script>

<template>
  <ul>
    <TodoItem
      v-for="todo in props.todos"
      :key="todo.id"
      :todo="todo"
      @toggle="emit('toggle', $event)"
      @remove="emit('remove', $event)"
    />
  </ul>
</template>

这里的重点是:

  • TodoList 负责列表循环;
  • TodoItem 负责单个任务展示;
  • 子组件事件可以继续向上传递。

4. App.vue

<script setup>
import { ref, computed } from 'vue'
import TodoInput from './components/TodoInput.vue'
import TodoList from './components/TodoList.vue'

const todos = ref([
  { id: 1, title: '学习 script setup', done: true },
  { id: 2, title: '学习 ref 和 reactive', done: false }
])

const unfinishedCount = computed(() => {
  return todos.value.filter(todo => !todo.done).length
})

function addTodo(title) {
  todos.value.push({
    id: Date.now(),
    title,
    done: false
  })
}

function toggleTodo(id) {
  const todo = todos.value.find(item => item.id === id)
  if (todo) {
    todo.done = !todo.done
  }
}

function removeTodo(id) {
  todos.value = todos.value.filter(item => item.id !== id)
}
</script>

<template>
  <main class="page">
    <h1>Vue3 Todo 入门案例</h1>

    <TodoInput @add="addTodo" />

    <p>未完成任务:{{ unfinishedCount }}</p>

    <TodoList
      :todos="todos"
      @toggle="toggleTodo"
      @remove="removeTodo"
    />
  </main>
</template>

拆分后,每个组件职责更清楚:

组件职责
App.vue保存任务数据,定义增删改逻辑
TodoInput.vue输入并提交新任务
TodoList.vue渲染任务列表
TodoItem.vue渲染单个任务并触发操作

十、组件通信:propsemit

Vue 组件通信入门先掌握两个方向:

1. 父传子:props

父组件:

<TodoList :todos="todos" />

子组件:

const props = defineProps({
  todos: {
    type: Array,
    default: () => []
  }
})

props 的本质是:父组件把数据传给子组件。

注意:

  • 子组件要先声明 props;
  • props 是单向数据流;
  • 子组件不建议直接修改 props;
  • 数组和对象默认值要写成函数,例如 default: () => []

2. 子传父:emit

子组件:

const emit = defineEmits(['add'])

emit('add', title)

父组件:

<TodoInput @add="addTodo" />

emit 的本质是:子组件触发事件,父组件监听事件。

新手可以这样记:

父传子用 props,子通知父用 emit

十一、生命周期:什么时候执行初始化逻辑

Vue 组件从创建到销毁,会经历一系列阶段。入门阶段先掌握两个钩子:

钩子常见用途
onMounted组件挂载后执行,比如请求数据、访问 DOM
onBeforeUnmount组件卸载前执行,比如清理定时器、事件监听

示例:

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

const seconds = ref(0)
let timer = null

onMounted(() => {
  timer = setInterval(() => {
    seconds.value++
  }, 1000)
})

onBeforeUnmount(() => {
  clearInterval(timer)
})
</script>

<template>
  <p>页面停留时间:{{ seconds }} 秒</p>
</template>

注意:

  • 生命周期钩子要在 setup 阶段同步注册;
  • 定时器、事件监听、订阅类逻辑要记得清理;
  • 不要在 onUpdated 里随便修改会触发更新的数据,否则容易造成循环更新。

十二、常见新手坑

1. 脚本中修改 ref 忘记写 .value

错误:

count++

正确:

count.value++

2. 模板中多写 .value

模板中通常直接写:

<p>{{ count }}</p>

不用写:

<p>{{ count.value }}</p>

3. reactive 解构后丢响应式

const state = reactive({ count: 0 })
const { count } = state

如果需要解构,使用 toRefs

const { count } = toRefs(state)

4. watch 监听对象属性时写错

监听对象中的某个属性,常用 getter:

watch(() => person.value.name, (newValue, oldValue) => {
  console.log(newValue, oldValue)
})

5. 子组件直接修改 props

不要这样:

props.todo.done = true

更推荐:

emit('toggle', props.todo.id)

让父组件去修改自己的数据。

十三、后面继续学什么

学完这篇文章后,可以按这个顺序继续:

推荐路线:

学习内容作用
Vue Router根据 URL 切换页面组件,适合多页面 SPA
Pinia管理多个组件共享的数据
自定义 hook封装和复用组合式 API 逻辑
插槽让父组件决定子组件内部某块结构
TypeScript给数据、props、事件增加类型约束

入门阶段不要急着同时学完所有东西。先把 script setuprefcomputedpropsemit 这些常用能力练熟,再进入 Router 和 Pinia,会顺很多。

十四、总结

这篇文章从创建项目开始,带你走了一遍 Vue3 入门主线:

  • Vue3 主要解决声明式渲染、响应式、组件化;
  • Vue3 新项目常用单文件组件和 <script setup>
  • refreactive 用来定义响应式数据;
  • computed 适合计算派生数据;
  • watch 适合监听变化后执行副作用;
  • 父传子用 props
  • 子通知父用 emit
  • 复杂页面要拆成多个组件;
  • 后续再继续学习 Vue Router、Pinia、自定义 hook。

最后再用一句话收尾:

Vue3 入门不是背 API,而是先理解“数据驱动页面、组件拆分页面、事件连接组件”这条主线。

参考资料

更多推荐