Vue基础知识-脚手架开发-通过TodoList案例深入理解props单向数据流、组件通信、computed计算属性、watch深度监听等概念。

一、项目概况

1. 功能清单

  • ✅ 任务添加:输入任务名称,按回车确认添加
  • ✅ 任务列表:渲染所有任务,显示任务状态(已完成 / 未完成)
  • ✅ 任务操作:单个任务删除、单个任务状态切换(已完成 / 未完成)
  • ✅ 全选 / 取消全选:一键切换所有任务状态
  • ✅ 统计功能:显示 “已完成任务数 / 总任务数”
  • ✅ 本地存储:任务数据持久化(刷新页面不丢失)
  • ✅ 边界处理:空任务拦截、删除确认、无任务时隐藏底部统计

2. 组件结构

采用 “父子组件” 分层设计,App 作为根组件统一管理数据,子组件负责具体功能,结构如下:

App(父组件)→ 管理核心数据items,提供操作方法
├─ MyHeader(子组件)→ 任务添加
├─ MyList(子组件)→ 任务列表容器
│  └─ MyItem(子组件)→ 单个任务项(渲染+删除+状态切换)
└─ MyFooter(子组件)→ 全选+统计+清除已完成任务

二、完整代码实现

1. 入口文件:main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
	el:'#app',
	render: h => h(App)
})

2. 根组件:App.vue(核心数据管理)

App 是父组件,负责存储任务数据items,并提供操作数据的方法(添加、删除、状态切换等),通过props传递给子组件。

<template>
	<div id="root">
		<div class="todo-container">
			<div class="todo-wrap">
			<!-- 
				<MyHeader/>、<MyList/>、<MyFooter/>三个兄弟组件都需用到items。因此item数据可定义在<App/>并由父组件实现数据:
					1 父传子:传递子组件data数据(props实现)
					2 子传父:传递子组件回调函数(props实现)
			 -->
				<MyHeader :addItem="addItem"/>
				<MyList :items="items" :changeCheck="changeCheck" :del="del"/>
				<MyFooter :items="items" :changeAllCheck="changeAllCheck" :del="del"/>
			</div>
		</div>
	</div>
</template>

<script>
	import MyHeader from './components/MyHeader.vue';
	import MyFooter from './components/MyFooter.vue';
	import MyList from './components/MyList.vue';
	export default{
		name:'App',
		components:{
			MyFooter,
			MyHeader,
			MyList
		},
		data() {
			return {
				items: JSON.parse(localStorage.getItem('items')) || [] //第一次调用localStorage.getItem('items')为null
				//复杂写法items:JSON.parse(localStorage.getItem('items'))?JSON.parse(localStorage.getItem('items')):[]
			}
		},
		//addItem()执行->items改变->模板重新解析->MyList组件里的items变(又因为props有响应式)->MyList组件重新解析
		methods:{
			addItem(item){
				this.items.unshift(item)
			},
			//函数写在哪里遵循原则:要修改的数据在哪,函数在哪
			changeCheck(id){
				this.items.forEach(item => {
					if(item.id === id) item.isChecked = !item.isChecked
				});
			},
			del(id){
				this.items = this.items.filter(item=>item.id!==id)
			},
			changeAllCheck(isChecked){
				this.items.forEach(item => {
					item.isChecked = isChecked
				});
			}
		},
		watch:{
			items:{
				//监视items变化并存入localStorage
				deep:true,
				handler(val){
					localStorage.setItem('items',JSON.stringify(val))
				}
			}
		}
	}
</script>

<style>
	/*base*/
	body {
		background: #fff;
	}
	.btn {
		display: inline-block;
		padding: 4px 12px;
		margin-bottom: 0;
		font-size: 14px;
		line-height: 20px;
		text-align: center;
		vertical-align: middle;
		cursor: pointer;
		box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
		border-radius: 4px;
	}
	.btn-danger {
		color: #fff;
		background-color: #da4f49;
		border: 1px solid #bd362f;
	}
	.btn-danger:hover {
		color: #fff;
		background-color: #bd362f;
	}
	.btn:focus {
		outline: none;
	}
	.todo-container {
		width: 600px;
		margin: 0 auto;
	}
	.todo-container .todo-wrap {
		padding: 10px;
		border: 1px solid #ddd;
		border-radius: 5px;
	}
</style>

3. 任务添加组件:MyHeader.vue

负责输入任务名称,按回车触发添加逻辑,通过props接收父组件的addItem方法,将新任务传递给父组件。

<template>
	<div class="todo-header">
		<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="name" @keyup.enter="newItem"/>
	</div>
</template>

<script>
	export default {
		/* 注意点:data,props,methods,computed中的名字不能重复。因为这些属性方法最终出现在vc或vm身上 */
		name:'MyHeader',
		data() {
			return {
				name:''
			}
		},
		props:['addItem'],
		methods:{
			newItem(){
				if(!this.name.trim()) return alert('输入不能为空')
				const item = {id:crypto.randomUUID(),name:this.name,isChecked:false}
				//注意这里子组件虽然调用了父组件的addItem()但是执行addItem()时里面的this依然是父组件
				this.addItem(item)
				this.name = ''
			}
		}
	}
</script>

<style scoped>
	/*header*/
	.todo-header input {
		width: 560px;
		height: 28px;
		font-size: 14px;
		border: 1px solid #ccc;
		border-radius: 4px;
		padding: 4px 7px;
	}

	.todo-header input:focus {
		outline: none;
		border-color: rgba(82, 168, 236, 0.8);
		box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
	}
</style>

4. 任务列表容器:MyList.vue

作为任务列表的 “容器”,接收父组件的items数据,通过v-for循环渲染MyItem子组件,同时传递任务数据和操作方法。

<template>
	<ul class="todo-main">
		<!-- 使用props给MyItem子组件传item对象 -->
		<MyItem v-for="
		item in items" 
		:key="item.id" 
		:item="item"
		:changeCheck="changeCheck"
		:del="del"
		/>
	</ul>
</template>

<script>
	import MyItem from './MyItem.vue';

	export default {
		name:'MyList',
		components:{
			MyItem
		},
		props:['items','changeCheck','del']
	}
</script>

<style scoped>
	/*main*/
	.todo-main {
		margin-left: 0px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding: 0px;
	}

	.todo-empty {
		height: 40px;
		line-height: 40px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding-left: 5px;
		margin-top: 10px;
	}

</style>

5. 单个任务项组件:MyItem.vue

渲染单个任务的内容、状态(复选框)和删除按钮,不直接修改props,通过调用父组件方法修改源数据(遵循单向数据流)。

<template>
	<div>
	<li >
		<label >
			<!--注意:v-model中绑定的不要是props的值。因为props建议只读-->
			<input type="checkbox" :checked="item.isChecked" @change="changeCheck(item.id)"/>
			<span>{{item.name}}</span>
		</label>
		<button class="btn btn-danger" @click="handleDel(item.id)">删除</button>
	</li>
	</div>
</template>

<script>
	export default {
		name:'MyItem',
		//props是响应式的。但是应遵循单项数据流原则,即不改其值。因此item值应该由父组件统一来改便于后期调试
		props:['item','changeCheck','del'],
		methods:{
			handleDel(id){
				if(confirm('确定删除?'))
					this.del(id)
			}
		}
	}
</script>

<style scoped>
		/*item*/
		li {
		list-style: none;
		height: 36px;
		line-height: 36px;
		padding: 0 5px;
		border-bottom: 1px solid #ddd;
	}

	li label {
		float: left;
		cursor: pointer;
	}

	li label li input {
		vertical-align: middle;
		margin-right: 6px;
		position: relative;
		top: -1px;
	}

	li button {
		float: right;
		display: none;
		margin-top: 3px;
	}

	li:before {
		content: initial;
	}

	li:last-child {
		border-bottom: none;
	}

	li:hover{
		background-color: #ddd;
	}
	
	li:hover button{
		display: block;
	}
</style>

6. 底部统计组件:MyFooter.vue

实现全选 / 取消全选、已完成任务统计、清除已完成任务功能,通过computed计算全选状态和任务数量。

<template>
	<div class="todo-footer" v-show="total">
		<label>
			 <input type="checkbox" v-model="isAll"/>
		</label>
		<span>
			<span>已完成{{ checkNum }}</span> / 全部{{ total }}
		</span>
		<button class="btn btn-danger" @click="clearFinishedItems">清除已完成任务</button>
	</div>
</template>

<script>
	export default {
		name:'MyFooter',
		props:['items','changeAllCheck','del'],
		computed:{
			isAll:{
				get(){
					return this.checkNum === this.total && this.total > 0
				},
				//set必须写上。因为勾选操作修改了isAll的值
				set(val){
					this.changeAllCheck(val)
				}
			},
			total(){
				return this.items.length
			},
			checkNum(){
				return this.items.filter(item=>item.isChecked).length
			}
		},
		methods: {
			clearFinishedItems(){
				this.items.forEach(item => {
					if(item.isChecked)
						this.del(item.id)
				});
			}
		},
	}
</script>

<style scoped>
	/*footer*/
	.todo-footer {
		height: 40px;
		line-height: 40px;
		padding-left: 6px;
		margin-top: 5px;
	}

	.todo-footer label {
		display: inline-block;
		margin-right: 20px;
		cursor: pointer;
	}

	.todo-footer label input {
		position: relative;
		top: -1px;
		vertical-align: middle;
		margin-right: 5px;
	}

	.todo-footer button {
		float: right;
		margin-top: 5px;
	}
</style>

三、核心知识点拆解

1. 组件通信:父传子 + 子传父

TodoList 的组件通信完全遵循 Vue 的单向数据流原则:

  • 父传子:通过props传递数据(如items)和方法(如addItem),子组件通过props接收后使用。
  • 子传父:子组件不直接修改props,而是通过调用父组件传递的回调函数(如addItemdel),将数据传递给父组件,由父组件修改源数据。

示例:MyHeader 添加任务时,通过this.addItem(newTask)调用父组件方法,将新任务传递给 App,App 修改items后,子组件(MyList、MyFooter)通过props响应式自动更新。

2. computed 计算属性:简化复杂逻辑

  • 全选状态(isAll):get 方法计算 “已完成数 === 总任务数”,set 方法响应复选框变化,调用父组件全选方法。
  • 任务统计(total、checkNum):动态计算总任务数和已完成数,避免在模板中写复杂表达式(如{{ items.filter(...) }})。

关键isAll必须写set方法,因为v-model绑定后,复选框的勾选操作会修改isAll的值,触发set,进而通知父组件更新所有任务状态。

3. watch 监听:本地存储同步

通过watch深度监听items变化(数组内对象属性的变化需要deep: true),将最新的items同步到localStorage,实现数据持久化:

watch: {
  items: {
    deep: true, // 深度监听
    handler(newVal) {
      localStorage.setItem('items', JSON.stringify(newVal))
    }
  }
}
  • 页面加载时,从localStorage读取数据:items: JSON.parse(localStorage.getItem('items')) || []
  • 注意:localStorage只能存储字符串,需用JSON.stringifyJSON.parse处理对象 / 数组。

4. 单向数据流:子组件不直接修改 props

Vue 规定:子组件不能直接修改 props 传递的数据,否则会触发警告且导致数据流向混乱。项目中通过以下方式遵循这一原则:

  • MyItem 组件:用:checked="item.isChecked"绑定状态,而非v-model,通过@change触发父组件changeCheck方法修改源数据。
  • MyFooter 组件:全选状态isAll的修改通过set方法调用父组件changeAllCheck,不直接修改items

5. 事件处理与表单绑定

  • 表单绑定:MyHeader 的输入框用v-model绑定name,实现双向数据绑定(仅组件内部使用,不涉及 props)。
  • 键盘事件@keyup.enter监听回车,触发任务添加。
  • 鼠标事件@change监听复选框状态变化,@click监听删除和全选操作,v-show/v-if控制元素显示隐藏。

四、项目运行流程

  1. 初始化:页面加载时,App 从localStorage读取任务数据,渲染 MyList(有数据则显示任务,无数据则显示提示)。
  2. 添加任务:MyHeader 输入任务名→按回车→调用addItemApp 的items更新→MyList 自动重新渲染→新任务显示在列表顶部。
  3. 切换状态:MyItem 勾选复选框→调用changeCheckApp 修改对应items的isChecked→MyItem 和 MyFooter(统计数、全选状态)自动更新。
  4. 全选操作:MyFooter 勾选全选框→触发isAllset→调用changeAllCheckApp 修改所有items的isChecked→所有 MyItem 的复选框同步状态。
  5. 删除任务:MyItem 点击删除→确认后调用delApp 删除item→MyList 和 MyFooter 自动更新。
  6. 本地存储:每次items变化(添加、删除、状态切换)→watch触发→同步到localStorage→刷新页面数据不丢失。

五、注意事项

  • ID 唯一性:用crypto.randomUUID()生成唯一 ID,兼容性良好(IE 不支持,可替换为Date.now() + Math.random())。
  • 命名冲突:组件的datapropsmethodscomputed命名不能重复,因为最终都会挂载到组件实例(vc)上。
  • 边界处理:空任务拦截、删除确认、无任务隐藏底部,提升用户体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值