东莞企业营销型网站建设,wordpress 仿站 教程,wordpress获取文章块,支付单页网站模板文章目录 React组件:万物皆组件 JSX: 将标签引入 JavaScriptJSX 规则1. 只能返回一个根元素2. 标签必须闭合3. 使用驼峰式命名法给 ~~所有~~ 大部分属性命名#xff01;高级提示#xff1a;使用 JSX 转化器 在 JSX 中通过大括号使用 JavaScript使用引号传递字符串使用大括号高级提示使用 JSX 转化器 在 JSX 中通过大括号使用 JavaScript使用引号传递字符串使用大括号一扇进入 JavaScript 世界的窗户可以在哪使用大括号使用 “双大括号”JSX 中的 CSS 和 对象 将 Props 传递给组件熟悉的 props向组件传递 props步骤 1: 将 props 传递给子组件步骤 2: 在子组件中读取 props给 prop 指定一个默认值使用 JSX 展开语法传递 propsProps 如何随时间变化 条件渲染条件返回 JSX选择性地返回 null三目运算符? :与运算符选择性地将 JSX 赋值给变量 渲染列表从数组中渲染数据用 key 保持列表项的顺序如何设定 key 值 保持组件纯粹纯函数组件作为公式副作用不符合预期的后果使用严格模式检测不纯的计算 哪些地方 **可能** 引发副作用 将 UI 视为树添加交互响应事件添加事件处理函数在事件处理函数中读取 props将事件处理函数作为 props 传递事件传播阻止传播阻止默认行为 组件的记忆 **state**当普通的变量无法满足时添加一个 state 变量遇见你的第一个 Hook剖析 useState赋予一个组件多个 state 变量State 是隔离且私有的 渲染和提交步骤 1: 触发一次渲染状态更新时重新渲染步骤 2: React 渲染你的组件步骤 3: React 把更改提交到 DOM 上尾声浏览器绘制 state 如同一张快照设置 state 会触发渲染把一系列 state 更新加入队列React 会对 state 更新进行批处理在下次渲染前多次更新同一个 state 更新 state 中的对象将 state 视为只读的局部 mutation 是可以接受的 使用展开语法复制对象使用一个事件处理函数来更新多个字段 更新一个嵌套对象对象并非是真正嵌套的 使用 Immer 编写简洁的更新逻辑通过使用 Immer你写出的代码看起来就像是你“打破了规则”而直接修改了对象 更新 state 中的数组在没有 mutation 的前提下更新数组向数组中添加元素 头部和尾部从数组中删除元素转换数组替换数组中的元素向数组中插入元素Slice()方法详解 其他改变数组的情况更新数组内部的对象使用 Immer 编写简洁的更新逻辑 以下内容均来自官方文档https://zh-hans.react.dev/learn React组件:
React 允许你将标签、CSS 和 JavaScript 组合成自定义“组件”即 **应用程序中可复用的 UI 元素**React 组件是常规的 JavaScript 函数但 组件的名称必须以大写字母开头否则它们将无法运行
export default function Profile() {return (imgsrchttps://i.imgur.com/MK3eW3Am.jpgaltKatherine Johnson/)
}
返回语句可以全写在一行上如下面组件中所示
return img srchttps://i.imgur.com/MK3eW3As.jpg altKatherine Johnson /;但是如果你的标签和 return 关键字不在同一行则必须把它包裹在一对括号中如下所示
return (divimg srchttps://i.imgur.com/MK3eW3As.jpg altKatherine Johnson //div);没有括号包裹的话任何在 return 下一行的代码都 将被忽略
组件可以渲染其他组件但是 请不要嵌套他们的定义
export default function Gallery() {// 永远不要在组件中定义组件function Profile() {// ...}// ...}当子组件需要使用父组件的数据时你需要 通过 props 的形式进行传递而不是嵌套定义。 万物皆组件
组件导入与导出
默认导出 export default 一个文件只能有一个默认导入如下代码块所示引入过程中你可能会遇到一些文件并未添加 .js 文件后缀如下所示
import Gallery from ./Gallery;无论是 ‘./Gallery.js’ 还是 ‘./Gallery’在 React 里都能正常使用只是前者更符合 原生 ES 模块。
同一文件中有且仅有一个默认导出但可以有多个具名导出首先用具名导出的方式将 Profile 组件从 Gallery.js导出不使用 default 关键字
export function Profile() {// ...}接着用具名导入的方式从 Gallery.js 文件中 导入Profile 组件用大括号:
import { Profile } from ./Gallery.js;最后在 App 组件里 渲染
export default function App() {return Profile /;}使用JSX书写标签语言 JSX: 将标签引入 JavaScript
JSX and React 是相互独立的 东西。但它们经常一起使用但你 可以 单独使用它们中的任意一个JSX 是一种语法扩展而 React 则是一个 JavaScript 的库 JSX 规则 1. 只能返回一个根元素
如果想要在一个组件中包含多个元素需要用一个父标签把它们包裹起来。
divh1海蒂·拉玛的待办事项/h1img srchttps://i.imgur.com/yXOvdOSs.jpg altHedy Lamarr classphotoul.../ul
/div如果你不想在标签中增加一个额外的
可以用 和 / 元素来代替 2. 标签必须闭合
JSX 要求标签必须正确闭合。像 这样的自闭合标签必须书写成 而像
oranges 这样只有开始标签的元素必须带有闭合标签需要改为 oranges。 3. 使用驼峰式命名法给 所有 大部分属性命名 JSX 最终会被转化为 JavaScript而 JSX 中的属性也会变成 JavaScript 对象中的键值对。在你自己的组件中经常会遇到需要用变量的方式读取这些属性的时候。但 JavaScript 对变量的命名有限制。例如变量名称不能包含 - 符号或者像 class 这样的保留字。由于 class 是一个保留字所以在 React 中需要用 className 来代替
高级提示使用 JSX 转化器
将现有的 HTML 中的所有属性转化 JSX 的格式是很繁琐的。我们建议使用 转化器 将 HTML 和 SVG 标签转化为 JSX。这种转化器在实践中非常有用。但我们依然有必要去了解这种转化过程中发生了什么这样你就可以编写自己的 JSX 了。 在 JSX 中通过大括号使用 JavaScript 使用引号传递字符串
当你想把一个字符串属性传递给 JSX 时把它放到单引号或双引号中
export default function Avatar() {return (imgclassNameavatarsrchttps://i.imgur.com/7vQD0fPs.jpgaltGregorio Y. Zara/);
}但是如果你想要动态地指定 src 或 alt 的值呢你可以 用 { 和 } 替代 和 以使用 JavaScript 变量
export default function Avatar() {const avatar https://i.imgur.com/7vQD0fPs.jpg;const description Gregorio Y. Zara;return (imgclassNameavatarsrc{avatar}alt{description}/);
}使用大括号一扇进入 JavaScript 世界的窗户 可以在哪使用大括号
在 JSX 中只能在以下两种场景中使用大括号
用作 JSX 标签内的文本{name}s To Do List 是有效的但是 {tag}Gregorio Y. Zara’s To Do List/{tag} 无效。用作紧跟在 符号后的 属性src{avatar} 会读取 avatar 变量但是 src“{avatar}” 只会传一个字符串 {avatar}。
使用 “双大括号”JSX 中的 CSS 和 对象
你甚至可以在 JSX 中传递对象。对象也用大括号表示例如 { name: “Hedy Lamarr”, inventions: 5 }。因此为了能在 JSX 中传递你必须用另一对额外的大括号包裹对象person{{ name: “Hedy Lamarr”, inventions: 5 }}。
// 注意style用了两层大括号
export default function TodoList() {return (ul style{{backgroundColor: black,color: pink}}liImprove the videophone/liliPrepare aeronautics lectures/liliWork on the alcohol-fuelled engine/li/ul);
}内联 style 属性 使用驼峰命名法编写。例如HTML
在你的组件里应该写成 ul style{{ backgroundColor: ‘black’ }}。 const baseUrl https://i.imgur.com/;
const person {name: Gregorio Y. Zara,imageId: 7vQD0fP,imageSize: s,theme: {backgroundColor: black,color: pink}
};export default function TodoList() {return (div style{person.theme}h1{person.name}s Todos/h1imgclassNameavatarsrc{baseUrlperson.imageIdperson.imageSize.jpg} //注意这行alt{person.name}/ulliImprove the videophone/liliPrepare aeronautics lectures/liliWork on the alcohol-fuelled engine/li/ul/div);
} 将 Props 传递给组件
React 组件使用 props 来互相通信。每个父组件都可以提供 props 给它的子组件从而将一些信息传递给它。Props 可能会让你想起 HTML 属性但你可以通过它们传递任何 JavaScript 值包括对象、数组和函数
熟悉的 props
Props 是你传递给 JSX 标签的信息。例如className、src、alt、width 和 height 便是一些可以传递给 的 props
向组件传递 props
你可以分两步给 Avatar 一些 props。
步骤 1: 将 props 传递给子组件
首先将一些 props 传递给 Avatar。例如让我们传递两个 propsperson一个对象和 size一个数字
export default function Profile() {return (Avatarperson{{ name: Lin Lanying, imageId: 1bX5QH6 }}size{100}/);
}步骤 2: 在子组件中读取 props
你可以通过在 function Avatar 之后直接列出它们的名字 person, size 来读取这些 props。这些 props 在 ({ 和 }) 之间并由逗号分隔。这样你可以在 Avatar 的代码中使用它们就像使用变量一样。
function Avatar({ person, size }) {// 在这里 person 和 size 是可访问的return (imgclassNameavatarsrc{getImageUrl(person)} //写的不全,可以参考react文档alt{person.name}width{size}height{size}/);
}Props 使你独立思考父组件和子组件。 例如你可以改变 Profile 中的 person 或 size props而无需考虑 Avatar 如何使用它们。 同样你可以改变 Avatar 使用这些 props 的方式不必考虑 Profile
你可以将 props 想象成可以调整的“旋钮”。它们的作用与函数的参数相同 —— 事实上props 正是 组件的唯一参数 React 组件函数接受一个参数一个 props 对象
function Avatar(props) {let person props.person;let size props.size;// ...}在声明 props 时 不要忘记 ( 和 ) 之间的一对花括号 { 和 }
function Avatar({ person, size }) {// ...}这种语法被称为 “解构”等价于于从函数参数中读取属性
function Avatar(props) {let person props.person;let size props.size;// ...}给 prop 指定一个默认值
如果你想在没有指定值的情况下给 prop 一个默认值你可以通过在参数后面写 和默认值来进行解构
function Avatar({ person, size 100 }) {// ...}现在 如果 渲染时没有 size prop size 将被赋值为 100。默认值仅在缺少 size prop 或 size{undefined} 时生效。 但是如果你传递了 size{null} 或 size{0}默认值将 不 被使用。
使用 JSX 展开语法传递 props
有时候传递 props 会变得非常重复
function Profile({ person, size, isSepia, thickBorder }) {return (div classNamecardAvatarperson{person}size{size}isSepia{isSepia}thickBorder{thickBorder}//div);}所以使用更简洁的“展开”语法是有意义的
function Profile(props) {return (div classNamecardAvatar {...props} //div);}Props 如何随时间变化
一个组件可能会随着时间的推移收到不同的 props然而props 是 不可变的一个计算机科学术语意思是“不可改变”。当一个组件需要改变它的 props例如响应用户交互或新数据时它不得不“请求”它的父组件传递 不同的 props —— 一个新对象它的旧 props 将被丢弃最终 JavaScript 引擎将回收它们占用的内存。React文档例题抽取组件
import { getImageUrl } from ./utils.js;export function Person({name,Img,Profession,Awards,Discovered}){return(section classNameprofileh2{name}/h2imgclassNameavatarsrc{getImageUrl(Img.id)}alt{name}width{Img.size}height{Img.size}/ullibProfession: /b {Profession}/lilibAwards: {Awards.num} /b {Awards.total}/lilibDiscovered: /b{Discovered}/li/ul/section)}export default function Gallery() {return (divh1Notable Scientists/h1Person nameMaria Skłodowska-CurieImg{{ id: szV5sdG, size: 70 }}Professionphysicist and chemistAwards{{ num: 4, total: (Nobel Prize in Physics, Nobel Prize in Chemistry, Davy Medal, Matteucci Medal) }}Discoveredpolonium (chemical element)/PersonnameKatsuko SaruhashiImg{{ id: YfeOqp2, size: 70 }}ProfessiongeochemistAwards{{ num: 2, total: (Miyake Prize for geochemistry, Tanaka Prize) }}Discovereda method for measuring carbon dioxide in seawater/section classNameprofileh2Maria Skłodowska-Curie/h2imgclassNameavatarsrc{getImageUrl(szV5sdG)}altMaria Skłodowska-Curiewidth{70}height{70}/ullibProfession: /b physicist and chemist/lilibAwards: 4 /b (Nobel Prize in Physics, Nobel Prize in Chemistry, Davy Medal, Matteucci Medal)/lilibDiscovered: /bpolonium (chemical element)/li/ul/sectionsection classNameprofileh2Katsuko Saruhashi/h2imgclassNameavatarsrc{getImageUrl(YfeOqp2)}altKatsuko Saruhashiwidth{70}height{70}/ullibProfession: /b geochemist/lilibAwards: 2 /b (Miyake Prize for geochemistry, Tanaka Prize)/lilibDiscovered: /ba method for measuring carbon dioxide in seawater/li/ul/section/div);
}
export function Card({children}){return(div classNamecarddiv classNamecard-content{children}/div/div/)
}
export default function Profile() {return (divCardh1Photo/h1imgclassNameavatarsrchttps://i.imgur.com/OKS67lhm.jpgaltAklilu Lemmawidth{70}height{70}//CardCardh1About/h1pAklilu Lemma was a distinguished Ethiopian scientist who discovered a natural treatment to schistosomiasis./p/Carddiv classNamecarddiv classNamecard-contenth1Photo/h1imgclassNameavatarsrchttps://i.imgur.com/OKS67lhm.jpgaltAklilu Lemmawidth{70}height{70}//div/divdiv classNamecarddiv classNamecard-contenth1About/h1pAklilu Lemma was a distinguished Ethiopian scientist who discovered a natural treatment to schistosomiasis./p/div/div/div);
}条件渲染
通常你的组件会需要根据不同的情况显示不同的内容。在 React 中你可以通过使用 JavaScript 的 if 语句、 和 ? : 运算符来选择性地渲染 JSX
条件返回 JSX
function Item({ name, isPacked }) {if (isPacked) {return li classNameitem{name} ✔/li;}return li classNameitem{name}/li;
}export default function PackingList() {return (sectionh1Sally Ride 的行李清单/h1ulItem isPacked{true} name宇航服 /Item isPacked{true} name带金箔的头盔 /Item isPacked{false} nameTam 的照片 //ul/section);
} 选择性地返回 null
function Item({ name, isPacked }) {if (isPacked) {return null;}return li classNameitem{name}/li;
}export default function PackingList() {return (sectionh1Sally Ride 的行李清单/h1ulItem isPacked{true} name宇航服 /Item isPacked{true} name带金箔的头盔 /Item isPacked{false} nameTam 的照片 //ul/section);
}三目运算符? :
return (li classNameitem{isPacked ? name ✔ : name}/li
);与运算符
你会遇到的另一个常见的快捷表达式是 JavaScript 逻辑与运算符。在 React 组件里通常用在当条件成立时你想渲染一些 JSX或者不做任何渲染。使用 你也可以实现仅当 isPacked 为 true 时渲染勾选符号
return (li classNameitem{name} {isPacked ✔}/li
);切勿将数字放在 左侧.JavaScript 会自动将左侧的值转换成布尔类型以判断条件成立与否。然而如果左侧是 0整个表达式将变成左侧的值0React 此时则会渲染 0 而不是不进行渲染。例如一个常见的错误是 messageCount
New messages。其原本是想当 messageCount 为 0 的时候不进行渲染但实际上却渲染了 0。
为了更正可以将左侧的值改成布尔类型messageCount 0
New messages。 选择性地将 JSX 赋值给变量
这种方式是最冗长的但也是最灵活的
function Item({ name, isPacked }) {let itemContent name;if (isPacked) {itemContent name ✔;}return (li classNameitem{itemContent}/li);
}export default function PackingList() {return (sectionh1Sally Ride 的行李清单/h1ulItem isPacked{true} name宇航服 /Item isPacked{true} name带金箔的头盔 /Item isPacked{false} nameTam 的照片 //ul/section);
}
练习2
function Item({ name, importance }) {return (li classNameitem{name} {importance!0 i(重要性: {importance})/i}/li);
}export default function PackingList() {return (sectionh1Sally Ride 的行李清单/h1ulItem importance{9} name宇航服 /Item importance{0} name带金箔的头盔 /Item importance{6} nameTam 的照片 //ul/section);
}
练习3
function Drink({ name }) {let a,b,cif(name tea){a leafb15–70 mg/cupc4,000 years}else{a beanb80–185 mg/cupc1,000 years}return (sectionh1{name}/h1dldtPart of plant/dtdd{a}/dddtCaffeine content/dtdd{b}/dddtAge/dtdd{c}/dd/dl/section);
}export default function DrinkList() {return (divDrink nametea /Drink namecoffee //div);
}渲染列表 从数组中渲染数据
这里我们有一个列表。
ulli凯瑟琳·约翰逊: 数学家/lili马里奥·莫利纳: 化学家/lili穆罕默德·阿卜杜勒·萨拉姆: 物理学家/lili珀西·莱温·朱利亚: 化学家/lili苏布拉马尼扬·钱德拉塞卡: 天体物理学家/li/ul这里给出一个由数组生成一系列列表项的简单示例
首先把数据 存储 到数组中
const people [凯瑟琳·约翰逊: 数学家,马里奥·莫利纳: 化学家,穆罕默德·阿卜杜勒·萨拉姆: 物理学家,珀西·莱温·朱利亚: 化学家,苏布拉马尼扬·钱德拉塞卡: 天体物理学家,];遍历 people 这个数组中的每一项并获得一个新的 JSX 节点数组 listItems
const listItems people.map(person li{person}/li);把 listItems 用 包裹起来然后 返回 它
return ul{listItems}/ul;你只想显示 profession 值是 化学家 的人
首先创建 一个用来存化学家们的新数组 chemists这里用到 filter() 方法过滤 people 数组来得到所有的化学家过滤的条件应该是 person.profession ‘化学家’
const chemists people.filter(person person.profession 化学家);接下来 用 map 方法遍历 chemists 数组:
const listItems chemists.map(person liimgsrc{getImageUrl(person)}alt{person.name}/pb{person.name}:/b{ person.profession }因{person.accomplishment}而闻名世界/p/li);最后返回 listItems
return ul{listItems}/ul;用 key 保持列表项的顺序
如果把上面任何一个沙盒示例在新标签页打开你就会发现控制台有这样一个报错Warning: Each child in a list should have a unique “key” prop.这是因为你必须给数组中的每一项都指定一个 key——它可以是字符串或数字的形式只要能唯一标识出各个数组项就行
li key{person.id}.../li用作 key 的值应该在数据中提前就准备好而不是在运行时才随手生成
export const people [{id: 0, // 在 JSX 中作为 key 使用name: 凯瑟琳·约翰逊,profession: 数学家,accomplishment: 太空飞行相关数值的核算,imageId: MK3eW3A,},{id: 1, // 在 JSX 中作为 key 使用name: 马里奥·莫利纳,profession: 化学家,accomplishment: 北极臭氧空洞的发现,imageId: mynHUSa,},{id: 2, // 在 JSX 中作为 key 使用name: 穆罕默德·阿卜杜勒·萨拉姆,profession: 物理学家,accomplishment: 关于基本粒子间弱相互作用和电磁相互作用的统一理论,imageId: bE7W1ji,},{id: 3, // 在 JSX 中作为 key 使用name: 珀西·莱温·朱利亚,profession: 化学家,accomplishment: 开创性的可的松药物、类固醇和避孕药,imageId: IOjWm71,},{id: 4, // 在 JSX 中作为 key 使用name: 苏布拉马尼扬·钱德拉塞卡,profession: 天体物理学家,accomplishment: 白矮星质量计算,imageId: lrWQx8l,},
];//
import { people } from ./data.js;
import { getImageUrl } from ./utils.js;export default function List() {const listItems people.map(person li key{person.id}imgsrc{getImageUrl(person)}alt{person.name}/pb{person.name}/b{ person.profession }因{person.accomplishment}而闻名世界/p/li);return ul{listItems}/ul;
}
如果你想让每个列表项都输出多个 DOM 节点而非一个的话该怎么做呢Fragment 语法的简写形式 / 无法接受 key 值所以你只能要么把生成的节点用一个
标签包裹起来要么使用长一点但更明确的 写法 import { Fragment } from react;// ...const listItems people.map(person Fragment key{person.id}h1{person.name}/h1p{person.bio}/p/Fragment);如何设定 key 值
不同来源的数据往往对应不同的 key 值获取方式
来自数据库的数据 如果你的数据是从数据库中获取的那你可以直接使用数据表中的主键因为它们天然具有唯一性。本地产生数据 如果你数据的产生和保存都在本地例如笔记软件里的笔记那么你可以使用一个自增计数器或者一个类似 uuid 的库来生成 key。
练习1
import { people } from ./data.js;
import { getImageUrl } from ./utils.js;export default function List() {const chemists people.filter(person person.profession 化学家)const others people.filter(person person.profession ! 化学家)const Items1 chemists.map(person li key{person.id}imgsrc{getImageUrl(person)}alt{person.name}/pb{person.name}:/b{ person.profession }因{person.accomplishment}而闻名世界/p/li)const Items2 others.map(person li key{person.id}imgsrc{getImageUrl(person)}alt{person.name}/pb{person.name}:/b{ person.profession }因{person.accomplishment}而闻名世界/p/li)const listItems people.map(person li key{person.id}imgsrc{getImageUrl(person)}alt{person.name}/pb{person.name}:/b{ person.profession }因{person.accomplishment}而闻名世界/p/li);return (articleh1化学家/h1ul{Items1}/ul/articlearticleh1其他/h1ul{Items2}/ul/article/);
}//使用reduce进行优化
import { people } from ./data.js;
import { getImageUrl } from ./utils.js;export default function List() {const { chemists, others } people.reduce((result, person) {if (person.profession 化学家) {result.chemists.push(person);} else {result.others.push(person);}return result;},{ chemists: [], others: [] });const renderItems (people) {return people.map(person (li key{person.id}img src{getImageUrl(person)} alt{person.name} /pb{person.name}:/b{ person.profession }因{person.accomplishment}而闻名世界/p/li));};return (articleh1化学家/h1ul{renderItems(chemists)}/ul/articlearticleh1其他/h1ul{renderItems(others)}/ul/article/);
}练习2
import { recipes } from ./data.js;export default function RecipeList() {return (divh1菜谱/h1{recipes.map(recipe(div key{recipe.id}h2{recipe.name}/h2ul{recipe.ingredients.map((ingredient,index)(li key{index}{ingredient}/li))}/ul/div))}/div);
}export const recipes [{id: greek-salad,name: 希腊沙拉,ingredients: [西红柿, 黄瓜, 洋葱, 油橄榄, 羊奶酪],},{id: hawaiian-pizza,name: 夏威夷披萨,ingredients: [披萨饼皮, 披萨酱, 马苏里拉奶酪, 火腿, 菠萝],},{id: hummus,name: 鹰嘴豆泥,ingredients: [鹰嘴豆, 橄榄油, 蒜瓣, 柠檬, 芝麻酱],},
];
当你使用 () 包裹 JSX 时它会被视为一个表达式而当你使用 {} 时它将被视为一个代码块。如果你决定使用 {} 替换 ()你需要在代码块内显式地使用 return 关键字返回 JSX练习3
import { recipes } from ./data.js;
export function Recipe({id,name,ingredients}){return(h2{name}/h2ul{ingredients.map((ingredient,index)li key{index}{ingredient}/li)}/ul/)
}
export default function RecipeList() {return (divh1菜谱/h1{recipes.map(recipe Recipe key{recipe.id} {...recipe}/)}/div);
}
export const recipes [{id: greek-salad,name: 希腊沙拉,ingredients: [西红柿, 黄瓜, 洋葱, 油橄榄, 羊奶酪],},{id: hawaiian-pizza,name: 夏威夷披萨,ingredients: [披萨饼皮, 披萨酱, 马苏里拉奶酪, 火腿, 菠萝],},{id: hummus,name: 鹰嘴豆泥,ingredients: [鹰嘴豆, 橄榄油, 蒜瓣, 柠檬, 芝麻酱],},
];
练习4
import React from react
const poem {lines: [I write, erase, rewrite,Erase again, and then,A poppy blooms.]
};export default function Poem() {return (article{poem.lines.map((line, index) (React.Fragment key{index}p{line}/p{index ! poem.lines.length-1 hr/}/React.Fragment))}/article);
}保持组件纯粹
部分 JavaScript 函数是 纯粹 的这类函数通常被称为纯函数。纯函数仅执行计算操作不做其他操作
纯函数组件作为公式
在计算机科学中尤其是函数式编程的世界中纯函数 通常具有如下特征
只负责自己的任务。它不会更改在该函数调用前就已存在的对象或变量。输入相同则输出相同。给定相同的输入纯函数应总是返回相同的结果
副作用不符合预期的后果
React 的渲染过程必须自始至终是纯粹的。组件应该只 返回 它们的 JSX而不 改变 在渲染前就已存在的任何对象或变量 — 这将会使它们变得不纯粹
let guest 0;function Cup() {// Bad: changing a preexisting variable!guest guest 1;return h2Tea cup for guest #{guest}/h2;
}export default function TeaSet() {return (Cup /Cup /Cup //);
}
Tea cup for guest #2Tea cup for guest #4Tea cup for guest #6
使用严格模式检测不纯的计算
React 提供了 “严格模式”在严格模式下开发时它将会调用每个组件函数两次。通过重复调用组件函数严格模式有助于找到违反这些规则的组件严格模式在生产环境下不生效因此它不会降低应用程序的速度。如需引入严格模式你可以用 React.StrictMode 包裹根组件。一些框架会默认这样做 哪些地方 可能 引发副作用
函数式编程在很大程度上依赖于纯函数但 某些事物 在特定情况下不得不发生改变。这是编程的要义这些变动包括更新屏幕、启动动画、更改数据等它们被称为 副作用。它们是 “额外” 发生的事情与渲染过程无关如果你用尽一切办法仍无法为副作用找到合适的事件处理程序你还可以调用组件中的 useEffect 方法将其附加到返回的 JSX 中。这会告诉 React 在渲染结束后执行它
练习1
import React,{useState,useEffect} from react
export default function Clock({ time }) {const [isNight,setIsNight] useState(false)useEffect((){const hours time.getHours()setIsNight(hours0 hours 6)},[time])return (h1 idtime className{isNight? night : day}{time.toLocaleTimeString()}/h1);
}
练习2
import Panel from ./Panel.js;
import { getImageUrl } from ./utils.js;
import {useState} from reactexport default function Profile({ person }) {const [ currentPerson,setCurrentPerson] useState(person)return (PanelHeader c{currentPerson}/Avatar c{currentPerson}//Panel)
}function Header({c}) {return h1{c.name}/h1;
}function Avatar({c}) {return (imgclassNameavatarsrc{getImageUrl(c)}alt{c.name}width{50}height{50}/);
}import Profile from ./Profile.js;export default function App() {return (Profile person{{imageId: lrWQx8l,name: Subrahmanyan Chandrasekhar,}} /Profile person{{imageId: MK3eW3A,name: Creola Katherine Johnson,}} //)
}练习3
export default function StoryTray({ stories }) {const newStories [...stories,{id: create,label: Create Story}]return (ul{newStories.map(story (li key{story.id}{story.label}/li))}/ul);
}将 UI 视为树
树是表示实体之间关系的常见方式它们经常用于建模 UI。渲染树表示单次渲染中 React 组件之间的嵌套关系。使用条件渲染渲染树可能会在不同的渲染过程中发生变化。使用不同的属性值组件可能会渲染不同的子组件。渲染树有助于识别顶级组件和叶子组件。顶级组件会影响其下所有组件的渲染性能而叶子组件通常会频繁重新渲染。识别它们有助于理解和调试渲染性能问题。依赖树表示 React 应用程序中的模块依赖关系。构建工具使用依赖树来捆绑必要的代码以部署应用程序。依赖树有助于调试大型捆绑包带来的渲染速度过慢的问题以及发现哪些捆绑代码可以被优化
添加交互 响应事件
React 允许你向 JSX 中添加事件处理程序。事件处理程序是你自己的函数它将在用户交互时被触发如点击、悬停、焦点在表单输入框上等等。 等内置组件只支持内置浏览器事件如 onClick。但是你也可以创建你自己的组件并给它们的事件处理程序 props 指定你喜欢的任何特定于应用的名称
添加事件处理函数
按照如下三个步骤即可让它在用户点击时显示消息
在 Button 组件 内部 声明一个名为 handleClick 的函数。实现函数内部的逻辑使用 alert 来显示消息。添加 onClick{handleClick} 到 JSX 中。
export default function Button() {function handleClick() {alert(你点击了我);}return (button onClick{handleClick}点我/button);
}事件处理函数有如下特点:
通常在你的组件 内部 定义。名称以 handle 开头后跟事件名称按照惯例通常将事件处理程序命名为 handle后接事件名。你会经常看到 onClick{handleClick}onMouseEnter{handleMouseEnter} 等。
或者你也可以在 JSX 中定义一个内联的事件处理函数
button onClick{function handleClick() {alert(你点击了我);}}或者直接使用更为简洁箭头函数
button onClick{() {alert(你点击了我);}}在事件处理函数中读取 props
由于事件处理函数声明于组件内部因此它们可以直接访问组件的 props。
function AlertButton({ message, children }) {return (button onClick{() alert(message)}{children}/button);
}export default function Toolbar() {return (divAlertButton message正在播放播放电影/AlertButtonAlertButton message正在上传上传图片/AlertButton/div);
} 将事件处理函数作为 props 传递
function Button({ onClick, children }) {return (button onClick{onClick}{children}/button);
}function PlayButton({ movieName }) {function handlePlayClick() {alert(正在播放 ${movieName});}return (Button onClick{handlePlayClick}播放 {movieName}/Button);
}function UploadButton() {return (Button onClick{() alert(正在上传)}上传图片/Button);
}export default function Toolbar() {return (divPlayButton movieName魔女宅急便 /UploadButton //div);
} 事件传播
事件处理函数还将捕获任何来自子组件的事件。通常我们会说事件会沿着树向上“冒泡”或“传播”它从事件发生的地方开始然后沿着树向上传播。
export default function Toolbar() {return (div classNameToolbar onClick{() {alert(你点击了 toolbar );}}button onClick{() alert(正在播放)}播放电影/buttonbutton onClick{() alert(正在上传)}上传图片/button/div);
}如果你点击任一按钮它自身的 onClick 将首先执行然后父级
的 onClick 会接着执行。因此会出现两条消息。如果你点击 toolbar 本身将只有父级 的 onClick 会执行 阻止传播
这个事件对象还允许你阻止传播。如果你想阻止一个事件到达父组件你需要像下面 Button 组件那样调用 e.stopPropagation()
function Button({ onClick, children }) {return (button onClick{e {e.stopPropagation();onClick();}}{children}/button);
}export default function Toolbar() {return (div classNameToolbar onClick{() {alert(你点击了 toolbar );}}Button onClick{() alert(正在播放)}播放电影/ButtonButton onClick{() alert(正在上传)}上传图片/Button/div);
} 阻止默认行为
某些浏览器事件具有与事件相关联的默认行为。例如点击 表单内部的按钮会触发表单提交事件默认情况下将重新加载整个页面
export default function Signup() {return (form onSubmit{e {e.preventDefault();alert(提交表单);}}input /button发送/button/form);
}练习2
export default function ColorSwitch({onChangeColor
}) {return (button onClick{e {e.stopPropagation();onChangeColor();}}改变颜色/button);
}组件的记忆 state 当普通的变量无法满足时
import { sculptureList } from ./data.js;export default function Gallery() {let index 0;function handleClick() {index index 1;}let sculpture sculptureList[index];return (button onClick{handleClick}Next/buttonh2i{sculpture.name} /i by {sculpture.artist}/h2h3 ({index 1} of {sculptureList.length})/h3img src{sculpture.url} alt{sculpture.alt}/p{sculpture.description}/p/);
}handleClick() 事件处理函数正在更新局部变量 index。但存在两个原因使得变化不可见
局部变量无法在多次渲染中持久保存。 当 React 再次渲染这个组件时它会从头开始渲染——不会考虑之前对局部变量的任何更改。更改局部变量不会触发渲染。 React 没有意识到它需要使用新数据再次渲染组件。
要使用新数据更新组件需要做两件事
保留 渲染之间的数据。触发 React 使用新数据渲染组件重新渲染。
useState Hook 提供了这两个功能
State 变量 用于保存渲染间的数据。State setter 函数 更新变量并触发 React 再次渲染组件
添加一个 state 变量
要添加 state 变量先从文件顶部的 React 中导入 useState
import { useState } from react;然后替换这一行
let index 0;将其修改为
const [index, setIndex] useState(0);遇见你的第一个 Hook
在 React 中useState 以及任何其他以“use”开头的函数都被称为 Hook。Hook 是特殊的函数只在 React 渲染时有效我们将在下一节详细介绍。它们能让你 “hook” 到不同的 React 特性中去Hooks ——以 use 开头的函数——只能在组件或自定义 Hook** 的最顶层调用。** 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use” React 特性类似于在文件顶部“导入”模块
剖析 useState
useState 的唯一参数是 state 变量的初始值。在这个例子中index 的初始值被useState(0)设置为 0。每次你的组件渲染时useState 都会给你一个包含两个值的数组
state 变量 (index) 会保存上次渲染的值。state setter 函数 (setIndex) 可以更新 state 变量并触发 React 重新渲染组件
以下是实际发生的情况
const [index, setIndex] useState(0);组件进行第一次渲染。 因为你将 0 作为 index 的初始值传递给 useState它将返回 [0, setIndex]。 React 记住 0 是最新的 state 值。你更新了 state。当用户点击按钮时它会调用 setIndex(index 1)。 index 是 0所以它是 setIndex(1)。这告诉 React 现在记住 index 是 1 并触发下一次渲染。组件进行第二次渲染。React 仍然看到 useState(0)但是因为 React 记住 了你将 index 设置为了 1它将返回 [1, setIndex]。以此类推
赋予一个组件多个 state 变量
你可以在一个组件中拥有任意多种类型的 state 变量
State 是隔离且私有的
State 是屏幕上组件实例内部的状态。换句话说如果你渲染同一个组件两次每个副本都会有完全隔离的 state改变其中一个不会影响另一个。练习1
import { useState } from react;
import { sculptureList } from ./data.js;export default function Gallery() {const [index, setIndex] useState(0);const [showMore, setShowMore] useState(false);function handleNextClick() {if(index sculptureList.length-1){setIndex(0)}else{setIndex(index 1);}}function handleMoreClick() {setShowMore(!showMore);}function handlePreviousClick(){if(index0){setIndex(sculptureList.length-1)}else{setIndex(index-1)}}let sculpture sculptureList[index];return (button onClick{handlePreviousClick}Previous/buttonbutton onClick{handleNextClick}Next/buttonh2i{sculpture.name} /i by {sculpture.artist}/h2h3 ({index 1} of {sculptureList.length})/h3button onClick{handleMoreClick}{showMore ? Hide : Show} details/button{showMore p{sculpture.description}/p}img src{sculpture.url} alt{sculpture.alt}//);
}
练习2
import {useState} from react
export default function Form() {//let firstName ;//let lastName ;const [firstName,setFirstName] useState()const [lastName,setLastName] useState()function handleFirstNameChange(e) {setFirstName(e.target.value);}function handleLastNameChange(e) {setLastName(e.target.value) ;}function handleReset() {setFirstName();setLastName() ;}return (form onSubmit{e e.preventDefault()}inputplaceholderFirst namevalue{firstName}onChange{handleFirstNameChange}/inputplaceholderLast namevalue{lastName}onChange{handleLastNameChange}/h1Hi, {firstName} {lastName}/h1button onClick{handleReset}Reset/button/form);
}
练习3
import { useState } from react;export default function FeedbackForm() {const [isSent, setIsSent] useState(false);const [message, setMessage] useState();if (isSent) {return h1Thank you!/h1;} else {// eslint-disable-next-linereturn (form onSubmit{e {e.preventDefault();alert(Sending: ${message});setIsSent(true);}}textareaplaceholderMessagevalue{message}onChange{e setMessage(e.target.value)}/br /button typesubmitSend/button/form);}
} 渲染和提交 步骤 1: 触发一次渲染
有两种原因会导致组件的渲染:
组件的 初次渲染。组件或者其祖先之一的 状态发生了改变。
状态更新时重新渲染
一旦组件被初次渲染你就可以通过使用 set 函数 更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列。你可以把这种情况想象成餐厅客人在第一次下单之后又点了茶、点心和各种东西具体取决于他们的胃口。
步骤 2: React 渲染你的组件
在你触发渲染后React 会调用你的组件来确定要在屏幕上显示的内容。“渲染中” 即 React 在调用你的组件。
在进行初次渲染时, React 会调用根组件。对于后续的渲染, React 会调用内部状态更新触发了渲染的函数组件
import Gallery from ./Gallery.js;
import { createRoot } from react-dom/client;const root createRoot(document.getElementById(root))
root.render(Gallery /);export default function Gallery() {return (sectionh1鼓舞人心的雕塑/h1Image /Image /Image //section);
}function Image() {return (imgsrchttps://i.imgur.com/ZF6s192.jpgaltFloralis Genérica by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals/);
}
渲染必须始终是一次 纯计算:
输入相同输出相同。 给定相同的输入组件应始终返回相同的 JSX。当有人点了西红柿沙拉时他们不应该收到洋葱沙拉只做它自己的事情。 它不应更改任何存在于渲染之前的对象或变量。一个订单不应更改其他任何人的订单。
步骤 3: React 把更改提交到 DOM 上
在渲染调用你的组件之后React 将会修改 DOM。
尾声浏览器绘制
在渲染完成并且 React 更新 DOM 之后浏览器就会重新绘制屏幕。尽管这个过程被称为“浏览器渲染”“browser rendering”但我们还是将它称为“绘制”“painting”以避免在这些文档的其余部分中出现混淆 state 如同一张快照
也许 state 变量看起来和一般的可读写的 JavaScript 变量类似。但 state 在其表现出的特性上更像是一张快照。设置它不会更改你已有的 state 变量但会触发重新渲染
设置 state 会触发渲染
在这个例子中当你按下 “send” 时setIsSent(true) 会通知 React 重新渲染 UI
import { useState } from react;export default function Form() {const [isSent, setIsSent] useState(false);const [message, setMessage] useState(Hi!);if (isSent) {return h1Your message is on its way!/h1}return (form onSubmit{(e) {e.preventDefault();setIsSent(true);sendMessage(message);}}textareaplaceholderMessagevalue{message}onChange{e setMessage(e.target.value)}/button typesubmitSend/button/form);
}function sendMessage(message) {// ...
}
当你单击按钮时会发生以下情况
执行 onSubmit 事件处理函数。setIsSent(true) 将 isSent 设置为 true 并排列一个新的渲染。React 根据新的 isSent 值重新渲染组件
当 React 重新渲染一个组件时
React 会再次调用你的函数函数会返回新的 JSX 快照React 会更新界面以匹配返回的快照
点击3按钮只会1
import { useState } from react;export default function Counter() {const [number, setNumber] useState(0);return (h1{number}/h1button onClick{() {setNumber(number 1);setNumber(number 1);setNumber(number 1);}}3/button/)
}
以下是这个按钮的点击事件处理函数通知 React 要做的事情
setNumber(number 1)number 是 0 所以 setNumber(0 1)。 React 准备在下一次渲染时将 number 更改为 1。 setNumber(number 1)number 是0 所以 setNumber(0 1)。 React 准备在下一次渲染时将 number 更改为 1。 setNumber(number 1)number 是0 所以 setNumber(0 1)。 React 准备在下一次渲染时将 number 更改为 1。
尽管你调用了三次 setNumber(number 1)但在 这次渲染的 事件处理函数中 number 会一直是 0所以你会三次将 state 设置成 1。这就是为什么在你的事件处理函数执行完以后React 重新渲染的组件中的 number 等于 1 而不是 3每一次渲染的 state 值都是固定的理解 : 快照
把一系列 state 更新加入队列 React 会对 state 更新进行批处理
button onClick{() {setNumber(number 1);setNumber(number 1);setNumber(number 1);}}3/button这可能会让你想起餐厅里帮你点菜的服务员。服务员不会在你说第一道菜的时候就跑到厨房相反他们会让你把菜点完让你修改菜品甚至会帮桌上的其他人点菜
在下次渲染前多次更新同一个 state
你可以像 setNumber(n n 1) 这样传入一个根据队列中的前一个 state 计算下一个 state 的 函数
button onClick{() {setNumber(n n 1);setNumber(n n 1);setNumber(n n 1);}}3/button当你在下次渲染期间调用 useState 时React 会遍历队列。之前的 number state 的值是 0所以这就是 React 作为参数 n 传递给第一个更新函数的值事件处理函数执行完成后React 将触发重新渲染。在重新渲染期间React 将处理队列。更新函数会在渲染期间执行因此 **更新函数必须是 **纯函数 并且只 返回 结果。不要尝试从它们内部设置 state 或者执行其他副作用。在严格模式下React 会执行每个更新函数两次但是丢弃第二个结果以便帮助你发现错误练习1
import { useState } from react;export default function RequestTracker() {const [pending, setPending] useState(0);const [completed, setCompleted] useState(0);async function handleClick() {setPending(pendingpending 1);await delay(3000);setPending(pendingpending - 1);setCompleted(completedcompleted 1);}return (h3等待{pending}/h3h3完成{completed}/h3button onClick{handleClick}购买/button/);
}function delay(ms) {return new Promise(resolve {setTimeout(resolve, ms);});
}
练习2
export function getFinalState(baseState, queue) {let finalState baseState;queue.forEach((e,index){if (typeof e number){finalState e;}else{finalState e(finalState)}})return finalState;
} 更新 state 中的对象
state 中可以保存任意类型的 JavaScript 值包括对象。但是你不应该直接修改存放在 React state 中的对象。相反当你想要更新一个对象时你需要创建一个新的对象或者将其拷贝一份然后将 state 更新为此对象state x 从 0 变为 5但是数字 0 本身并没有发生改变。在 JavaScript 中无法对内置的原始值如数字、字符串和布尔值进行任何更改。现在考虑 state 中存放对象的情况
const [position, setPosition] useState({ x: 0, y: 0 });从技术上来讲可以改变对象自身的内容。当你这样做时就制造了一个 mutation
position.x 5;然而虽然严格来说 React state 中存放的对象是可变的但你应该像处理数字、布尔值、字符串一样将它们视为不可变的。因此你应该替换它们的值而不是对它们进行修改
将 state 视为只读的
换句话说你应该 把所有存放在 state 中的 JavaScript 对象都视为只读的。
onPointerMove{e {position.x e.clientX;position.y e.clientY;
}}onPointerMove{e {setPosition({x: e.clientX,y: e.clientY});
}}局部 mutation 是可以接受的 像这样的代码是有问题的因为它改变了 state 中现有的对象
position.x e.clientX;position.y e.clientY;但是像这样的代码就 没有任何问题因为你改变的是你刚刚创建的一个新的对象
const nextPosition {};nextPosition.x e.clientX;nextPosition.y e.clientY;setPosition(nextPosition);事实上它完全等同于下面这种写法
setPosition({x: e.clientX,y: e.clientY});使用展开语法复制对象
import { useState } from react;export default function Form() {const [person, setPerson] useState({firstName: Barbara,lastName: Hepworth,email: bhepworthsculpture.com});function handleFirstNameChange(e) {setPerson({...person,firstName: e.target.value});}function handleLastNameChange(e) {setPerson({...person,lastName: e.target.value});}function handleEmailChange(e) {setPerson({...person,email: e.target.value});}return (labelFirst name:inputvalue{person.firstName}onChange{handleFirstNameChange}//labellabelLast name:inputvalue{person.lastName}onChange{handleLastNameChange}//labellabelEmail:inputvalue{person.email}onChange{handleEmailChange}//labelp{person.firstName}{ }{person.lastName}{ }({person.email})/p/);
}
把当前的数据复制到新对象中因为你只改变了其中一个字段
setPerson({firstName: e.target.value, // 从 input 中获取新的 first namelastName: person.lastName,email: person.email});你可以使用 …对象展开 语法这样你就不需要单独复制每个属性。
setPerson({...person, // 复制上一个 person 中的所有字段firstName: e.target.value // 但是覆盖 firstName 字段 });使用一个事件处理函数来更新多个字段
你也可以在对象的定义中使用 [ 和 ] 括号来实现属性的动态命名。下面是同一个例子但它使用了一个事件处理函数而不是三个
import { useState } from react;export default function Form() {const [person, setPerson] useState({firstName: Barbara,lastName: Hepworth,email: bhepworthsculpture.com});function handleChange(e) {setPerson({...person,[e.target.name]: e.target.value});}return (labelFirst name:inputnamefirstNamevalue{person.firstName}onChange{handleChange}//labellabelLast name:inputnamelastNamevalue{person.lastName}onChange{handleChange}//labellabelEmail:inputnameemailvalue{person.email}onChange{handleChange}//labelp{person.firstName}{ }{person.lastName}{ }({person.email})/p/);
} 更新一个嵌套对象
考虑下面这种结构的嵌套对象
const [person, setPerson] useState({name: Niki de Saint Phalle,artwork: {title: Blue Nana,city: Hamburg,image: https://i.imgur.com/Sd1AgUOm.jpg,}});为了修改 city 的值你首先需要创建一个新的 artwork 对象其中预先填充了上一个 artwork 对象中的数据然后创建一个新的 person 对象并使得其中的 artwork 属性指向新创建的 artwork 对象
const nextArtwork { ...person.artwork, city: New Delhi };const nextPerson { ...person, artwork: nextArtwork };setPerson(nextPerson);或者写成一个函数调用
setPerson({...person, // 复制其它字段的数据 artwork: { // 替换 artwork 字段 ...person.artwork, // 复制之前 person.artwork 中的数据city: New Delhi // 但是将 city 的值替换为 New Delhi}});对象并非是真正嵌套的
下面这个对象从代码上来看是“嵌套”的
let obj {name: Niki de Saint Phalle,artwork: {title: Blue Nana,city: Hamburg,image: https://i.imgur.com/Sd1AgUOm.jpg,}};然而当我们思考对象的特性时“嵌套”并不是一个非常准确的方式。当这段代码运行的时候不存在“嵌套”的对象。你实际上看到的是两个不同的对象
let obj1 {title: Blue Nana,city: Hamburg,image: https://i.imgur.com/Sd1AgUOm.jpg,};let obj2 {name: Niki de Saint Phalle,artwork: obj1};对象 obj1 并不处于 obj2 的“内部”。例如下面的代码中obj3 中的属性也可以指向 obj1
let obj1 {title: Blue Nana,city: Hamburg,image: https://i.imgur.com/Sd1AgUOm.jpg,};let obj2 {name: Niki de Saint Phalle,artwork: obj1};let obj3 {name: Copycat,artwork: obj1};如果你直接修改 obj3.artwork.city就会同时影响 obj2.artwork.city 和 obj1.city。这是因为 obj3.artwork、obj2.artwork 和 obj1 都指向同一个对象。当你用“嵌套”的方式看待对象时很难看出这一点。相反它们是相互独立的对象只不过是用属性“指向”彼此而已
使用 Immer 编写简洁的更新逻辑通过使用 Immer你写出的代码看起来就像是你“打破了规则”而直接修改了对象
updatePerson(draft {draft.artwork.city Lagos;});尝试使用 Immer:
运行 npm install use-immer 添加 Immer 依赖用 import { useImmer } from ‘use-immer’ 替换掉 import { useState } from ‘react’
下面我们把上面的例子用 Immer 实现一下
import { useImmer } from use-immer;export default function Form() {const [person, updatePerson] useImmer({name: Niki de Saint Phalle,artwork: {title: Blue Nana,city: Hamburg,image: https://i.imgur.com/Sd1AgUOm.jpg,}});function handleNameChange(e) {updatePerson(draft {draft.name e.target.value;});}function handleTitleChange(e) {updatePerson(draft {draft.artwork.title e.target.value;});}function handleCityChange(e) {updatePerson(draft {draft.artwork.city e.target.value;});}function handleImageChange(e) {updatePerson(draft {draft.artwork.image e.target.value;});}return (labelName:inputvalue{person.name}onChange{handleNameChange}//labellabelTitle:inputvalue{person.artwork.title}onChange{handleTitleChange}//labellabelCity:inputvalue{person.artwork.city}onChange{handleCityChange}//labellabelImage:inputvalue{person.artwork.image}onChange{handleImageChange}//labelpi{person.artwork.title}/i{ by }{person.name}br /(located in {person.artwork.city})/pimg src{person.artwork.image} alt{person.artwork.title}//);
}
练习1
import { useState } from react;export default function Scoreboard() {const [player, setPlayer] useState({firstName: Ranjani,lastName: Shettar,score: 10,});function handlePlusClick() {//player.score;setPlayer({...player,score: player.score1})}function handleFirstNameChange(e) {setPlayer({...player,firstName: e.target.value,});}function handleLastNameChange(e) {setPlayer({...player,lastName: e.target.value});}return (labelScore: b{player.score}/b{ }button onClick{handlePlusClick}1/button/labellabelFirst name:inputvalue{player.firstName}onChange{handleFirstNameChange}//labellabelLast name:inputvalue{player.lastName}onChange{handleLastNameChange}//label/);
}
练习2
import { useState } from react;
import Background from ./Background.js;
import Box from ./Box.js;const initialPosition {x: 0,y: 0
};export default function Canvas() {const [shape, setShape] useState({color: orange,position: initialPosition});function handleMove(dx, dy) {//shape.position.x dx;//shape.position.y dy;let newPos {x: shape.position.xdx,y: shape.position.ydy}setShape({...shape,position: newPos})}function handleColorChange(e) {setShape({...shape,color: e.target.value});}return (selectvalue{shape.color}onChange{handleColorChange}option valueorangeorange/optionoption valuelightpinklightpink/optionoption valuealicebluealiceblue/option/selectBackgroundposition{initialPosition}/Boxcolor{shape.color}position{shape.position}onMove{handleMove}Drag me!/Box/);
}
练习3
import { useState } from react;
import { useImmer } from use-immer;
import Background from ./Background.js;
import Box from ./Box.js;const initialPosition {x: 0,y: 0
};export default function Canvas() {const [shape, updateShape] useImmer({color: orange,position: initialPosition});function handleMove(dx, dy) {//shape.position.x dx;//shape.position.y dy;updateShape(draft{draft.position {x: shape.position.x dx,y: shape.position.y dy}})}function handleColorChange(e) {/*setShape({...shape,color: e.target.value});*/updateShape(draft{draft.color e.target.value})}return (selectvalue{shape.color}onChange{handleColorChange}option valueorangeorange/optionoption valuelightpinklightpink/optionoption valuealicebluealiceblue/option/selectBackgroundposition{initialPosition}/Boxcolor{shape.color}position{shape.position}onMove{handleMove}Drag me!/Box/);
} 更新 state 中的数组
数组是另外一种可以存储在 state 中的 JavaScript 对象它虽然是可变的但是却应该被视为不可变。同对象一样当你想要更新存储于 state 中的数组时你需要创建一个新的数组或者创建一份已有数组的拷贝值并使用新数组设置 state
在没有 mutation 的前提下更新数组
在 JavaScript 中数组只是另一种对象。同对象一样你需要将 React state 中的数组视为只读的。这意味着你不应该使用类似于 arr[0] ‘bird’ 这样的方式来重新分配数组中的元素也不应该使用会直接修改原始数组的方法例如 push() 和 pop()。相反每次要更新一个数组时你需要把一个新的数组传入 state 的 setting 方法中。为此你可以通过使用像 filter() 和 map() 这样不会直接修改原始值的方法从原始数组生成一个新的数组你需要避免使用左列的方法而首选右列的方法
向数组中添加元素 头部和尾部
push() 会直接修改原始数组而你不希望这样
import { useState } from react;let nextId 0;export default function List() {const [name, setName] useState();const [artists, setArtists] useState([]);return (h1振奋人心的雕塑家们/h1inputvalue{name}onChange{e setName(e.target.value)}/button onClick{() {artists.push({id: nextId,name: name,});}}添加/buttonul{artists.map(artist (li key{artist.id}{artist.name}/li))}/ul/);
}
你应该创建一个 新 数组其包含了原始数组的所有元素 以及 一个在末尾的新元素。这可以通过很多种方法实现最简单的一种就是使用 …数组展开 语法
setArtists( // 替换 state[ // 是通过传入一个新数组实现的...artists, // 新数组包含原数组的所有元素{ id: nextId, name: name } // 并在末尾添加了一个新的元素]
);数组展开运算符还允许你把新添加的元素放在原始的 …artists 之前
setArtists([{ id: nextId, name: name },...artists // 将原数组中的元素放在末尾]);从数组中删除元素
从数组中删除一个元素最简单的方法就是将它过滤出去。换句话说你需要生成一个不包含该元素的新数组。这可以通过 filter 方法实现
setArtists(artists.filter(a a.id ! artist.id)
);这里artists.filter(s s.id ! artist.id) 表示“创建一个新的数组该数组由那些 ID 与 artists.id 不同的 artists 组成”。换句话说每个 artist 的“删除”按钮会把 那一个 artist 从原始数组中过滤掉并使用过滤后的数组再次进行渲染。注意filter 并不会改变原始数组
转换数组
如果你想改变数组中的某些或全部元素你可以用 map() 创建一个新数组
function handleClick() {const nextShapes shapes.map(shape {if (shape.type square) {// 不作改变return shape;} else {// 返回一个新的圆形位置在下方 50px 处return {...shape,y: shape.y 50,};}});// 使用新的数组进行重渲染setShapes(nextShapes);}替换数组中的元素
想要替换数组中一个或多个元素是非常常见的。类似 arr[0] ‘bird’ 这样的赋值语句会直接修改原始数组所以在这种情况下你也应该使用 map
function handleIncrementClick(index) {const nextCounters counters.map((c, i) {if (i index) {// 递增被点击的计数器数值return c 1;} else {// 其余部分不发生变化return c;}});setCounters(nextCounters);}向数组中插入元素
有时你也许想向数组特定位置插入一个元素这个位置既不在数组开头也不在末尾。为此你可以将数组展开运算符 … 和 slice() 方法一起使用
Slice()方法详解
slice() 方法是 JavaScript 数组的一个方法它用于从数组中提取出一部分元素生成一个新的数组而不修改原始数组。slice() 方法接收两个参数表示截取的开始和结束位置。截取范围是左闭右开区间即包括开始位置对应的元素但不包括结束位置对应的元素。
const fruits [apple, banana, orange, grape, kiwi];// 从索引 1包括开始截取到索引 3不包括的元素
const slicedFruits fruits.slice(1, 3);console.log(slicedFruits); // 输出: [banana, orange]
console.log(fruits); // 输出: [apple, banana, orange, grape, kiwi]在上述例子中slice(1, 3) 截取了索引 1包括到索引 3不包括之间的元素生成了一个新的数组 [banana, orange]。原始数组 fruits 并没有被修改。
slice() 方法的常见用法和注意事项
截取整个数组 如果没有传递任何参数给 slice()它将返回数组的浅拷贝相当于复制整个数组。
const copyOfFruits fruits.slice();负数索引 slice() 方法支持负数索引表示从数组末尾开始计算的位置。
const lastTwoFruits fruits.slice(-2);不传结束位置 如果只传递一个参数表示从这个位置开始一直截取到数组末尾。
const afterIndex2 fruits.slice(2);不修改原数组 slice() 方法不会修改原始数组而是返回一个新的数组。适用于类数组对象 slice() 方法也可以用于类数组对象比如 DOM 元素集合。
const elements document.getElementsByTagName(div);
const divArray Array.prototype.slice.call(elements);总体而言slice() 是一个非常方便的数组方法可用于创建数组的副本或截取数组的特定部分而不影响原始数组。
function handleClick() {const insertAt 1; // 可能是任何索引const nextArtists [// 插入点之前的元素...artists.slice(0, insertAt),// 新的元素{ id: nextId, name: name },// 插入点之后的元素...artists.slice(insertAt)];setArtists(nextArtists);setName();} 其他改变数组的情况
排序 或者 逆转: 先拷贝一个新的,再操作
function handleClick() {const nextList [...list];nextList.reverse();setList(nextList);}然而即使你拷贝了数组你还是不能直接修改其内部的元素。这是因为数组的拷贝是浅拷贝——新的数组中依然保留了与原始数组相同的元素
更新数组内部的对象
当你更新一个嵌套的 state 时你需要从想要更新的地方创建拷贝值一直这样直到顶层
使用 Immer 编写简洁的更新逻辑
import { useState } from react;
import { useImmer } from use-immer;let nextId 3;
const initialList [{ id: 0, title: Big Bellies, seen: false },{ id: 1, title: Lunar Landscape, seen: false },{ id: 2, title: Terracotta Army, seen: true },
];export default function BucketList() {const [myList, updateMyList] useImmer(initialList);const [yourList, updateYourList] useImmer(initialList);function handleToggleMyList(id, nextSeen) {updateMyList(draft {const artwork draft.find(a a.id id);artwork.seen nextSeen;});}function handleToggleYourList(artworkId, nextSeen) {updateYourList(draft {const artwork draft.find(a a.id artworkId);artwork.seen nextSeen;});}return (h1艺术愿望清单/h1h2我想看的艺术清单/h2ItemListartworks{myList}onToggle{handleToggleMyList} /h2你想看的艺术清单/h2ItemListartworks{yourList}onToggle{handleToggleYourList} //);
}function ItemList({ artworks, onToggle }) {return (ul{artworks.map(artwork (li key{artwork.id}labelinputtypecheckboxchecked{artwork.seen}onChange{e {onToggle(artwork.id,e.target.checked);}}/{artwork.title}/label/li))}/ul);
}
这是因为你并不是在直接修改原始的 state而是在修改 Immer 提供的一个特殊的 draft 对象。同理你也可以为 draft 的内容使用 push() 和 pop() 这些会直接修改原值的方法。在幕后Immer 总是会根据你对 draft 的修改来从头开始构建下一个 state。这使得你的事件处理程序非常的简洁同时也不会直接修改 state练习1
import { useState } from react;const initialProducts [{id: 0,name: Baklava,count: 1,
}, {id: 1,name: Cheese,count: 5,
}, {id: 2,name: Spaghetti,count: 2,
}];export default function ShoppingCart() {const [products,setProducts] useState(initialProducts)function handleIncreaseClick(productId) {const newPro products.map((c,i){if(i productId){c.count}return c})setProducts(newPro)}return (ul{products.map(product (li key{product.id}{product.name}{ }(b{product.count}/b)button onClick{() {handleIncreaseClick(product.id);}}/button/li))}/ul);
}
练习2
import { useState } from react;const initialProducts [{id: 0,name: Baklava,count: 1,
}, {id: 1,name: Cheese,count: 5,
}, {id: 2,name: Spaghetti,count: 2,
}];export default function ShoppingCart() {const [products,setProducts] useState(initialProducts)function handleIncreaseClick(productId) {setProducts(products.map(product {if (product.id productId) {return {...product,count: product.count 1};} else {return product;}}))}function handleDecreaseClick(productId){let newPro products.map((e,i){if(e.id productId){e.count--}return e}).filter(e{if(e.count!0){return e}})setProducts(newPro)}return (ul{products.map(product (li key{product.id}{product.name}{ }(b{product.count}/b)button onClick{() {handleIncreaseClick(product.id);}}/buttonbutton onClick{(){handleDecreaseClick(product.id)}}–/button/li))}/ul);
}
map和filter使用在 JavaScript 中map() 函数的回调函数中的 return 语句通常用于创建一个新的数组而且 map() 期望每个回调都返回一个值。如果没有显式使用 return则该回调默认返回 undefined。
在你的例子中使用 map() 函数更新数组的元素时确保在回调函数中使用 return 语句将修改后的元素返回。这是因为 map() 会创建一个新数组该数组的每个元素都是通过回调函数处理原始数组的对应元素得到的。
function handleIncreaseClick(productId) {const newPro products.map((product, index) {if (index productId) {product.count;}return product; // 确保使用 return 返回修改后的元素});setProducts(newPro);
}在这个例子中通过 map() 创建了一个新的数组 newPro其中被点击的产品的 count 属性被增加。return product; 确保了每个元素都被正确返回从而构成了新的数组。
filter 函数的回调函数通常期望返回一个布尔值用于确定数组中的元素是否应该被保留为 true还是被过滤掉为 false。因此通常来说在 filter 函数中使用 return 语句是很常见的。
const newArray array.filter(item {// 返回 true 表示保留该元素// 返回 false 表示过滤掉该元素return someCondition;
});然而在 JavaScript 中如果回调函数只有一行且是一个表达式你可以省略 return 语句。该表达式的结果将被隐式视为回调函数的返回值。
const newArray array.filter(item someCondition);在这个简短的情况下someCondition 的结果将被视为该元素是否被保留的条件。
总的来说是否使用 return 取决于你的代码风格和个人偏好但在大多数情况下为了代码的清晰和可读性最好在 filter 函数中使用 return 语句。练习3
import { useState } from react;
import AddTodo from ./AddTodo.js;
import TaskList from ./TaskList.js;let nextId 3;
const initialTodos [{ id: 0, title: Buy milk, done: true },{ id: 1, title: Eat tacos, done: false },{ id: 2, title: Brew tea, done: false },
];export default function TaskApp() {const [todos, setTodos] useState(initialTodos);function handleAddTodo(title) {/*todos.push({id: nextId,title: title,done: false});*/if(title ! ){setTodos([...todos,{id: nextId,title: title,done: false}])}else{alert(输入框不能为空)}}function handleChangeTodo(nextTodo) {/*const todo todos.find(t t.id nextTodo.id);todo.title nextTodo.title;todo.done nextTodo.done;*/setTodos(todos.map(e{if(e.id nextTodo.id){e.title nextTodo.title;e.done nextTodo.done;}return e}))}function handleDeleteTodo(todoId) {/*const index todos.findIndex(t t.id todoId);todos.splice(index, 1);*/setTodos(todos.filter(e{return e.id ! todoId}))}return (AddTodoonAddTodo{handleAddTodo}/TaskListtodos{todos}onChangeTodo{handleChangeTodo}onDeleteTodo{handleDeleteTodo}//);
}
练习4
import { useState } from react;
import { useImmer } from use-immer;
import AddTodo from ./AddTodo.js;
import TaskList from ./TaskList.js;let nextId 3;
const initialTodos [{ id: 0, title: Buy milk, done: true },{ id: 1, title: Eat tacos, done: false },{ id: 2, title: Brew tea, done: false },
];export default function TaskApp() {const [todos, updateTodos] useImmer(initialTodos);function handleAddTodo(title) {/*todos.push({id: nextId,title: title,done: false});*/if(title ! ){updateTodos(draft{draft.push({id: nextId,title: title,done: false})})}else{alert(输入不能为空)}}function handleChangeTodo(nextTodo) {/*const todo todos.find(t t.id nextTodo.id);todo.title nextTodo.title;todo.done nextTodo.done;*/updateTodos(draft{const todo draft.find(t t.id nextTodo.id);todo.title nextTodo.title;todo.done nextTodo.done;})}function handleDeleteTodo(todoId) {/*const index todos.findIndex(t t.id todoId);todos.splice(index, 1);*/updateTodos(draft{const index draft.findIndex(t t.id todoId);draft.splice(index, 1);})}return (AddTodoonAddTodo{handleAddTodo}/TaskListtodos{todos}onChangeTodo{handleChangeTodo}onDeleteTodo{handleDeleteTodo}//);
}