[译]函数组件和类组件到底哪里不一样

原文:How Are Function Components Different from Classes?javascript

译者: github.com/joe06102html

React 里的函数组件和类组件有哪些不一样?java

一度,二者的区别在于类组件能提供更多的能力(好比局部的状态)。可是有了Hooks以后,状况却有所不一样了。react

也许以前你据说性能也是二者的差异。可是哪一个性能更好?很差说。我一直很谨慎地对待这类结论,由于不少性能测试都是不全面的。性能主要仍是在于代码的逻辑而非你选择了函数组件或者类组件。据咱们观察,虽然二者的优化策略有所差异,可是总体来讲性能区别是微不足道的。git

不管哪一种状况下,咱们都不推荐用 Hooks 重写你现有的组件。除非你有其余缘由而且不介意作第一个吃螃蟹的人。由于 Hooks 还不是很成熟(就像 2014 年的 React),有一些“最佳实践”尚待发掘。github

因此咱们该怎么办?函数组件和类组件本质上难道没有区别吗?固然有,二者在心智模型上有区别,也就是接下来我要说的二者之间最大的区别。早在 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,3 秒后弹窗就会显示‘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 自己无关。由于刚才的例子都没用到 Hooks!

这个差异只关乎函数组件和类组件。若是你常常用到函数组件的话, 你应该要理解它。


咱们会经过一个 React 应用里常见的 Bug 来讲明这个差异。

打开这个示例,里面有一个表示当前用户的下拉框以及上面实现的两个关注组件 -- 每一个都渲染了一个关注按钮。

按照下面的顺序,分别操做两个的按钮:

  1. 点击其中一个 Follow 按钮
  2. 在 3 秒内改变下拉框中选择的用户
  3. 观察弹窗中出现的文字

你会看到一个奇怪的现象:

  • 函数组件中,点击 Follow Dan,而后切换到 Sophie,弹窗仍然显示‘Followed Dan’
  • 类组件中,弹窗却显示‘Followed Sophie’:

Demonstration of the steps

在这个例子里,表现正确的应该是第一个。若是我点击关注了一我的,而后切换到另外一我的,个人组件应该知道我最后关注了谁。 类组件的表现很明显不正确。

(可是你真的应该关注Sophie。)


因此类组件的表现为何是这样的?

让咱们仔细看下showMessage这个方法:

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

这个方法从this.props.user中读取用户。在 React 中,props 是不可变的,因此它们不会改变。可是,this一直都是可变的。

事实上,这就是this在 class 中的目的。React 经过常常的更新 this 来保证你在 render 和生命周期方法中总能读取到最新版本的数据。

因此,当咱们请求期间,this.props 改变了,showMessage方法就会从’太新‘的 props 中读取用户信息。

这其实展露了一个关于 UI 本质的现象。若是概念上来讲 UI 是应用当前状态的一种映射,那处理事件其实也是渲染的一部分结果 - 就像视觉上的渲染输出同样。咱们的处理事件其实”属于“某个特定 props 和 state 生成的特定渲染。

可是,延时任务打破了这种关联。咱们的showMessage回调再也不和特定的渲染绑定,因此就”失去“了其正确的 props。正是从 this 中读取这个行为,切断了二者之间的关联。


假设函数组件不存在,咱们会怎么解决这类问题?

咱们想沿着 props 丢失的地方,以某种方式修复正确的 props 生成的渲染和 showMessage 回调之间的关联。

其中一种方法就是尽早地从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>;
  }
}
复制代码

上面这种方法能够奏效。可是,随着时间的推移,这样的代码会变得很啰嗦,而且容易出错。若是须要更多属性怎么办?若是还要访问 state 呢?若是showMessage又调用了别的函数,而这个函数又从 props 或者 state 读取了某些属性,咱们又会遇到一样的问题。因此咱们仍是得传递 this.props 和 this.state 给每一个在 showMessage 中被调用的函数。

这么作很容易下降效率。并且开发人员很容易忘记,也很难保证必定会按照上面的方法来避免问题,因此常常要去解决这类 bug。

一样的,把alert代码内联到handleClick也解决不了这个问题。咱们但愿以更细的粒度来组织代码,同时也但愿能读取和特定渲染对应的 props 和 state 值。这个问题其实非 React 才有,-- 任何一个存在相似this这类可变数据对象的 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>;
  }
}
复制代码

这样其实根本没什么用。记住,问题在于咱们读取this.props的时机太晚了 - 和咱们用的语法没什么关系!不过,若是咱们借助 JavaScript 的闭包,就能够彻底解决这个问题。

平时咱们老是避免闭包。由于随着时间推移,闭包中的可变对象会变得很难维护。可是在 React 中 props 和 state 都是不可变的(至少咱们是这么推荐的!)。这样就避免了搬起石头砸本身的脚。

这意味着若是你将每次渲染的 props 或者 state 关住,你就能够保证他们对应的都是特定渲染的结果:

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:

Capturing Pokemon

这样一来,任何内部的代码(包括showMessage)就能保证读取到的都是此次渲染的 props。React 也就”动不了咱们的奶酪”了。

而后咱们想加多少函数就能加多少,而且它们都能读取到正确的 props 或者 state。闭包简直是救星!

可是上面的例子看起来有些奇怪。既然都有类了,咱们为何还要在 render 函数里定义函数,而不直接定义类的方法?

事实上,咱们能够经过拿掉“class”的外壳来简化代码:

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

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

  return <button onClick={handleClick}>Follow</button>;
}
复制代码

和以前的例子同样,这里的props也能被捕获 - 由于 React 将它做为参数传递。不一样于this,React 不会对这里的props对象对任何修改。

若是你在函数定义中将props解构,看起来会更加明显。

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

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

  return <button onClick={handleClick}>Follow</button>;
}
复制代码

当父组件以不一样的 props 去渲染ProfilePage组件时,React 会从新调用ProfilePage这个函数。可是咱们以前已经触发的事件处理函数仍然“属于”上一次渲染,而且user的值以及引用它的showMessage回调也“属于”上一次。一切都是原封不动。

这也就是为何,在这个demo的函数式版本中,点击 Follow Sophie,而后切换到 Sunil,却仍然提示Followed Sophie:

Demo of correct behavior


如今,咱们理解了 React 中函数式组件和类组件之间最大的区别就是:

函数式组件能够捕获渲染的值。

在 Hooks 中,这个规则也一样适用于 state。 假若有下面的例子:

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>
    </>
  );
}
复制代码

(这里是在线 Demo)

尽管这不是一个很好的聊天应用界面,可是它也能说明一样的问题:若是我发送了一条消息,应用应该清楚地知道我发送了哪条消息。这个函数组件中的message捕获了特定渲染的 state,而浏览器所点击的事件处理函数也是渲染结果的一部分。因此message就是我点击“Send”时输入的值。

如今咱们知道了 React 中的函数默承认以捕获 props 和 state。可是若是咱们想访问不属于此次渲染的最新的 props 或 state 呢?或者是想要稍后再访问它们呢?

在类组件中,你会经过读取this.props或者this.state来实现由于 this 自己是可变的。React 会修改它。在函数组件中,你能够拥有一个可变的值,而且它在每次组件渲染的时候都是能够被共享的。这就是ref

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

可是,你必需要本身管理它。 ref扮演了一个实例字段的角色。它就像是逃往可变的、命令式世界的出口。你可能对”Dom Refs“比较熟悉,可是 ref 的概念更加通用一些。它就是一个你能够往里面装些东西的盒子。

甚至在视觉上,this.somthing看起来就像是另外一个something.current。它们有着一样的含义。

默认状况下,React 不会在函数式组件中为最新的 props 或者 state 建立 ref。由于大多数状况下你不须要它,声明它就会有点浪费。可是若是你须要,能够手动跟踪这些值。

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里的 message 值,咱们就能够看到在咱们按下发送按钮那一刻它的值。可是当咱们从latestMessage.current读取的时候,咱们会看到最新的值 -- 即便在咱们点击发送以后继续输入。

大家能够本身比较一下这两个例子,看看二者有什么区别。Ref 提供了一种”退出“一致性渲染规则的方法,在某些状况下比较有用。

总的来讲,你应该避免在渲染时读取或设置 refs,由于它们是可变的。咱们但愿保持渲染可预测。可是,若是你想读取某个 prop 或 state 的最新值,手动更新 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 赋值来保证 ref 的值只在每次 DOM 更新以后才会改变。这样能够确保咱们的改变不会影响别的功能例如Time SlicingSuspense,由于它们依赖于非连续的渲染。

一般咱们不须要这样使用 Ref。大部分状况下,捕获 props 或 state 是更好的方式。可是,当处理一些命令式的 APIs 像intervalsubscriptions会比较方便。记住你能够像使用 this 同样来追踪任何值 -- 一个属性,一个状态变量,整个 props 对象,甚至是一个函数。

这个模式能够方便地优化代码 -- 例如useCallback的依赖改变的过于频繁时。可是,使用 reducer 一般是一个更好地选择(又是一个能够用一篇文章来讨论的新特性!)


在这片文章里,咱们了解了类中常见的问题,以及闭包是如何帮咱们解决的。可是,当你想要指定依赖来优化 Hooks 的时候,可能会遇到过时的闭包问题。这是否意味着闭包才是问题所在呢?我不这么认为。

就像咱们上面看到的,闭包帮助咱们解决了一些平时很难注意到的细微的问题。而且,它也能帮咱们方便地写出在并发模式下也能够正常工做的代码。这是由于组件内部逻辑绑定了它渲染时所对应的的 props 和 state。

在我所了解的案例中,“过时的闭包”问题都是由于开发者错误的认为“函数不会改变”或者“props 一直都是相同的”。但事实并不是如此,但愿我这篇文章里已经解释清楚了。

函数组件会和 props 和 state 绑定,因此确认它们的对应性很重要。这不是 bug,而是函数组件的特色。例如,函数也不该该从 useEffect 或者 useCallback 的依赖中被移除。(正确的作法是使用 useReducer 或者 useRef 解决,咱们很快就会出一份关于 2 者如何选择的文档)。

当咱们在咱们的 React 项目中大量使用函数组件时,咱们须要调整对代码优化以及哪些值会随着时间改变的见解。

就像Fredrik 说得

到目前为止,我发现使用 hooks 最好的心智规则就是:编码的时候当作任何值在任什么时候候都会改变。

函数也不该该排除在这条规则以外。可是把它当作 React 学习中的常识还须要一段时间。由于它要求开发者从类组件的心智模型中作一些调整。可是我但愿这篇文章能帮你从一个全新的视角看待这个问题。

React 函数组件老是能捕获它们的值 -- 如今咱们知道了为何。

Smiling Pikachu

由于它们是各类不一样的神奇宝贝。

Discuss on TwitterEdit on GitHub

相关文章
相关标签/搜索