主要记录阅读官方文档和做官方challenge过程中的一些个人查漏补缺的笔记。这是第一节描述UI的教程笔记,包括官方解释的纯函数概念,props的使用,列表的渲染等等。
默认导出 vs 具名导出
组件的导出方式决定了其导入方式。当你用默认导入的方式,导入具名导出的组件时,就会报错。如下表格可以帮你更好地理解它们:
语法 | 导出语句 | 导入语句 |
---|---|---|
默认 | export default function Button() {} | import Button from './Button.js'; |
具名 | export function Button() {} | import { Button } from './Button.js'; |
当使用默认导入时,你可以在 import
语句后面进行任意命名。比如 import Banana from './Button.js'
,如此你能获得与默认导出一致的内容。相反,对于具名导入,导入和导出的名字必须一致。这也是为什么称其为 具名 导入的原因!
通常,文件中仅包含一个组件时,人们会选择默认导出,而当文件中包含多个组件或某个值需要导出时,则会选择具名导出。 无论选择哪种方式,请记得给你的组件和相应的文件命名一个有意义的名字。我们不建议创建未命名的组件,比如 export default () => {}
,因为这样会使得调试变得异常困难。
JSX规则
三个基本规则
-
只能返回一个根元素
注意JSX需要一个根元素包裹,如果你不想在标签中增加一个额外的
<div>
,可以用<>
和</>
元素来代替
<>
<h1>海蒂·拉玛的代办事项</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
class="photo"
>
<ul>
...
</ul>
</>
这个空标签被称作 Fragment. React Fragment 允许你将子元素分组,而不会在 HTML 结构中添加额外节点。
为什么多个 JSX 标签需要被一个父元素包裹?
JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。
-
标签必须闭合
-
使用驼峰式命名法给 大部分属性命名
理解:JSX中内联css为什么用“双大括号”
外面的大括号用来包裹JS表达式,里面的大括号则表示一个JS对象
<ul style={
{
backgroundColor: 'black',
color: 'pink'
}
}>
内联
style
属性 使用驼峰命名法编写。例如,HTML<ul style="background-color: black">
在你的组件里应该写成<ul style={{ backgroundColor: 'black' }}>
。
props推荐写法
props指定默认值
如果你想在没有指定值的情况下给 prop 一个默认值,你可以通过在参数后面写 =
和默认值来进行解构:
function Avatar({ person, size = 100 }) {
// ...
}
默认值仅在缺少 size
prop 或 size={undefined}
时生效。 但是如果你传递了 size={null}
或 size={0}
,默认值将 不 被使用。
JSX展开传递props
function Profile(props) {
return (
<div className="card">
<Avatar {...props} />
</div>
);
}
这会将 Profile
的所有 props 转发到 Avatar
,而不列出每个名字。
请克制地使用展开语法。 如果你在所有其他组件中都使用它,那就有问题了。 通常,它表示你应该拆分组件,并将子组件作为 JSX 传递。
将JSX作为子组件传递
html中我们能用组件嵌套组件,那如何能够用组件嵌套组件?
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>
);
}
可以将带有 children
prop 的组件看作有一个“洞”,可以由其父组件使用任意 JSX 来“填充”。你会经常使用 children
prop 来进行视觉包装:面板、网格等等。
条件渲染&列表渲染
陷阱:请勿将数字放在&&
左侧
JavaScript 会自动将左侧的值转换成布尔类型以判断条件成立与否。然而,如果左侧是 0
,整个表达式将变成左侧的值(0
),React 此时则会渲染 0
而不是不进行渲染。
例如,一个常见的错误是 messageCount && <p>New messages</p>
。其原本是想当 messageCount
为 0 的时候不进行渲染,但实际上却渲染了 0
。
为了更正,可以将左侧的值改成布尔类型:messageCount > 0 && <p>New messages</p>
。
key的相关注意事项
-
直接放在
map()
方法里的 JSX 元素一般都需要指定key
值!这些 key 会告诉 React,每个组件对应着数组里的哪一项,所以 React 可以把它们匹配起来。这在数组项进行移动(例如排序)、插入或删除等操作时非常重要。一个合适的
key
可以帮助 React 推断发生了什么,从而得以正确地更新 DOM 树。用作 key 的值应该在数据中提前就准备好,而不是在运行时才随手生成: -
组件不会把
key
当作 props 的一部分Key 的存在只对 React 本身起到提示作用。如果你的组件需要一个 ID,那么请把它作为一个单独的 prop 传给组件:
<Profile key={id} userId={id} />
。 -
key 值在兄弟节点之间必须是唯一的。key 值不能改变。
// 官方列表渲染示例
import { people } from './data.js';
import { getImageUrl } from './utils.js';
export default function List() {
const listItems = people.map(person =>
<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>;
}
如何让map时每个列表项显示多个DOM节点
Fragment 语法的简写形式 <> </>
无法接受 key 值,所以你只能要么把生成的节点用一个 <div>
标签包裹起来,要么使用长一点但更明确的 <Fragment>
写法:
import { Fragment } from 'react';
// ...
const listItems = people.map(person =>
<Fragment key={person.id}>
<h1>{person.name}</h1>
<p>{person.bio}</p>
</Fragment>
);
这里的 Fragment 标签本身并不会出现在 DOM 上,这串代码最终会转换成 <h1>
、<p>
、<h1>
、<p>
…… 的列表。
保持组件纯粹
纯函数
纯函数 通常具有如下特征:
- 只负责自己的任务。它不会更改在该函数调用前,就已存在的对象或变量。
- 输入相同,则输出相同。给定相同的输入,纯函数应总是返回相同的结果。
React 便围绕着这个概念进行设计。React 假设你编写的所有组件都是纯函数。也就是说,对于相同的输入,你所编写的 React 组件必须总是返回相同的 JSX。
副作用
React 的渲染过程必须自始至终是纯粹的。组件应该只 返回 它们的 JSX,而不 改变 在渲染前,就已存在的任何对象或变量 — 这将会使它们变得不纯粹!比如读写组件外部声明的变量,意味着 多次调用这个组件会产生不同的 JSX!
用严格模型来检测这些不纯的计算
当你想根据用户输入 更改 某些内容时,你应该 设置状态,而不是直接写入变量。当你的组件正在渲染时,你永远不应该改变预先存在的变量或对象。
React 提供了 “严格模式”,在严格模式下开发时,它将会调用每个组件函数两次。通过重复调用组件函数,严格模式有助于找到违反这些规则的组件。
原来的函数并不纯粹,因此调用它两次就出现了问题。但对于修复后的纯函数版本,即使调用该函数两次也能得到正确结果。纯函数仅仅执行计算,因此调用它们两次不会改变任何东西 — 就像两次调用 double(2)
并不会改变返回值,两次求解 y = 2x 不会改变 y 的值一样。相同的输入,总是返回相同的输出。
严格模式在生产环境下不生效,因此它不会降低应用程序的速度。如需引入严格模式,你可以用 <React.StrictMode>
包裹根组件。一些框架会默认这样做。
局部mutation
官方解释,虽然不能改变预先存在的变量的值,但完全可以在渲染时更改你 刚刚 创建的变量和对象。也就是组件内部创建使用变量是不会有影响的。
哪些地方可能引发副作用
某些事物 在特定情况下不得不发生改变。这些变动包括更新屏幕、启动动画、更改数据等,它们被称为 副作用。它们是 “额外” 发生的事情,与渲染过程无关。
在 React 中,副作用通常属于 事件处理程序。事件处理程序是 React 在你执行某些操作(如单击按钮)时运行的函数。即使事件处理程序是在你的组件 内部 定义的,它们也不会在渲染期间运行! 因此事件处理程序无需是纯函数。
如果无法为副作用找到合适的事件处理程序,你还可以调用组件中的 useEffect
方法将其附加到返回的 JSX 中。这会告诉 React 在渲染结束后执行它。然而,这种方法应该是你最后的手段。
React为何侧重纯函数
- 你的组件可以在不同的环境下运行 — 例如,在服务器上!由于它们针对相同的输入,总是返回相同的结果,因此一个组件可以满足多个用户请求。
- 你可以为那些输入未更改的组件来 跳过渲染,以提高性能。这是安全的做法,因为纯函数总是返回相同的结果,所以可以安全地缓存它们。
- 如果在渲染深层组件树的过程中,某些数据发生了变化,React 可以重新开始渲染,而不会浪费时间完成过时的渲染。纯粹性使得它随时可以安全地停止计算。
小结
-
一个组件必须是纯粹的,就意味着:
- 只负责自己的任务。 不应更改渲染前存在的任何对象或变量。
- 输入相同,则输出相同。 给定相同的输入,组件应该总是返回相同的 JSX。
-
渲染随时可能发生,因此组件不应依赖于彼此的渲染顺序。
-
你不应该改变组件用于渲染的任何输入。这包括 props、state 和 context。通过 “设置” state) 来更新界面,而不要改变预先存在的对象。
-
努力在你返回的 JSX 中表达你的组件逻辑。当你需要“改变事物”时,你通常希望在事件处理程序中进行。作为最后的手段,你可以使用
useEffect
。 -
编写纯函数需要一些练习,但它充分释放了 React 范式的能力。