一、目标效果与功能拆解

我们要实现的 TodoList 功能包括:

  • ✅ 输入待办事项

  • ✅ 点击按钮添加到列表

  • ✅ 显示待办数量

  • ✅ 点击删除某一项

最终效果示意

[ 输入待办 ]  [ 添加 ]
-----------------------
✅ 学习 Vue3
❌ 写博客
✅ 运动
-----------------------
共 3 条待办

二、组件拆分设计(很重要)

很多新手会犯一个错误:

👉 所有逻辑都写在 App.vue

我们先做一次正确的组件拆分:

src/
├── App.vue               # 页面组装
└── components/
    ├── TodoInput.vue     # 输入 + 添加按钮
    ├── TodoList.vue      # 列表展示
    └── TodoItem.vue      # 单个待办项

📌 拆分原则

  • 一个组件只负责一类 UI 和行为

  • 父组件管数据,子组件管展示和事件


三、App.vue:数据中枢

1️⃣ 使用 refreactive

<script setup lang="ts">
import { ref, reactive } from 'vue'
import TodoInput from './components/TodoInput.vue'
import TodoList from './components/TodoList.vue'

// 输入框内容
const inputValue = ref('')

// 待办列表
const todos = reactive([
  { id: 1, text: '学习 Vue3', done: false },
  { id: 2, text: '写博客', done: true }
])

// 添加待办
const addTodo = () => {
  if (!inputValue.value.trim()) return

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

  inputValue.value = ''
}

// 删除待办
const removeTodo = (id: number) => {
  const index = todos.findIndex(t => t.id === id)
  if (index > -1) todos.splice(index, 1)
}
</script>

2️⃣ 模板部分

<template>
  <div class="app">
    <h1>Vue3 TodoList</h1>

    <TodoInput
      v-model:value="inputValue"
      @add="addTodo"
    />

    <TodoList
      :todos="todos"
      @remove="removeTodo"
    />

    <p>共 {{ todos.length }} 条待办</p>
  </div>
</template>

四、TodoInput.vue:输入组件

<script setup lang="ts">
defineProps<{
  value: string
}>()

const emit = defineEmits<{
  (e: 'update:value', value: string): void
  (e: 'add'): void
}>()
</script>

<template>
  <div class="todo-input">
    <input
      :value="value"
      @input="e => $emit('update:value', (e.target as HTMLInputElement).value)"
      placeholder="请输入待办事项"
    />
    <button @click="$emit('add')">添加</button>
  </div>
</template>

📌 重点:

  • 使用 v-model拆解写法

  • 子组件不直接改 props


五、TodoList.vue:列表容器

<script setup lang="ts">
defineProps<{
  todos: Array<{
    id: number
    text: string
    done: boolean
  }>
}>()

const emit = defineEmits<{
  (e: 'remove', id: number): void
}>()
</script>

<template>
  <ul class="todo-list">
    <li v-for="todo in todos" :key="todo.id">
      <TodoItem
        :todo="todo"
        @remove="emit('remove', todo.id)"
      />
    </li>
  </ul>
</template>

六、TodoItem.vue:单条待办

<script setup lang="ts">
defineProps<{
  todo: {
    id: number
    text: string
    done: boolean
  }
}>()

const emit = defineEmits<{
  (e: 'remove'): void
}>()
</script>

<template>
  <div class="todo-item">
    <span :class="{ done: todo.done }">
      {{ todo.text }}
    </span>
    <button @click="$emit('remove')">删除</button>
  </div>
</template>

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

七、模板语法快速复习表

语法

作用

{{ }}

插值表达式

v-model

双向绑定

v-bind:/ :

属性绑定

v-on:/ @

事件绑定

v-for

循环渲染

v-if / v-show

条件渲染


八、ref 与 reactive 实战小结

API

适用场景

ref

基本类型、DOM 引用

reactive

对象、数组

.value

ref在 JS 中必须写

📌 记住一句话:

基本类型用 ref,复杂对象用 reactive。

📢 下期预告

第 05 篇:Vue3 响应式原理初探 —— 从 Object.defineProperty 到 Proxy

更多推荐