react学习笔记-保持组件纯粹

保持组件纯粹

部分JavaScript函数是纯粹的,这些函数通常被称为纯函数。纯函数近执行计算操作,不做其他操作。你可以通过将组件按纯函数严格编写,以避免一些随着代码库的增长而出现的、令人困扰的bug以及不可预测的行为。但为了获得这些好处,你需要遵循一些规则。

你将会学习到

-纯函数是什么,以及它如何帮助你避免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。

App.js:

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,而不改变在渲染前,就已存在的任何对象或变量-这将会使它们变得不纯粹!

以下是违反这一规则的组件示例:

App.js:

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传入来修复此组件:

App.js:

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中,你可以在渲染时读取三种输入:propsstatecontext。你应该始终将这些输入视为只读。

当你想根据用户输入更改某些内容时,你应该设置状态,而不是直接写入变量。当你的组件正在渲染时,你永远不应该改变预先存在的变量或对象。

React提供了“严格模式”,在严格模式下开发时,它将会调用每个组件函数两次。通过重复调用组件函数,严格模式有助于找到违反这些规则的组件。

我们注意到,原始示例显示的是“Guest #2”、“Guest #4”和“Guest #6”,而不是“Guest #1”、“Guest #2”和“Guest #3”。原来的函数并不纯粹,因此调用它两次就出现了问题。但对于修复后的纯函数版本,即是调用该函数两次也能得到正确结果。纯函数仅仅执行计算,因此调用它们两次不会改变任何东西-就像两次调用double(2)并不会改变返回值,两次求解y = 2x 不会改变y的值一样。相同的输入,总是返回相同的输出。

严格模式在生产环境下不生效,因此它不会降低应用程序的速度。如需引入严格模式,你可以用<React.StrictMode>包裹根组件。一些框架会默认这样做。

局部mutation:组件的小秘密

上述示例的问题出在渲染过程中,组件改变了预先存在的变量的值。为了让它听起来可怕一点,我们将这种现象称为突变(mutation)。纯函数不会改变函数作用域外的变量、或在函数调用前创建的对象—这会使函数变得不纯粹!

但是,你完全可以在渲染时更改你刚刚创建的变量或对象。在本示例中,你创建一个[]数组,将其分配给一个cups变量,然后push-一打cup进去:

App.js:

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;

}

如果cups变量或[]数组是在TeaGathering函数之外创建的,这将是一个很大的问题!因为如果那样的话,当你调用数组的push方法时,就会更改预先存在的对象。

但是,这里不会有影响,因为每次渲染时,你都是在TeaGathering函数内部创建的它们。TeaGathering之外的代码并不会知道发生了什么。这就被称为“局部mutation”-如同藏在组件里的小秘密。

哪些地方 可能 引发副作用

函数式编程在很大程度上依赖于纯函数,但某些事物在特定情况下不得不发生改变。这是编程的要义!这些变动包括更新屏幕、启动动画、更改数据等,它们被称为副作用。它

们是“额外”发生的事情,与渲染过程无关。

在React中,副作用通常属于事物处理程序。事件处理程序是React在你执行某些操作(如单机按钮)时运行的函数。即使事件处理程序是在你的组件内部定义的,它们也不会在渲染期间运行!因此事件处理程序无需是纯函数

如果你用尽一切办法,仍无法为副作用找到合适的事件处理程序,你还可以调用组件中的useEffect方法将其附加到返回的JSX中。这会告诉React在渲染结束后执行它。然而,这种方法应该是你最后的手段。

如果可能,请尝试仅通过渲染过程来表达你的逻辑。你会惊讶于这能带给你多少好处!

React为何侧重于纯函数?

编写纯函数需要遵循一些习惯和规程。但它开启了绝妙的机遇:

-你的组件可以在不用的环境下运行-例如,在服务器上!由于它们针对相同的输入,总是返回相同的额结果,因此一个组件可以满足多个用户需求。

-你可以为哪些输入未更改的组件来跳过渲染,以提高性能。这是安全的做法,因为纯函数总是返回相同的结果,所以可以安全地缓存它们。

-如果在渲染深层组件树的过程中,某些数据发生了变化,React可以重新开始渲染,而不会浪费时间完成过时的渲染。纯粹性使得它随时可以安全地停止计算。

我们正在构建的每个React新特性都利用到了纯函数。从数据获取到动画再到性能,保持组件的纯粹可以充分释放React范式的能力。

摘要

-个组件必须是纯粹的,就意味着:

--只负责自己的任务。它不会更改在该函数调用前就以存在的对象或变量。

--输入相同,则输出相同。给定相同的输入,组件应该总是返回相同的JSX。

-渲染随时可能发生,因此组件不应依赖于彼此的渲染顺序。

-你不应该改变任何用于组件渲染的输入。这包括props、state和context。通过“设置”state来更新界面,而不改变预先存在的对象。

-努力在你返回的JSX中表达你的组件逻辑。当你需要“改变事物”时,你通常希望在事件处理程序中进行。作为最后的手段,你可以使用useEffect。

-编写纯函数需要一些练习,但它充分释放了React范式的能力。

尝试一些挑战

修复坏掉的时钟

该组件尝试在午夜到早上6点期间,将<h1>的CSS类设置为“night”,而在其他时间都设置为“day”。但它不起作用。你能修复这个组件吗?

你可以临时更改计算机的时区来验证你的解决方案是否有效。当前时间位于午夜至早上六点之间时,时钟应该有相反的颜色!

Clock.js:

export default function Clock({ time }) {

  const hours = time.getHours();

  if (hours >= 0 && hours <= 6) {

    document.getElementById('time').className = 'night';

  } else {

    document.getElementById('time').className = 'day';

  }

  return (

    <h1 id="time">

      {time.toLocaleTimeString()}

    </h1>

  );

}

答案:

你可以计算className,并将其包含在渲染的输出中,一次实现对组件的修复:

Clock.js:

export default function Clock({ time }) {

  const hours = time.getHours();

  let className;

  if (hours >= 0 && hours <= 6) {

    className = 'night';

  } else {

    className = 'day';

  }

  return (

    <h1 className={className}>

      {time.toLocaleTimeString()}

    </h1>

  );

}

在这个例子中,副作用(修改DOM)完全没有必要。你需要返回FSX。

修复损坏的材料

两个Profile组件使用不同的数据并排呈现。在第一个资料中点击“Collapse”折叠,然后点击“Expand”展开它。你会看到两个资料现在显示的是同一个人。这是一个bug。

找出产生bug的原因,并修复它。

Profile.js:

import Panel from './Panel.js';

import { getImageUrl } from './utils.js';

let currentPerson;

export default function Profile({ person }) {

  currentPerson = person;

  return (

    <Panel>

      <Header />

      <Avatar />

    </Panel>

  )

}

function Header() {

  return <h1>{currentPerson.name}</h1>;

}

function Avatar() {

  return (

    <img

      className="avatar"

      src={getImageUrl(currentPerson)}

      alt={currentPerson.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',

      }} />

    </>

  )

}

初始状态

点击第一个collapse

答案:

问题在与Profile组件写入了一个预先存在的currentPerson变量,而Header和Avatar组件读取了这个变量。这使得三个组件都变得不纯粹且难以预测。

要修复这个bug,需要删除currentPerson变量。同时,通过props将所有信息从Profile传递到Header和Avatar中。你需要向两个组件各添加一个person prop,并将其一直向下传递。

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',

      }} />

    </>

  );

}

第一个按钮折叠再展开时没有改变图片内容

请记住,React无法保证组件函数以任何特定的顺序执行,因此你无法通过设置变量在它们之间进行通信。所有的交流都必须通过props进行。

修复损坏的故事集

你所在公司的CEO要求你在在线时钟app中添加“故事”,你不能拒绝。你编写了一个StoryTray组件,它接收一个stories列表,后跟一个“Create Story”占位符。

你在作为props的stories数组末尾push了一个假故事来实现“Create Story”。但处于某种原因,“Create Story”出现了不止一次。请修复这个问题。

StoryTray.js:

export default function StoryTray({ stories }) {

  stories.push({

    id: 'create',

    label: 'Create Story'

  });

  return (

    <ul>

      {stories.map(story => (

        <li key={story.id}>

          {story.label}

        </li>

      ))}

    </ul>

  );

}

答案:

请注意,每当时钟更新时,“Create Story”都会被添加两次。这暗示我们在渲染过程中发生了mutation-严格模式调用两次组件,可以使这些问题更加明显。

StoryTray的功能不纯粹。通过在接收到的stories数组(一个prop!)上调用push方法,它正改变着一个在StoryTray渲染之前创建的对象。这使得它有问题并且难以预测。

最简单的解决方案是完全不碰数组,单独渲染“Create Story”:

StoryTray.js:

export default function StoryTray({ stories }) {

  return (

    <ul>

      {stories.map(story => (

        <li key={story.id}>

          {story.label}

        </li>

      ))}

      <li>Create Story</li>

    </ul>

  );

}

或者,你可以在push之前创建一个数组(通过复制现有数组)

StoryTray.js:

export default function StoryTray({ stories }) {

  // 复制数组!

  const storiesToDisplay = stories.slice();

  // 不影响原始数组

  storiesToDisplay.push({

    id: 'create',

    label: 'Create Story'

  });

  return (

    <ul>

      {storiesToDisplay.map(story => (

        <li key={story.id}>

          {story.label}

        </li>

      ))}

    </ul>

  );

}

这使你的mutation保持在局部,并使你的渲染函数保持纯粹。但你仍然需要小心:例如,当你想要更改数组的任意项时,必须先对齐进行拷贝。

记住数组上的哪些操作会修改原始数组、哪些不会,这非常有帮助。例如,push、pop、reverse和sort会改变原始数组,但slice、filter和map则会创建一个新数组。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值