本文最后更新于 2024年12月20日 下午
旅行清单(Travel-List) 功能和知识点蛮齐全的一个 app 页面
代码及功能解析 文件概览
页面展示
(1)App.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import { useState } from "react" ;import Logo from "./Logo" ;import Form from "./Form" ;import PackingList from "./PackingList" ;import Stats from "./Stats" ;export default function App ( ) { const [items, setItems] = useState ([]); function handleAddItems (item ) { setItems ((items ) => [...items, item]); } function handleDeleteItem (id ) { setItems ((items ) => items.filter ((item ) => item.id !== id)); } function handleToggleItem (id ) { setItems ((items ) => items.map ((item ) => item.id === id ? { ...item, packed : !item.packed } : item ) ); } function handleClearList ( ) { const confirmed = window .confirm ( "Are you sure you want to delete all items?" ); if (confirmed) setItems ([]); } return ( <div className ="app" > <Logo /> <Form onAddItems ={handleAddItems} /> <PackingList items ={items} onDeleteItem ={handleDeleteItem} onToggleItem ={handleToggleItem} onClearList ={handleClearList} /> <Stats items ={items} /> </div > ); }
解释 (1)handleToggleItem 函数用 map 更新 items 数组中特定元素:
1、不可变性原则 在 React 中,状态是不可变的。我们不能直接修改原始状态对象,而是要基于原始状态创建一个新的状态。map
方法可以创建一个新的数组,其中每个元素都是基于原数组中的元素返回的,但不直接修改原数组。
2、条件更新某一项 map
方法允许你遍历数组中的每个元素,并返回一个新数组。通过在 map
中对每个元素进行条件判断,我们可以只修改需要修改的那一项,而不影响其他项。
(2)handleClearList 函数实现清空列表功能,并通过用户确认来避免误操作:
**window.confirm
**:
是浏览器提供的一个方法,用来显示一个带有 确认消息 的对话框。
这个对话框有两个按钮:“确定” 和 “取消” 。
返回值:
如果用户点击 “确定” ,window.confirm
返回 true
。
如果用户点击 “取消” ,返回 false
。
**const confirmed = window.confirm(...)
**:
通过 window.confirm
获取用户的操作结果。
用户点击 “确定” ,confirmed
就是 true
。
用户点击 “取消” ,confirmed
就是 false
。
(3) 为什么 setItems([])
符合不可变性?
React 强调不可变性 ,是指我们不直接修改现有状态对象或数组 ,而是创建一个新对象或数组 并将其作为新的状态替代旧状态 。
1、原始状态不会被修改 :
假设当前 items
状态是一个非空数组 [item1, item2, item3]
。
调用 setItems([])
后,React 直接将新的状态 []
(一个新的空数组)替换原来的 items
。
原始数组 [item1, item2, item3]
仍然保留在内存中,并不会被直接修改。
2、创建了一个新数组 :
(4)return 里几个子组件:
举个例子:
<Form onAddItems={handleAddItems} />
!!!各组件和 props 的关系总结 !!!
父组件(App 组件)负责管理状态 (items
)和核心逻辑(如 handleAddItems
、handleDeleteItem
)。
子组件(如 Form、PackingList、Stats)负责 UI 和用户交互 。
通过 props 将父组件的函数和数据传递给子组件 ,子组件通过调用这些函数与父组件通信,实现状态的更新。
(2)Logo.js 1 2 3 export default function Logo ( ) { return <h1 > ⛰️ Far Away 💼</h1 > ; }
Windows 系统 VSCode 输入 emjoy 表情的快捷键:Windows+. (点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import { useState } from "react" ;export default function Form ({ onAddItems } ) { const [description, setDescription] = useState ("" ); const [quantity, setQuantity] = useState (1 ); function handleSubmit (e ) { e.preventDefault (); if (!description) return ; const newItem = { description, quantity, packed : false , id : Date .now () }; onAddItems (newItem); setDescription ("" ); setQuantity (1 ); } return ( <form className ="add-form" onSubmit ={handleSubmit} > <h3 > What do you need for your trip?😘</h3 > <select value ={quantity} //下拉框默认显示值为quantity值 ,初始为1 onChange ={(e) => setQuantity(Number(e.target.value))} > {Array.from({ length: 20 }, (_, i) => i + 1).map((num) => ( <option value ={num} key ={num} > {num} //勾选后此num为select当前值 </option > ))} </select > <input type ="text" placeholder ="Item..." value ={description} onChange ={(e) => setDescription(e.target.value)} ></input > <button > Add</button > </form > ); }
解释 (1)e.preventDefault()
:
用于阻止浏览器执行事件的默认行为。在表单提交时,默认行为是刷新页面或跳转到指定的 URL。通过调用 e.preventDefault()
,可以阻止这种默认行为,使得表单提交时不会导致页面刷新闪烁,从而允许执行自定义的逻辑,例如处理数据或更新状态,而不丢失当前的页面状态。
(2)onAddItems(newItem)
:
调用父组件传递的onAddItems
函数,将newItem
传递过去。
(3)<select>
:
提供一个下拉列表,用户可以选择 1 到 20 之间的数量。
**value={quantity}
**:下拉框的当前值绑定到quantity
状态。
**onChange
**:每当用户选择不同的值时,setQuantity
会将值更新为数字类型。
(4){Array.from(…).map(…)}:
用花括号包裹是因为在HTML 标签 内增加JavaScript 语言(以下两个)时需要。
Array.from
的作用
Array.from
用于创建一个新数组。语法如下:
1 Array .from ({ length : n }, (element, index ) => )
**{ length: 20 }
**:表示创建一个长度为 20 的数组。数组的每个位置初始值为undefined
。
(_, i) => i + 1
:生成器函数,用于返回新数组的每个元素。
_
:占位符,表示当前元素的值(此处没用)。
i
:当前元素的索引。
i + 1
:生成从 1 到 20 的连续整数。
.map
的作用
.map
方法用于对数组中的每个元素执行一个函数,并返回一个新数组。
遍历数组[1, 2, 3, ..., 20]
。
对于每个num
(即 1 到 20 的数字),生成一个<option>
元素。
1 2 3 <option value={num} key={num}> {num} </option>
**value={num}
**:设置<option>
的value
属性为num
。
**key={num}
**:React 要求数组渲染时每个元素必须有唯一的key
,方便 React 高效更新。
**{num}
**:<option>
的内容是num
。
(4)PackingList.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import { useState } from "react" ;import Item from "./Item" ;export default function PackingList ({ items, onDeleteItem, onToggleItem, onClearList, } ) { const [sortBy, setSortBy] = useState ("input" ); let sortedItems; if (sortBy === "input" ) sortedItems = items; if (sortBy === "description" ) sortedItems = items .slice () .sort ((a, b ) => a.description .localeCompare (b.description )); if (sortBy === "packed" ) sortedItems = items .slice () .sort ((a, b ) => Number (a.packed ) - Number (b.packed )); return ( <div className ="list" > <ul > {sortedItems.map((item) => ( <Item item ={item} onDeleteItem ={onDeleteItem} onToggleItem ={onToggleItem} key ={item.id} /> //Item:组件component名;item:道具props名;{item}:对象本身 ))} </ul > <div className ="actions" > <select value ={sortBy} onChange ={(e) => setSortBy(e.target.value)}> <option value ="input" > Sort by input order</option > <option value ="description" > Sort by description</option > <option value ="packed" > Sort by packed status</option > </select > <button onClick ={onClearList} > Clear list</button > </div > </div > ); }
解释 (1)物品列表排序:
slice()
:
用来创建一个新的数组副本,避免直接修改原数组(items
)。
React 等框架中建议避免直接改变状态,确保数据的不可变性。
localeCompare
:
Number(a.packed)
:
将布尔值转为数字进行排序:false
为 0
,true
为 1
,未打包物品排在已打包物品前面。
(2)<ul>....</ul>
:
将每个sortedItems
数组中的元素渲染成一个<Item />
组件,并将相关数据和回调函数作为props
传递给<Item />
组件。
(5)Item.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export default function Item ({ item, onDeleteItem, onToggleItem } ) { return ( <li > <input type ="checkbox" //定义复选框 value ={item.packed} //复选框的值绑定到item.packed onChange ={() => { //当复选框状态改变时(点击复选框)触发 onToggleItem(item.id); //调用onToggleItem,并传递当前item的id }} /> <span style ={item.packed ? { textDecoration: "line-through " } : {}}> {item.quantity} {item.description} </span > <button onClick ={() => onDeleteItem(item.id)}>❌</button > </li > ); }
(6)Stats.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export default function Stats ({ items } ) { if (!items.length ) return ( <p className ="stats" > <em > Start adding some items to your packing list🚀</em > </p > ); const numItems = items.length ; const numPacked = items.filter ((item ) => item.packed ).length ; const percentage = Math .round ((numPacked / numItems) * 100 ); return ( <footer className ="stats" > <em > {percentage === 100 ? "You got everything!Ready to go ✈️" : `💼You have ${numItems} items on your list,and you already packed ${numPacked} (${percentage}%)`} </em > </footer > ); }
解释 (1)反引号(``)用于模板字符串:
模板字符串允许在字符串中嵌入变量或表达式,而不需要手动拼接。它的语法是:
**${}
**:插值语法,用于将变量或表达式的值嵌入到字符串中。
**反引号 ``**:可以包含多行字符串和变量表达式,且更直观、易读。
如果不用模板字符串,可以改成这样(不易读):
1 2 3 4 5 6 7 "💼 You have " + numItems + " items on your list, and you already packed " + numPacked + " (" + percentage + "%)" ;
写在最后 最初是把所有组件放在一个文件里,但过于冗杂,于是创建了一个components 文件夹 存放所有组件。把一个文件里的每个组件进行剥离的快捷方式:
(1) 收缩一段组件代码,右键选择重构
(2) 重构后显示如下,选择move to a new file
在文件夹中立马有新的文件生成,且文件名和组件名相同,会在父组件里自动添加import {组件名} from “./文件名” ,此组件函数前会加上export 。但 jonas 老师更喜欢另一种方式:父组件里去掉组件名的花括号,此组件函数前加上export default 。
完结~撒花★,° :.☆( ̄ ▽  ̄)/$:.°★ 。😋