Fork me on GitHub

函数组件与类有何不同?

React函数组件与React类有何不同?

一段时间以来,规范的答案一直是类提供对更多功能(如状态)的访问。使用Hooks不再是事实。

也许您已经听说其中之一的性能更好。哪一个?许多这样的基准都是有缺陷的,因此我会谨慎地从中得出结论性能主要取决于代码在做什么,而不取决于您选择的是函数还是类。在我们的观察中,性能差异可以忽略不计,但优化策略是有点不同

无论哪种情况,除非您有其他原因并且不介意成为早期采用者,否则我们都不建议您重写现有组件。挂钩仍然很新(就像React在2014年一样),并且一些“最佳实践”还没有进入教程。

那那把我们留在哪里呢?React函数和类之间根本没有根本区别吗?当然,在心理模型中有。在本文中,我将探讨它们之间的最大区别。自从2015年引入功能组件以来,它就一直存在,但经常被人们忽略:

功能组件捕获呈现的值。

让我们解开这意味着什么。


注意:本文不是对类或函数的价值判断。我只是在React中描述这两种编程模型之间的区别。有关更广泛地采用功能的问题,请参阅Hooks FAQ


考虑以下组件:

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

它显示一个模拟网络请求的按钮,setTimeout然后显示一个确认警报。例如,如果props.user'Dan',它将'Followed Dan'在三秒钟后显示很简单。

(请注意,在上面的示例中使用箭头还是函数声明都没关系。function handleClick()将以完全相同的方式工作。)

我们如何编写它作为一个类?幼稚的翻译可能看起来像这样:

class ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

通常认为这两个代码段是等效的。人们经常在这些模式之间自由重构,而没有注意到它们的含义:

找出两个版本之间的差异

但是,这两个代码段略有不同。好好看看他们。你看到区别了吗?就个人而言,我花了一段时间才看到这一点。

前面有剧透,所以如果您想自己解决这个问题,这里有一个现场演示本文的其余部分将说明差异及其重要性。


在继续之前,我想强调一下,我所描述的差异与React Hooks本身无关。上面的示例甚至都没有使用钩子!

都是关于React中函数和类之间的区别。如果您打算在React应用程序中更频繁地使用函数,那么您可能想了解它。


我们将通过React应用程序中常见的错误来说明差异。

使用当前的配置文件选择器和上面的两个实现打开此示例沙箱ProfilePage-分别呈现“关注”按钮。

使用两个按钮尝试以下操作顺序:

  1. 单击“跟随”按钮之一。
  2. 在3秒钟之前更改选定的配置文件。
  3. 阅读警报文本。

您会发现一个独特的区别:

  • 使用上述ProfilePage 功能,在Dan的个人资料上单击“关注”,然后导航至Sophie's仍会提示'Followed Dan'
  • 使用上述ProfilePage 类,它会发出警报'Followed Sophie'

示范步骤


在此示例中,第一个行为是正确的行为。如果我关注某人,然后导航至其他人的个人资料,则我的组件不会对我关注的人感到困惑。此类的实现显然是错误的。

(不过,您应该完全跟随苏菲。)


那么为什么我们的类示例会这样表现呢?

让我们仔细看一下showMessage我们类中的方法:

class ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);  };

此类方法从中读取this.props.user道具在React中是不可变的,因此它们永远不会改变。但是,this ,并且一直都是可变的。

确实,那this是课堂上的全部目的随着时间的推移,React自身会对其进行变异,以便您可以在render和生命周期方法中阅读最新版本

因此,如果在请求进行过程中我们的组件重新呈现,则this.props它将更改。showMessage方法user从“太新”中读取props

这暴露了有关用户界面性质的有趣观察。如果说UI从概念上来说是当前应用程序状态的函数,则事件处理程序是呈现结果的一部分-就像可视输出一样我们的事件处理程序“属于”具有特定道具和状态的特定渲染。

但是,安排一个超时时间(其回调读取)this.props会破坏该关联。我们的showMessage回调未“绑定”到任何特定的渲染,因此它“丢失”了正确的道具。从阅读中this切断了这种联系。


假设功能组件不存在。我们将如何解决这个问题?

我们想要以某种方式“修复”render具有正确道具和showMessage读取道具的回调之间的连接一路上props迷路了。

一种方法是this.props在事件期间及早阅读,然后将其显式传递给超时完成处理程序:

class ProfilePage extends React.Component {
  showMessage = (user) => {    alert('Followed ' + user);
  };

  handleClick = () => {
    const {user} = this.props;    setTimeout(() => this.showMessage(user), 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

有效但是,这种方法使代码随着时间的推移变得更加冗长且易于出错。如果我们需要的不仅仅是一个道具,该怎么办?如果我们还需要访问该州怎么办?如果showMessage调用另一个方法,并且该方法读取this.props.somethingthis.state.something,我们将再次遇到完全相同的问题。因此,我们将必须将this.propsthis.state作为参数传递给from调用的每个方法showMessage

这样做会打败一堂课通常提供的人机工程学。这也很难记住或执行,这就是为什么人们经常选择解决错误的原因。

同样,内联alert代码handleClick无法解决更大的问题。我们希望以一种可以将其拆分为更多方法的方式来构造代码,而且还可以读取与该调用相关的渲染相对应的props和state。这个问题甚至不是React独有的-您可以在将数据放入可变对象(如)的任何UI库中重现此问题this

也许,我们可以方法绑定到构造函数中?

class ProfilePage extends React.Component {
  constructor(props) {
    super(props);
    this.showMessage = this.showMessage.bind(this);    this.handleClick = this.handleClick.bind(this);  }

  showMessage() {
    alert('Followed ' + this.props.user);
  }

  handleClick() {
    setTimeout(this.showMessage, 3000);
  }

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

不,这不能解决任何问题。记住,问题在于我们读this.props得太晚了—而不是我们正在使用的语法!但是,如果我们完全依靠JavaScript闭包,问题将消失。

经常避免使用闭包,因为很难考虑可以随时间变化的值。但是在React中,道具和状态是不可变的!(或者至少是一个强烈的建议。)这消除了关闭的主要负担。

这意味着,如果您关闭特定渲染的道具或状态,则始终可以指望它们保持完全相同:

class ProfilePage extends React.Component {
  render() {
    // Capture the props!    const props = this.props;
    // Note: we are *inside render*.
    // These aren't class methods.
    const showMessage = () => {
      alert('Followed ' + props.user);    };

    const handleClick = () => {
      setTimeout(showMessage, 3000);
    };

    return <button onClick={handleClick}>Follow</button>;
  }
}

您已经在渲染时“捕获”了道具

这样,showMessage保证其中的任何代码(包括)都能看到该特定渲染器的道具。React不再“移动我们的奶酪”。

然后,我们可以在内部添加任意数量的帮助程序函数,并且它们都将使用捕获的道具和状态。关闭救援!


上面示例是正确的,但看起来很奇怪。如果在内部定义函数render而不使用类方法,那么拥有一个类有什么意义

实际上,我们可以通过删除周围的类“ shell”来简化代码:

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

就像上面一样,props仍然被捕获-React将它们作为参数传递。不像thisprops对象本身不会被React突变。

如果您props在函数定义中进行分解,则会更加明显

function ProfilePage({ user }) {  const showMessage = () => {
    alert('Followed ' + user);  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

当父组件ProfilePage使用不同的道具进行渲染时,React将ProfilePage再次调用该函数。但是我们已经单击了事件处理程序,该事件处理程序使用其自己的user值和showMessage读取该事件回调单击了“属于”先前的渲染他们都完好无损。

这就是为什么在此演示的功能版本中,单击Sophie的配置文件上的“关注”,然后将选择更改为Sunil会提示'Followed Sophie'

正确行为演示

此行为是正确的。(尽管您可能也想关注Sunil!)


现在我们了解了React中函数和类之间的巨大区别:

功能组件捕获呈现的值。

对于Hooks,相同的原理也适用于状态。考虑以下示例:

function MessageThread() {
  const [message, setMessage] = useState('');

  const showMessage = () => {
    alert('You said: ' + message);
  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
  };

  return (
    <>
      <input value={message} onChange={handleMessageChange} />
      <button onClick={handleSendClick}>Send</button>
    </>
  );
}

(这是现场演示。)

虽然这不是一个很好的消息应用程序UI,但它说明了同一点:如果我发送特定消息,则组件不应对实际发送的消息感到困惑。该功能组件message捕获“属于”渲染的状态,该状态返回了浏览器调用的单击处理程序。因此,message将设置为当我单击“发送”时输入的内容。


因此,我们知道React默认会捕获props和state中的函数。但是,如果我们阅读不属于该特定渲染器的最新道具或状态怎么办?如果我们想“从未来阅读它们”怎么办?

在课堂上,您可以通过阅读this.propsthis.state因为this它本身是可变的来实现。React使它变异。在功能组件中,还可以具有所有组件渲染器共享的可变值。它被称为“参考”:

function MyComponent() {
  const ref = useRef(null);
  // You can read or write `ref.current`.
  // ...
}

但是,您必须自己进行管理。

引用实例字段具有相同的作用这是进入可变命令性世界的逃生门。您可能熟悉“ DOM refs”,但是这个概念更为笼统。这只是一个盒子,您可以在其中放一些东西。

即使在视觉上,也this.something看起来像的镜子something.current它们代表相同的概念。

默认情况下,React不会为功能组件中的最新道具或状态创建引用。在许多情况下,您不需要它们,分配它们将浪费大量的工作。但是,您可以根据需要手动跟踪值:

function MessageThread() {
  const [message, setMessage] = useState('');
  const latestMessage = useRef('');
  const showMessage = () => {
    alert('You said: ' + latestMessage.current);  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
    latestMessage.current = e.target.value;  };

如果我们读messageshowMessage,则在按下“发送”按钮时将看到消息。但是,当我们阅读时latestMessage.current,即使在按下“发送”按钮后继续键入,我们也会获得最新的值。

您可以比较这两个 演示以自己查看差异。引用是一种“退出”渲染一致性的方法,在某些情况下可以方便使用。

通常,应避免渲染过程中读取或设置ref 因为它们是可变的。我们希望使渲染可预测。但是,如果我们要获取特定道具或状态的最新值,则手动更新ref可能会很烦人。我们可以使用效果使其自动化:

function MessageThread() {
  const [message, setMessage] = useState('');

  // Keep track of the latest value.  const latestMessage = useRef('');  useEffect(() => {    latestMessage.current = message;  });
  const showMessage = () => {
    alert('You said: ' + latestMessage.current);  };

(这里是一个演示。)

我们效果进行分配,以便ref值仅在DOM更新后才更改。这确保了我们的变异不会破坏依赖于可中断渲染的“时间片”和“暂挂”等功能

不需要经常使用这样的ref。捕获道具或状态通常是更好的默认设置。但是,在处理诸如间隔和订阅之类的命令性API时可能会很方便请记住,您可以跟踪任何这样的值-一个prop,一个状态变量,整个props对象甚至一个函数。

这种模式也可以方便地进行优化-例如,当useCallback身份更改过于频繁时。但是,使用减速器通常是更好的解决方案(有关未来博客文章的主题!)


在本文中,我们研究了类中常见的损坏模式,以及闭包如何帮助我们修复该模式。但是,您可能已经注意到,当您尝试通过指定依赖项数组来优化Hooks时,您可能会遇到带有过期闭包的bug。这是否意味着关闭是问题所在?我不这么认为。

正如我们在上面看到的,闭包实际上可以帮助我们解决难以发现的细微问题。同样,它们使编写在并发模式下正常工作的代码变得容易得多这是可能的,因为组件内部的逻辑关闭了渲染该组件的正确属性和状态。

到目前为止,在所有情况下,“过时的关闭”问题都是由于错误地假设“功能不变”或“道具始终相同”而发生的情况并非如此,因为我希望这篇帖子有助于澄清。

功能封闭了它们的道具和状态-因此,它们的身份同样重要。这不是错误,而是功能组件的功能。例如,不应将功能从“依赖项数组”中排除为useEffectuseCallback(正确的解决方案通常是useReducer上述useRef解决方案之一,或者是上述解决方案-我们很快会记录如何在它们之间进行选择。)

当我们使用函数编写大多数React代码时,我们需要调整关于优化代码的直觉,以及随时间变化的值

正如弗雷德里克(Fredrik)所说

到目前为止,用钩子找到的最好的心理规则是“代码好像随时可以更改任何值”。

函数也不例外。这在React学习材料中成为常识将需要一些时间。它需要从班级的心态进行一些调整。但我希望本文能帮助您以崭新的眼光看它。

React函数始终捕获其值-现在我们知道了原因。

 

 
posted @ 2021-03-12 16:20  广东靓仔-啊锋  阅读(267)  评论(0编辑  收藏  举报