功能性组件和Classes有什么不同?

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

有一段时间,规范的答案是类可以访问更多功能(如状态)。但是自从有了Hook后,这个答案变得不唯一了。

也许你听说其中一个表现更好。哪一个?许多此类基准都存在缺陷,因此我会小心地从中得出结论。性能主要取决于代码的作用,而不是您选择的是函数还是类。在我们的观察中,虽然优化策略有点不同,但性能差异可以忽略不计。

在任何一种情况下,除非您有其他原因并且不介意成为早期采用者,否则我们不建议您重写现有组件。Hooks仍然是新的。

那么功能性函数和类是否又根本的区别?

函数组件捕获rendered的值。

让我们看看这句话是什么意思?

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

思考一下这个组件:

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’。很简单。(注意在上面的例子中我是否使用箭头或函数声明并不重要。函数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本身无关。以上示例甚至不使用Hooks!这就是React中函数和类之间的区别。如果您计划在React应用程序中更频繁地使用函数,则可能需要了解它。

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

打开此示例

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

  • 单击其中一个“关注”按钮。
  • 在3秒之前更改所选的配置文件。
  • 阅读警报文字。

你会注意到一个特殊的区别:

  • 使用上面的ProfilePage函数,单击Follow on Dan的个人资料,然后导航到Sophie’s仍然会提醒’Followed Dan’。
  • 使用上面的ProfilePage类,它会警告’Followed Sophie’:

《功能性组件和Classes有什么不同?》

在此示例中,第一个行为是正确的行为。如果我跟随一个人然后导航到另一个人的个人资料,我的组件不应该让使用的人感到困惑。这个类实现显然是错误的。
那么为什么我们的类示例会以这种方式运行?
让我们仔细看看我们类中的showMessage方法:

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

此类方法从this.props.user读取。PropsReact中是不可变的,因此它们永远不会改变。然而,这一直是,并且一直是可变的。
React本身会随着时间的推移而变异,以便您可以在渲染和生命周期方法中阅读新版本。

因此,如果我们的组件在请求处于运行状态时render,则this.props将会更改。showMessage方法从“too new”的props中读取用户。

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

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

一种方法是在事件期间尽早读取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,我们将再次遇到完全相同的问题。所以我们必须通过showMessage调用的每个方法将this.propsthis.state作为参数传递。

同样,在handleClick中内联警报代码并不能解决更大的问题。我们希望以允许将其拆分为更多方法的方式构造代码,同时还要读取与该调用相关的渲染所对应的propsstate。这个问题甚至不是React独有的 – 您可以在任何将数据放入像这样的可变对象的UI库中重现它。
也许,我们可以绑定构造函数中的方法?

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>;
  }
}

不,这不能解决任何问题。请记住,问题是我们从这里读取。支持太晚了 – 不是我们正在使用的语法!但是,如果我们完全依赖JavaScript闭包,问题就会消失。

通常会避免闭包,因为很难想象随着时间的推移可能会发生变异的价值。但在React中,propsstate是不可改变的!(或者至少,这是一个强烈的推荐。)这消除了一个主要的封闭区域。

这意味着如果你从特定渲染中关闭propsstate,它们的值保持完全相同:

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>;
  }
}

你在渲染时“捕获”了props

这样,它内部的任何代码(包括showMessage)都可以保证看到这个特定渲染的道具。React不再“move our cheese”了。
然后我们可以在里面添加任意数量的辅助函数,它们都会使用捕获的propsstate

上面的例子是正确的,但看起来很奇怪。如果在render中定义函数而不是使用类方法,那么有一个类是什么意思?
实际上,我们可以通过删除它周围的类“shell”来简化代码:

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

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

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

就像上面一样,props仍然被捕获 – React将它们作为参数传递。与此不同,props对象本身永远不会被React变异。如果你在函数定义中构造props,那就更明显了:

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

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

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

当父组件使用不同的props呈现ProfilePage时,React将再次调用ProfilePage函数。但是我们已经点击的事件处理程序“属于”具有自己的用户值的前一个渲染和读取它的showMessage回调。他们都完好无损。
《功能性组件和Classes有什么不同?》

现在我们了解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,但它说明了同样的观点:如果我发送特定消息,组件不应该对实际发送的消息感到困惑。此函数组件的消息捕获“属于”渲染器的状态,该渲染器返回浏览器调用的单击处理程序。因此,当我单击“发送”时,消息将设置为输入中的内容。

因此,默认情况下,我们知道React捕获道具和状态中的函数。但是,如果我们想要阅读不属于这个特定渲染的最新道具或州,该怎么办?如果我们想 “read them from the future”怎么办?

在类中,你可以通过阅读this.props或this.state来实现它,因为它本身是可变的。React改变了它。在函数组件中,您还可以具有由所有组件呈现共享的可变值。它被称为“ref”:

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

但是,您必须自己管理它。

ref与实例字段扮演相同的角色。它是进入可变命令世界的逃脱舱。您可能熟悉“DOM refs”,但概念更为通用。它只是一个盒子,你可以把东西放进去。即使在视觉上,这个东西看起来像是某种东西的镜子。它们代表了相同的概念。默认情况下,React不会为函数组件中的最新props或状态创建引用。在许多情况下,您不需要它们,分配它们将是浪费的工作。但是,如果您愿意,可以手动跟踪值:

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;
  };

如果我们在showMessage中读取消息,我们会在按下“发送”按钮时看到消息。但是当我们读取latestMessage.current时,我们得到最新的值 – 即使我们在按下发送按钮后继续输入。你可以比较两个演示(https://codesandbox.io/s/93m5… https://codesandbox.io/s/ox200vw8k9),看看差异。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);
  };

demo

结论

在这篇文章中,我们研究了类中常见的破碎模式,以及闭包如何帮助我们修复它。但是,您可能已经注意到,当您尝试通过指定依赖关系数组来优化Hook时,您可能会遇到带有过时闭包的错误。是否意味着闭包是问题?我不这么认为。

正如我们上面所看到的,闭包实际上帮助我们解决了很难注意到的细微问题。同样,它们使编写在并发模式下正常工作的代码变得更加容易。这是可以的,因为组件内部的逻辑关闭了正确的props和渲染state
在我到目前为止看到的所有情况中,由于错误地假设“功能不会改变”或“props总是相同”,所以会出现“陈旧的封闭”问题。事实并非如此,因为我希望这篇文章有助于澄清。

    原文作者:_小生_
    原文地址: https://segmentfault.com/a/1190000018391208
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞