背景:React现在已经更新到19了,文档地址也做了全面的更新,上一次系统性的学习还是在16-17的大版本更新。所以,现在就开始重新学习吧~
学习内容:
- React官网教程:https://zh-hans.react.dev/learn/describing-the-ui
- 其他辅助资料(看到再补充)
补充说明:这次学习更多的是以学习笔记的形式记录,看到哪记到哪
基础知识
React 应用是由被称为 组件 的独立 UI 片段构建而成。React 组件本质上是可以任意添加标签的 JavaScript 函数
React 允许你将标签、CSS 和 JavaScript 组合成自定义"组件",即 应用程序中可复用的 UI 元素
React 组件是一段可以使用标签进行扩展 的 JavaScript 函数,组件的名称必须以大写字母开头(React的语法规定,这样它才能分清是React组件还是正常的html标签)
js
// export default 导出声明
// function Profile 定义函数,function名必须首字母大写
export default function Profile() {
// return 如果换行,就必须用()将内容包裹
// 没有括号包裹的话,任何在 return 下一行的代码都将被忽略!
return (
<img
src="https://i.imgur.com/MK3eW3Am.jpg"
alt="Katherine Johnson"
/>
)
// 或者 标签只有一行的时候括号可以省略
return <img src="https://i.imgur.com/MK3eW3Am.jpg" alt="Katherine Johnson" />
)
}
// 你可以只定义组件一次,然后按需多处和多次使用
export default function Gallery() {
return (
<section>
<h1>了不起的科学家</h1>
<Profile />
<Profile />
<Profile />
</section>
);
}
// 组件不建议嵌套组件定义,不然会很慢并且可能会有bug产生
export default function Gallery() {
// 🔴 永远不要在组件中定义组件
function Profile() {
// ...
}
// ...
}
// 使用时可以import './Gallery.js' 或者 './Gallery',在 React 里都能正常使用,只是前者更符合 原生 ES 模块
import Gallery from './Gallery';
import Gallery from './Gallery.js';
导出方式
默认导出 vs 具名导出
一个文件里有且仅有一个 默认 导出,但是可以有任意多个 具名 导出
当使用默认导入时,可以在 import 语句后面进行任意命名。比如 import Banana from './Button.js'。相反,对于具名导入,导入和导出的名字必须一致。
同一文件中,有且仅有一个默认导出,但可以有多个具名导出
语法 | 导出语句 | 导入语句 |
---|---|---|
默认 | export default function Button() {} | import Button from './Button.js'; |
具名 | export function Button() {} | import { Button } from './Button.js'; |
JSX
- 只能返回一个根元素
在一个组件中包含多个元素,需要用一个父标签把它们包裹起来,如果不想添加新的dom元素,可以使用<>...</>(Fragment)
,React Fragment 允许将子元素分组,而不会在 HTML 结构中添加额外节点
原因:JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来 - 标签必须闭合
这是强制规定,要么自闭合(<img />
),要么添加闭合标签(<li>...</li>
) - 使用驼峰式命名法给大部分属性命名
JSX 最终会被转化为 JavaScript,而 JSX 中的属性也会变成 JavaScript 对象中的键值对。组件经常会遇到需要用变量的方式读取这些属性的时候。但 JavaScript 对变量的命名有限制,所以需要避开这些限制- 变量名称不能包含 - 符号,所以属性大部分用驼峰
- 变量不能用保留字如class,所以在jsx中用className代替
- 由于历史原因,aria-* 和 data-* 属性是以带 - 符号的 HTML 格式书写的
需要将一个字符串属性传递给 JSX 时,把它放到单引号或双引号
js
// "" 引号中的内容按照字符串的形式处理,单引号双引号都可以,但使用双引号会多点
// {} 大括号中的内容会被动态引用,直接在标签中使用 JavaScript,可以在其他地方声明,在使用的时候直接读取js对应的值
// 大括号内的任何 JavaScript 表达式都能正常运行
export default function Avatar() {
const alt = "Gregorio Y. Zara"
//
return (
<img
className="avatar"
src="https://i.imgur.com/7vQD0fPs.jpg"
alt={alt}
/>
);
}
大括号使用场景:
- 用作 JSX 标签内的文本:
<h1>{name}'s To Do List</h1>
是有效的,但是<{tag}>Gregorio Y. Zara's To Do List</{tag}>
无效。 - 用作紧跟在 = 符号后的 属性:
src={avatar}
会读取 avatar 变量,但是src="{avatar}"
只会传一个字符串 {avatar}
在JSX中还可以传递对象,对象也是用大括号表示,所以要引用对象的时候就需要使用两个括号
JSX 是一种模板语言的最小实现,因为它允许你通过 JavaScript 来组织数据和逻辑
Props
React 组件使用 props 来互相通信。每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它,包括对象、数组和函数等
js
import { getImageUrl } from './utils.js';
// props是组件的唯一参数
// function里使用大括号获取props是解构
// 也可以写成这样
// function Avatar(props) {
// const person = props.person
// const size = props.size
// }
// 如果你想在没有指定值的情况下给 prop 一个默认值,可以通过在参数后面写 = 和默认值来进行解构
// 默认值仅在缺少 size prop 或 size={undefined} 时生效,等于null都不行
function Avatar({ person, size=100 }) {
// person 和 size 是可访问的
return (
<img
className="avatar"
src={getImageUrl(person)}
alt={person.name}
width={size}
height={size}
/>
);
}
export default function Profile() {
// 这样使用不同的参数,就能展示出两个类似但是又独立的组件,这就是组件复用一个很重要的意义
return (
<div>
<Avatar
size={100}
person={{
name: 'Katsuko Saruhashi',
imageId: 'YfeOqp2'
}}
/>
<Avatar
size={80}
person={{
name: 'Aklilu Lemma',
imageId: 'OKS67lh'
}}
/>
</div>
);
}
// 还可以使用 JSX 展开语法传递 props
// 像这个场景,props里面所有的内容都是需要传递到Avatar组件中时,就可以直接用展开语法传递
function Profile({ person, size, isSepia, thickBorder }) {
return (
<div className="card">
<Avatar
person={person}
size={size}
isSepia={isSepia}
thickBorder={thickBorder}
/>
</div>
);
}
// 可以写成这样
function Profile(props) {
return (
<div className="card">
<Avatar {...props} />
</div>
);
}
// 假设Profile 中isSepia不需要传递,其他都需要,还可以写成这样
function Profile({isSepia, ...rest}) {
return (
<div className="card">
<Avatar {...rest} />
</div>
);
}
当你将内容嵌套在 JSX 标签中时,父组件将在名为 children 的 prop 中接收到该内容
通俗的来说就是某个组件标签内容,在接收的时候会自动处理成children的props
<Aaa><div>里面是一系列的内容</div> </Aaa>
,在声明Aaa这个组件时,有一个隐藏props,children,表示的就是div及其包裹的内容
可以将带有 children prop 的组件看作有一个"洞",可以由其父组件使用任意 JSX 来"填充"
js
import Avatar from './Avatar.js';
function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}
export default function Profile() {
return (
<Card>
<Avatar
size={100}
person={{
name: 'Katsuko Saruhashi',
imageId: 'YfeOqp2'
}}
/>
</Card>
);
}
props 是 不可变的。当一个组件需要改变它的 props(例如,响应用户交互或新数据)时,它不得不"请求"它的父组件传递 不同的 props ------ 一个新对象!它的旧 props 将被丢弃,最终 JavaScript 引擎将回收它们占用的内存
Props 是只读的时间快照:每次渲染都会收到新版本的 props
你不能改变 props。当你需要交互性时,你可以设置 state。
渲染
在 React 中,可以通过使用 JavaScript 的 if 语句、&& 和 ? : 运算符来选择性地渲染 JSX
- if /else
js
// 这段代码的意思是如果这个组件引用的时候返回了isPacked,那展示的时候就带勾,否则就不带
// 这种代码很适合使用在非黑即白的场景下,如果是,就展示A,其他情况都展示B
// 注意两种情况下都需要写return
function Item({ name, isPacked }) {
if (isPacked) {
return <li className="item">{name} ✅</li>;
}
return <li className="item">{name}</li>;
}
// 还可以写成这样
// 这样虽然冗长,但最灵活
function Item({ name, isPacked }) {
let itemContent = name;
if (isPacked) {
itemContent = name + " ✅";
}
return (
<li className="item">
{itemContent}
</li>
);
}
在一些情况下,你不想有任何东西进行渲染。比如,你不想显示已经打包好的物品。但一个组件必须返回一些东西。这种情况下,你可以直接返回 null
一般来说,尽量不要在子组件里返回null,而是由父组件控制是否渲染
- 三目运算符(? :)
有时候可能需要更加紧凑的形式来进行条件渲染,这个时候三目运算符就可以使用了
js
// 这种写法和上面if/else的写法是完全一样的,因为JSX 元素不是"实例",它们没有内部状态也不是真实的 DOM 节点
function Item({ name, isPacked }) {
return (
<li className="item">
{isPacked ? name + ' ✅' : name}
</li>
);
}
- 与运算符(&&)
在 React 组件里,通常用在当条件成立时,想渲染一些 JSX,或者不做任何渲染,这时候使用&&
&&运算符左侧(我们的条件)为 true 时,它则返回其右侧的值(在我们的例子里是勾选符号)。但如果左侧是 false,则整个表达式会变成 false
逻辑与(&&)运算符从左到右对操作数求值,遇到第一个假值操作数时立即返回;如果所有的操作数都是真值,则返回最后一个操作数的值
在 JSX 里,React 会将 false 视为一个"空值",不会被渲染
⚠️⚠️⚠️ 如果左侧是 0,整个表达式将变成左侧的值(0),React 此时则会渲染 0 而不是不进行渲染(这个记得和常规的JS进行区分)
js
// 上面的例子也可以表现成,isPacked,才展示勾,就可以写成这样
return (
<li className="item">
{name} {isPacked && '✅'}
</li>
);
渲染列表
我们也可以像在js那样使用数组的方法在jsx里渲染列表,目前示例用的filter和map
js
const people = [{
id: 0,
name: '凯瑟琳·约翰逊',
profession: '数学家',
}, {
id: 1,
name: '马里奥·莫利纳',
profession: '化学家',
}, {
id: 2,
name: '穆罕默德·阿卜杜勒·萨拉姆',
profession: '物理学家',
}, {
id: 3,
name: '珀西·莱温·朱利亚',
profession: '化学家',
}, {
id: 4,
name: '苏布拉马尼扬·钱德拉塞卡',
profession: '天体物理学家',
}];
export default function List() {
// 这里根据具体的业务需求,用fill,reduce,concat等等都可以,并不局限于filter
const chemists = people.filter(person =>
person.profession === '化学家'
);
// 箭头函数会隐式地返回位于 => 之后的表达式,所以这里可以省略 return 语句
// 但如果箭头函数后面带了大括号,就必须使用return
// const listItems = chemists.map(person => {
// return <li>...</li>;
// });
// 箭头函数 => { 后面的部分被称为 "块函数体",块函数体支持多行代码的写法,但要用 return 语句才能指定返回值。假如忘了写 return,函数什么都不会返回
const listItems = chemists.map(person =>
// 直接放在 map() 方法里的 JSX 元素一般都需要指定 key 值
// key 会告诉 React,每个组件对应着数组里的哪一项,所以 React 可以把它们匹配起来。这在数组项进行移动(例如排序)、插入或删除等操作时非常重要
<li key={person.id}>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
因{person.accomplishment}而闻名世界
</p>
</li>
);
return <ul>{listItems}</ul>;
}
// 如果需要渲染多个元素,可以用div包裹,也可以用Fragment,但是不能用<></>这种简写形式,因为它不支持添加key
// Fragment不会出现在DOM上
const listItems = people.map(person =>
<Fragment key={person.id}>
<h1>{person.name}</h1>
<p>{person.bio}</p>
</Fragment>
);
直接放在 map() 方法里的 JSX 元素一般都需要指定 key 值
key 会告诉 React,每个组件对应着数组里的哪一项,所以 React 可以把它们匹配起来。这在数组项进行移动(例如排序)、插入或删除等操作时非常重要
- key 值在兄弟节点之间必须是唯一的。 不要求全局唯一,在不同的数组中可以使用相同的 key。
- key 值不能改变,否则就失去了使用 key 的意义!⚠️所以千万不要在渲染时动态地生成 key。
为什么React需要Key
- key可以作为数组中的唯一标识,即使元素的位置在渲染的过程中发生了改变,它提供的 key 值也能让 React 在整个生命周期中一直认得它
- 最好不要使用index作为key,数组项在插入、删除或者重新排序等操作中发生改变时,可能会引起一些奇奇怪怪的bug(这个时候index会产生一些变化)
- 不要在运行过程中动态地产生 key,像是 key={Math.random()} 这种方式。这会导致每次重新渲染后的 key 值都不一样,从而使得所有的组件和 DOM 元素每次都要重新创建
- 组件不会把 key 当作 props 的一部分。Key 的存在只对 React 本身起到提示作用