赞
踩
React 组件使用 props 来互相通信。每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它。Props 可能会让你想起 HTML 属性,但你可以通过它们传递任何 JavaScript 值,包括对象、数组和函数。
Props是你传递给JSX标签的信息。例如,className
、src
、alt
、width
和 height
便是一些可以传递给 <img>
的 props:
你可以传递给 <img>
标签的 props 是预定义的(ReactDOM 符合 HTML 标准)。但是你可以将任何 props 传递给 你自己的 组件。
首先,将一些 props 传递给 Avatar
。例如,让我们传递两个 props:person
(一个对象)和 size
(一个数字):
export default function Profile() {
return (
<Avatar
person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
size={100}
/>
);
}
现在,你可以在 Avatar
组件中读取这些 props 了。
你可以通过在 function Avatar
之后直接列出它们的名字 person, size
来读取这些 props。这些 props 在 ({
和 })
之间,并由逗号分隔。这样,你可以在 Avatar
的代码中使用它们,就像使用变量一样。
function Avatar({ person, size }) {
// 在这里 person 和 size 是可访问的
}
向使用 person
和 size
props 渲染的 Avatar
添加一些逻辑,你就完成了。
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
对象,所以可以将它解构为单独的 props。
如果你想在没有指定值的情况下给 prop 一个默认值,你可以通过在参数后面写 =
和默认值来进行解构:
function Avatar({ person, size = 100 }) {
// ...
}
现在, 如果 <Avatar person={...} />
渲染时没有 size
prop, size
将被赋值为 100
。
默认值仅在缺少 size
prop 或 size={undefined}
时生效。 但是如果你传递了 size={null}
或 size={0}
,默认值将 不 被使用。
有时候,传递 props 会变得非常重复:
function Profile({ person, size, isSepia, thickBorder }) { return ( <div className="card"> <Avatar person={person} size={size} isSepia={isSepia} thickBorder={thickBorder} /> </div> ); }
重复代码没有错(它可以更清晰)。但有时你可能会重视简洁。一些组件将它们所有的 props 转发给子组件,正如 Profile
转给 Avatar
那样。因为这些组件不直接使用他们本身的任何 props,所以使用更简洁的“展开”语法是有意义的:
function Profile(props) {
return (
<div className="card">
<Avatar {...props} />
</div>
);
}
这会将 Profile
的所有 props 转发到 Avatar
,而不列出每个名字。
请克制地使用展开语法。 如果你在所有其他组件中都使用它,那就有问题了。 通常,它表示你应该拆分组件,并将子组件作为 JSX 传递。
嵌套浏览器内置标签是很常见的:
<div>
<img />
</div>
有时你会希望以相同的方式嵌套自己的组件:
<Card>
<Avatar />
</Card>
当您将内容嵌套在 JSX 标签中时,父组件将在名为 children
的 prop 中接收到该内容。例如,下面的 Card
组件将接收一个被设为 <Avatar />
的 children
prop 并将其包裹在 div 中渲染:
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> ); }
尝试用一些文本替换 <Card>
中的 <Avatar>
,看看 Card
组件如何包裹任意嵌套内容。它不必“知道”其中渲染的内容。你会在很多地方看到这种灵活的模式。
可以将带有 children
prop 的组件看作有一个“洞”,可以由其父组件使用任意 JSX 来“填充”。你会经常使用 children
prop 来进行视觉包装:面板、网格等等。
一个组件可能会随着时间的推移收到不同的 props。 Props 并不总是静态的!在这里,time
prop 每秒都在变化。当你选择另一种颜色时,color
prop 也改变了。Props 反映了组件在任何时间点的数据,并不仅仅是在开始时。
然而,props 是 不可变的(一个计算机科学术语,意思是“不可改变”)。当一个组件需要改变它的 props(例如,响应用户交互或新数据)时,它不得不“请求”它的父组件传递 不同的 props —— 一个新对象!它的旧 props 将被丢弃,最终 JavaScript 引擎将回收它们占用的内存。
不要尝试“更改 props”。 当你需要响应用户输入(例如更改所选颜色)时,你可以“设置 state”,你可以在 State: 一个组件的内存 中继续了解。
function Avatar({ person, size })
解构语法。size = 100
,用于缺少值或值为 undefined
的 props 。<Avatar {...props} />
JSX 展开语法转发所有 props,但不要过度使用它!<Card><Avatar /></Card>
这样的嵌套 JSX,将被视为 Card
组件的 children
prop。通常你的组件会需要根据不同的情况显示不同的内容。在 React 中,你可以通过使用 JavaScript 的 if
语句、&&
和 ? :
运算符来选择性地渲染 JSX。
null
在一些情况下,你不想有任何东西进行渲染。比如,你不想显示已经打包好的物品。但一个组件必须返回一些东西。这种情况下,你可以直接返回 null
。
if (isPacked) {
return null;
}
return <li className="item">{name}</li>;
//如果组件的 isPacked 属性为 true,那么它将只返回 null。否则,它将返回相应的 JSX 用来渲染。
实际上,在组件里返回 null
并不常见,因为这样会让想使用它的开发者感觉奇怪。通常情况下,你可以在父组件里选择是否要渲染该组件。让我们接着往下看吧!
JavaScript 有一种紧凑型语法来实现条件判断表达式——条件运算符 又称“三目运算符”。
return (
<li className="item">
{isPacked ? name + ' ✔' : name}
</li>
);
现在,假如你想将对应物品的文本放到另一个 HTML 标签里,比如用 <del>
来显示删除线。你可以添加更多的换行和括号,以便在各种情况下更好地去嵌套 JSX:
function Item({ name, isPacked }) { return ( <li className="item"> {isPacked ? ( <del> {name + ' ✔'} </del> ) : ( name )} </li> ); } export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name="宇航服" /> <Item isPacked={true} name="带金箔的头盔" /> <Item isPacked={false} name="Tam 的照片" /> </ul> </section> ); }
对于简单的条件判断,这样的风格可以很好地实现,但需要适量使用。如果你的组件里有很多的嵌套式条件表达式,则需要考虑通过提取为子组件来简化这些嵌套表达式。在 React 里,标签也是你代码中的一部分,所以你可以使用变量和函数来整理一些复杂的表达式。
你会遇到的另一个常见的快捷表达式是 [JavaScript 逻辑与(&&
)运算符](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Logical_AND#:~:text=The logical AND ( %26%26 ) operator,it returns a Boolean value.)。在 React 组件里,通常用在当条件成立时,你想渲染一些 JSX,或者不做任何渲染。使用 &&
,你也可以实现仅当 isPacked
为 true
时,渲染勾选符号。
return (
<li className="item">
{name} {isPacked && '✔'}
</li>
);
你可以认为,“当 isPacked
为真值时,则(&&
)渲染勾选符号,否则,不渲染。”
下面为具体的例子:
function Item({ name, isPacked }) { return ( <li className="item"> {name} {isPacked && '✔'} </li> ); } export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name="宇航服" /> <Item isPacked={true} name="带金箔的头盔" /> <Item isPacked={false} name="Tam 的照片" /> </ul> </section> ); }
JavaScript && 表达式 的左侧(我们的条件)为 true
时,它则返回其右侧的值(在我们的例子里是勾选符号)。但条件的结果是 false
,则整个表达式会变成 false
。在 JSX 里,React 会将 false
视为一个“空值”,就像 null
或者 undefined
,这样 React 就不会在这里进行任何渲染。
切勿将数字放在 &&
左侧.
JavaScript 会自动将左侧的值转换成布尔类型以判断条件成立与否。然而,如果左侧是 0
,整个表达式将变成左侧的值(0
),React 此时则会渲染 0
而不是不进行渲染。
例如,一个常见的错误是 messageCount && <p>New messages</p>
。其原本是想当 messageCount
为 0 的时候不进行渲染,但实际上却渲染了 0
。
为了更正,可以将左侧的值改成布尔类型:messageCount > 0 && <p>New messages</p>
。
当这些快捷方式妨碍写普通代码时,可以考虑使用 if
语句和变量。因为你可以使用 let
进行重复赋值,所以一开始你可以将你想展示的(这里指的是物品的名字)作为默认值赋予给该变量。
let itemContent = name;
结合 if
语句,当 isPacked
为 true
时,将 JSX 表达式的值重新赋值给 itemContent
:
if (isPacked) {
itemContent = name + " ✔";
}
在 JSX 中通过大括号使用 JavaScript。将变量用大括号嵌入在返回的 JSX 树中,来嵌套计算好的表达式与 JSX:
<li className="item">
{itemContent}
</li>
这种方式是最冗长的,但也是最灵活的。下面是相关的例子:
function Item({ name, isPacked }) { let itemContent = name; if (isPacked) { itemContent = name + " ✔"; } return ( <li className="item"> {itemContent} </li> ); } export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name="宇航服" /> <Item isPacked={true} name="带金箔的头盔" /> <Item isPacked={false} name="Tam 的照片" /> </ul> </section> ); }
跟之前的一样,这个方式不仅仅适用于文本,任意的 JSX 均适用:
function Item({ name, isPacked }) { let itemContent = name; if (isPacked) { itemContent = ( <del> {name + " ✔"} </del> ); } return ( <li className="item"> {itemContent} </li> ); } export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name="宇航服" /> <Item isPacked={true} name="带金箔的头盔" /> <Item isPacked={false} name="Tam 的照片" /> </ul> </section> ); }
if
语句来选择性地返回 JSX 表达式。{cond ? <A /> : <B />}
表示 “当 cond
为真值时, 渲染 <A />
,否则 <B />
”。{cond && <A />}
表示 “当 cond
为真值时, 渲染 <A />
,否则不进行渲染”。if
,你也可以不使用它们。你可能经常需要通过 JavaScript 的数组方法 来操作数组中的数据,从而将一个数据集渲染成多个相似的组件。在这篇文章中,你将学会如何在 React 中使用 filter()
筛选需要渲染的组件和使用 map()
把数组转换成组件数组。
这里我们有一个列表。
<ul>
<li>凯瑟琳·约翰逊: 数学家</li>
<li>马里奥·莫利纳: 化学家</li>
<li>穆罕默德·阿卜杜勒·萨拉姆: 物理学家</li>
<li>珀西·莱温·朱利亚: 化学家</li>
<li>苏布拉马尼扬·钱德拉塞卡: 天体物理学家</li>
</ul>
可以看到,这些列表项之间唯一的区别就是其中的内容/数据。未来你可能会碰到很多类似的情况,在那些场景中,你想基于不同的数据渲染出相似的组件,比如评论列表或者个人资料的图库。在这样的场景下,可以把要用到的数据存入 JavaScript 对象或数组,然后用 map()
或 filter()
这样的方法来渲染出一个组件列表。
这里给出一个由数组生成一系列列表项的简单示例:
const people = [
'凯瑟琳·约翰逊: 数学家',
'马里奥·莫利纳: 化学家',
'穆罕默德·阿卜杜勒·萨拉姆: 物理学家',
'珀西·莱温·朱利亚: 化学家',
'苏布拉马尼扬·钱德拉塞卡: 天体物理学家',
];
people
这个数组中的每一项,并获得一个新的 JSX 节点数组 listItems
:const listItems = people.map(person => <li>{person}</li>);
listItems
用 <ul>
包裹起来,然后 返回 它:return <ul>{listItems}</ul>;
让我们把 people
数组变得更加结构化一点。
const people = [{ id: 0, name: 'Creola Katherine Johnson', profession: 'mathematician', }, { id: 1, name: 'Mario José Molina-Pasquel Henríquez', profession: 'chemist', }, { id: 2, name: 'Mohammad Abdus Salam', profession: 'physicist', }, { id: 3, name: 'Percy Lavon Julian', profession: 'chemist', }, { id: 4, name: 'Subrahmanyan Chandrasekhar', profession: 'astrophysicist', }];
现在,假设你只想在屏幕上显示职业是 化学家
的人。那么你可以使用 JavaScript 的 filter()
方法来返回满足条件的项。这个方法会让数组的子项经过 “过滤器”(一个返回值为 true
或 false
的函数)的筛选,最终返回一个只包含满足条件的项的新数组。
既然你只想显示 profession
值是 化学家
的人,那么这里的 “过滤器” 函数应该长这样:(person) => person.profession === '化学家'
。下面我们来看看该怎么把它们组合在一起:
chemists
,这里用到 filter()
方法过滤 people
数组来得到所有的化学家,过滤的条件应该是 person.profession === '化学家'
:const chemists = people.filter(person =>
person.profession === '化学家'
);
chemists
数组:const listItems = chemists.map(person =>
<li>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
因{person.accomplishment}而闻名世界
</p>
</li>
);
listItems
:return <ul>{listItems}</ul>;
因为箭头函数会隐式地返回位于 =>
之后的表达式,所以你可以省略 return
语句。
const listItems = chemists.map(person =>
<li>...</li> // 隐式地返回!
);
不过,如果你的 =>
后面跟了一对花括号 {
,那你必须使用 return
来指定返回值!
const listItems = chemists.map(person => { // 花括号
return <li>...</li>;
});
箭头函数 => {
后面的部分被称为 “块函数体”,块函数体支持多行代码的写法,但要用 return
语句才能指定返回值。假如你忘了写 return
,那这个函数什么都不会返回!
这是因为你必须给数组中的每一项都指定一个 key
——它可以是字符串或数字的形式,只要能唯一标识出各个数组项就行:
<li key={person.id}>...</li>
注意: 直接放在map() 方法里的JSX元素一般都需要指定key值
React 里需要 key 和文件夹里的文件需要有文件名的道理是类似的。它们(key 和文件名)都让我们可以从众多的兄弟元素中唯一标识出某一项(JSX 节点或文件)。而一个精心选择的 key 值所能提供的信息远远不止于这个元素在数组中的位置。即使元素的位置在渲染的过程中发生了改变,它提供的 key
值也能让 React 在整个生命周期中一直认得它。
部分 JavaScript 函数是 纯粹 的,这类函数通常被称为纯函数。纯函数仅执行计算操作,不做其他操作。你可以通过将组件按纯函数严格编写,以避免一些随着代码库的增长而出现的、令人困扰的 bug 以及不可预测的行为。但为了获得这些好处,你需要遵循一些规则。
在计算机科学中(尤其是函数式编程的世界中),纯函数 通常具有如下特征:
举个你非常熟悉的纯函数示例:数学中的公式。
考虑如下数学公式:y = 2x。
若 x = 2 则 y = 4。永远如此。
若 x = 3 则 y = 6。永远如此。
若 x = 3,那么 y 并不会因为时间或股市的影响,而有时等于 9 、 –1 或 2.5。
若 y = 2x 且 x = 3, 那么 y 永远 等于 6.
我们使用 JavaScript 的函数实现,看起来将会是这样:
function double(number) {
return 2 * number;
}
上述例子中,double()
就是一个 纯函数。如果你传入 3
,它将总是返回 6
。
React 便围绕着这个概念进行设计。React 假设你编写的所有组件都是纯函数。也就是说,对于相同的输入,你所编写的 React 组件必须总是返回相同的 JSX。
function Recipe({ drinkers }) { return ( <ol> <li>Boil {drinkers} cups of water.</li> <li>Add {drinkers} spoons of tea and {0.5 * drinkers} spoons of spice.</li> <li>Add {0.5 * drinkers} cups of milk to boil and sugar to taste.</li> </ol> ); } export default function App() { return ( <section> <h1>Spiced Chai Recipe</h1> <h2>For two</h2> <Recipe drinkers={2} /> <h2>For a gathering</h2> <Recipe drinkers={4} /> </section> ); }
当你给函数 Recipe
传入 drinkers={2}
参数时,它将返回包含 2 cups of water
的 JSX。永远如此。
而当你传入 drinkers={4}
时,它将返回包含 4 cups of water
的 JSX。永远如此。
就像数学公式一样。
你可以把你的组件当作食谱:如果你遵循它们,并且在烹饪过程中不引入新食材,你每次都会得到相同的菜肴。那这道 “菜肴” 就是组件用于 React 渲染 的 JSX。
React 的渲染过程必须自始至终是纯粹的。组件应该只 返回 它们的 JSX,而不 改变 在渲染前,就已存在的任何对象或变量 — 这将会使它们变得不纯粹!
以下是违反这一规则的组件示例:
let guest = 0; function Cup() { // Bad:正在更改预先存在的变量! guest = guest + 1; return <h2>Tea cup for guest #{guest}</h2>; } export default function TeaSet() { return ( <> <Cup /> <Cup /> <Cup /> </> ); }
该组件正在读写其外部声明的 guest
变量。这意味着 多次调用这个组件会产生不同的 JSX!并且,如果 其他 组件读取 guest
,它们也会产生不同的 JSX,其结果取决于它们何时被渲染!这是无法预测的。
回到我们的公式 y = 2x ,现在即使 x = 2 ,我们也不能相信 y = 4 。我们的测试可能会失败,我们的用户可能会感到困扰,飞机可能会从天空坠毁——你将看到这会引发多么扑朔迷离的 bugs!
你可以 将 guest
作为 prop 传入 来修复此组件:
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup guest={1} />
<Cup guest={2} />
<Cup guest={3} />
</>
);
}
现在你的组件就是纯粹的,因为它返回的 JSX 只依赖于 guest
prop。
一般来说,你不应该期望你的组件以任何特定的顺序被渲染。调用 y = 5x 和 y = 2x 的先后顺序并不重要:这两个公式相互独立。同样地,每个组件也应该“独立思考”,而不是在渲染过程中试图与其他组件协调,或者依赖于其他组件。渲染过程就像是一场学校考试:每个组件都应该自己计算 JSX!
尽管你可能还没使用过,但在 React 中,你可以在渲染时读取三种输入:props,state 和 context。你应该始终将这些输入视为只读。
当你想根据用户输入 更改 某些内容时,你应该 设置状态,而不是直接写入变量。当你的组件正在渲染时,你永远不应该改变预先存在的变量或对象。
React 提供了 “严格模式”,在严格模式下开发时,它将会调用每个组件函数两次。通过重复调用组件函数,严格模式有助于找到违反这些规则的组件。
我们注意到,原始示例显示的是 “Guest #2”、“Guest #4” 和 “Guest #6”,而不是 “Guest #1”、“Guest #2” 和 “Guest #3”。原来的函数并不纯粹,因此调用它两次就出现了问题。但对于修复后的纯函数版本,即使调用该函数两次也能得到正确结果。纯函数仅仅执行计算,因此调用它们两次不会改变任何东西 — 就像两次调用 double(2)
并不会改变返回值,两次求解 y = 2x 不会改变 y 的值一样。相同的输入,总是返回相同的输出。
严格模式在生产环境下不生效,因此它不会降低应用程序的速度。如需引入严格模式,你可以用 <React.StrictMode>
包裹根组件。一些框架会默认这样做。
上述示例的问题出在渲染过程中,组件改变了 预先存在的 变量的值。为了让它听起来更可怕一点,我们将这种现象称为 突变(mutation) 。纯函数不会改变函数作用域外的变量、或在函数调用前创建的对象——这会使函数变得不纯粹!
但是,你完全可以在渲染时更改你 *刚刚* 创建的变量和对象。在本示例中,你创建一个 []
数组,将其分配给一个 cups
变量,然后 push
一打 cup 进去:
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaGathering() {
let cups = [];
for (let i = 1; i <= 12; i++) {
cups.push(<Cup key={i} guest={i} />);
}
return cups;
}
如果 cups
变量或 []
数组是在 TeaGathering
函数之外创建的,这将是一个很大的问题!因为如果那样的话,当你调用数组的 push 方法时,就会更改 预先存在的 对象。
但是,这里不会有影响,因为每次渲染时,你都是在 TeaGathering
函数内部创建的它们。TeaGathering
之外的代码并不会知道发生了什么。这就被称为 “局部 mutation” — 如同藏在组件里的小秘密。
函数式编程在很大程度上依赖于纯函数,但 某些事物 在特定情况下不得不发生改变。这是编程的要义!这些变动包括更新屏幕、启动动画、更改数据等,它们被称为 副作用。它们是 “额外” 发生的事情,与渲染过程无关。
在 React 中,副作用通常属于 事件处理程序。事件处理程序是 React 在你执行某些操作(如单击按钮)时运行的函数。即使事件处理程序是在你的组件 内部 定义的,它们也不会在渲染期间运行! 因此事件处理程序无需是纯函数。
如果你用尽一切办法,仍无法为副作用找到合适的事件处理程序,你还可以调用组件中的 useEffect
方法将其附加到返回的 JSX 中。这会告诉 React 在渲染结束后执行它。然而,这种方法应该是你最后的手段。
编写纯函数需要遵循一些习惯和规程。但它开启了绝妙的机遇:
树是项目和 UI 之间的关系模型,通常使用树结构来表示 UI。例如,浏览器使用树结构来建模 HTML(DOM)与CSS(CSSOM)。移动平台也使用树来表示其视图层次结构。
与浏览器和移动平台一样,React 还使用树结构来管理和建模 React 应用程序中组件之间的关系。这些树是有用的工具,用于理解数据如何在 React 应用程序中流动以及如何优化呈现和应用程序大小。
组件的一个主要特性是能够由其他组件组合而成。在 嵌套组件 中有父组件和子组件的概念,其中每个父组件本身可能是另一个组件的子组件。
当渲染 React 应用程序时,可以在一个称为渲染树的树中建模这种关系。
在 React 渲染树中,根节点是应用程序的 根组件。在这种情况下,根组件是 App
,它是 React 渲染的第一个组件。树中的每个箭头从父组件指向子组件
也许会注意到在上面的渲染树中,没有提到每个组件渲染的 HTML 标签。这是因为渲染树仅由 React 组件 组成。
React 是跨平台的 UI 框架。react.dev 展示了一些渲染到使用 HTML 标签作为 UI 原语的 web 的示例。但是 React 应用程序同样可以渲染到移动设备或桌面平台,这些平台可能使用不同的 UI 原语,如 UIView 或 FrameworkElement。
这些平台 UI 原语不是 React 的一部分。无论应用程序渲染到哪个平台,React 渲染树都可以为 React 应用程序提供见解。
在 React 应用程序中,可以使用树来建模的另一个关系是应用程序的模块依赖关系。当 拆分组件 和逻辑到不同的文件中时,就创建了 JavaScript 模块,在这些模块中可以导出组件、函数或常量。
模块依赖树中的每个节点都是一个模块,每个分支代表该模块中的 import
语句。
树的根节点是根模块,也称为入口文件。它通常包含根组件的模块。
与同一应用程序的渲染树相比,存在相似的结构,但也有一些显著的差异:
inspirations.js
,在这个树中也有所体现。渲染树仅封装组件。Copyright.js
出现在 App.js
下,但在渲染树中,Copyright
作为 InspirationGenerator
的子组件出现。这是因为 InspirationGenerator
接受 JSX 作为 children props,因此它将 Copyright
作为子组件渲染,但不导入该模块。Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。