Skip to content

React为什么要用函数式组件

Posted on:August 30, 2022

引出问题

试想有这么一个组件,它渲染了一个利用 setTimeout 来模拟网络请求,然后显示一个确认警告的按钮。 函数式:

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

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

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

class 组件式:

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

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

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

尝试按照以下顺序来分别使用这两个按钮:

  1. 点击  其中某一个 Follow 按钮。
  2. 在 3 秒内  切换  选中的账号。
  3. 查看  弹出的文本。

你将看到一个奇特的区别:

在这个例子中,第一个行为是正确的。如果我关注一个人,然后导航到了另一个人的账号,我的组件不应该混淆我关注了谁。 在这里,类组件的实现很明显是错误的。

原因

让我们来仔细看看我们类组件中的  showMessage  方法:

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

这个类方法从  this.props.user  中读取数据。在 React 中 Props 是不可变(immutable)的,所以他们永远不会改变。然而,this是,而且永远是,可变(mutable)的。

事实上,这就是类组件  this  存在的意义。React 本身会随着时间的推移而改变,以便你可以在渲染方法以及生命周期方法中得到最新的实例

因为 this 是挂载到组件实例上的,在 React 的整个生命周期里伴随着组件的创建、更新、销毁,所以一旦 React 组件重新渲染,就会生成一个新的实例,从而 this 也指向了这个新的实例,导致组件内的 state 和 props 始终都是最新的值。

所以如果在请求已经发出的情况下我们的组件进行了重新渲染,this.props将会改变。showMessage方法从一个“过于新”的props中得到了user

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

然而,调用一个回调函数读取  this.props  的 timeout 会打断这种关联。我们的  showMessage  回调并没有与任何一个特定的渲染“绑定”在一起,所以它“失去”了正确的 props。从 this 中读取数据的这种行为,切断了这种联系。

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

一种方法是在调用事件之前读取this.props,然后将他们显式地传递到 timeout 回调函数中去:

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

但这种方法使得代码明显变得更加冗长,并且随着时间推移容易出错。如果我们需要的不止是一个 props 怎么办?如果我们还需要访问 state 怎么办?如果 showMessage 调用了另一个方法,然后那个方法中读取了 this.props.something 或者 this.state.something,我们又将遇到同样的问题。然后我们不得不将this.propsthis.state以函数参数的形式在被showMessage调用的每个方法中一路传递下去。

如果我们能利用 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>;
  }
}

结论

函数组件捕获了渲染所使用的值,保证了在一段时间内,相同的状态渲染相同的结果。

Hooks 的优势

  1. 函数组件的写法更轻量,更灵活: 在函数组件中,我们不需要去继承一个 class 对象,不需要去记忆那些生命周期,不需要固定的把数据定义在 state 中。函数作为 js 中的一等公民,函数式编程方式可以让我们更灵活的去组织代码。

  2. 类组件存在自身缺陷: 最常见的就是,在 React 中,如果我们定义一个方法,我们必须使用 bind 或者箭头函数去约束这个方法的 this 作用域。但是在函数组件中,可以通过闭包的方式,在一次渲染中,组件的 props 和 state 是保持不变的,而且传递的方法本身就是已经被约束的了。

  3. 逻辑是分散的,难以复用: 在 React 里,数据是定义在 state 中的,然后需要编写相关的事件方法,再在生命周期里进行逻辑的初始化,组件更新的时候处理,最后在模板里写 JSX,如果我们去维护这些代码,为了去查看这些逻辑,要上下翻,找出各自的数据、方法、生命周期和模板。

  4. Hooks 更贴合 React 的基本理念: React 的核心理念之一,相同的参数输入应该产生相同的输出。简单说,它应当是一个简单的纯函数。