react 保持组件纯粹

Posted on 2026-04-03 13:20  打杂滴  阅读(1)  评论(0)    收藏  举报

纯函数:组件作为公式
在计算机科学中(尤其是函数式编程的世界中),纯函数 通常具有如下特征:

只负责自己的任务。它不会更改在该函数调用前就已存在的对象或变量。
输入相同,则输出相同。给定相同的输入,纯函数应总是返回相同的结果。

 

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

 

博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3