纯函数:组件作为公式
在计算机科学中(尤其是函数式编程的世界中),纯函数 通常具有如下特征:
只负责自己的任务。它不会更改在该函数调用前就已存在的对象或变量。
输入相同,则输出相同。给定相同的输入,纯函数应总是返回相同的结果。
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} />
</>
);
}
![image]()
管你可能还没使用过,但在 React 中,你可以在渲染时读取三种输入:props,state 和 context。你应该始终将这些输入视为只读。
当你想根据用户输入 更改 某些内容时,你应该 设置状态,而不是直接写入变量。当你的组件正在渲染时,你永远不应该改变预先存在的变量或对象。
React 提供了 “严格模式”,在严格模式下开发时,它将会调用每个组件函数两次。通过重复调用组件函数,严格模式有助于找到违反这些规则的组件。
![image]()
原始示例显示的是 “Guest #2”、“Guest #4” 和 “Guest #6”,而不是 “Guest #1”、“Guest #2” 和 “Guest #3”。原来的函数并不纯粹,因此调用它两次就出现了问题。但对于修复后的纯函数版本,即使调用该函数两次也能得到正确结果。
纯函数仅仅执行计算,因此调用它们两次不会改变任何东西
局部 mutation:组件的小秘密
上述示例的问题出在渲染过程中,组件改变了 预先存在的 变量的值。为了让它听起来更可怕一点,我们将这种现象称为 突变(mutation) 。纯函数不会改变函数作用域外的变量、或在函数调用前创建的对象——这会使函数变得不纯粹!
但是,你完全可以在渲染时更改你 刚刚 创建的变量和对象。在本示例中,你创建一个 [] 数组,将其分配给一个 cups 变量,然后 push 一打 cup 进去:
push方法的作用:push()是JavaScript数组的一个方法,用于向数组末尾添加一个或多个元素。在
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaGathering() {
const cups = [];
for (let i = 1; i <= 12; i++) {
cups.push(<Cup key={i} guest={i} />);
}
return cups;
}
![image]()
如果 cups 变量或 [] 数组是在 TeaGathering 函数之外创建的,这将是一个很大的问题!因为如果那样的话,当你调用数组的 push 方法时,就会更改 预先存在的 对象。
但是,这里不会有影响,因为每次渲染时,你都是在 TeaGathering 函数内部创建的它们。TeaGathering 之外的代码并不会知道发生了什么。这就被称为 “局部 mutation” — 如同藏在组件里的小秘密。
摘要
一个组件必须是纯粹的,就意味着:
只负责自己的任务。 它不会更改在该函数调用前就已存在的对象或变量。
输入相同,则输出相同。 给定相同的输入,组件应该总是返回相同的 JSX。
渲染随时可能发生,因此组件不应依赖于彼此的渲染顺序。
你不应该改变任何用于组件渲染的输入。这包括 props、state 和 context。通过 “设置” state 来更新界面,而不要改变预先存在的对象。
努力在你返回的 JSX 中表达你的组件逻辑。当你需要“改变事物”时,你通常希望在事件处理程序中进行。作为最后的手段,你可以使用 useEffect。
编写纯函数需要一些练习,但它充分释放了 React 范式的能力。
实现:
两个 Profile 组件使用不同的数据并排呈现。在第一个资料中点击 “Collapse” 折叠,然后点击 “Expand” 展开它。
Panel.css 如下:
.panel {
border: 1px solid #ccc;
border-radius: 8px;
margin-bottom: 1rem;
padding: 1rem;
background-color: #fff;
overflow: hidden;
width: 300px;
}
.toggle-btn {
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
padding: 0.25rem 0.5rem;
margin-bottom: 1rem;
cursor: pointer;
}
.panel-content {
margin-top: 0.5rem;
}
export function getImageUrl(person) {
return (
'https://i.imgur.com/' +
person.imageId +
's.jpg'
);
}
utils.js 如下:
export function getImageUrl(person) {
return (
'https://i.imgur.com/' +
person.imageId +
's.jpg'
);
}
Panel.js 如下:
import { useState } from 'react';
import "./Panel.css"
export default function Panel({ children }) {
const [isExpanded, setIsExpanded] = useState(false);
const togglePanel = () => {
setIsExpanded(!isExpanded);
};
return (
<div className="panel">
<button onClick={togglePanel} className="toggle-btn">
{isExpanded ? 'Collapse' : 'Expand'}
</button>
{isExpanded && <div className="panel-content">{children}</div>}
</div>
);
}
Profile.js 如下:
import Panel from './Panel.js';
import { getImageUrl } from './utils.js';
export default function Profile({ person }) {
return (
<Panel>
<Header person={person} />
<Avatar person={person} />
</Panel>
)
}
function Header({ person }) {
return <h1>{person.name}</h1>;
}
function Avatar({ person }) {
return (
<img
className="avatar"
src={getImageUrl(person)}
alt={person.name}
width={50}
height={50}
/>
);
}
App.js如下:
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',
}} />
</>
);
}
效果如下:
![image]()