# 1. 引言

为什么要了解 Function 写法的组件呢？因为它正在变得越来越重要。

那么 React 中 **Function Component 与 Class Component 有何不同？**

[how-are-function-components-different-from-classes](https://overreacted.io/how-are-function-components-different-from-classes/) 这篇文章带来了一个独特的视角。

> 顺带一提，以后会用 Function Component 代替 Stateless Component 的说法，原因是：自从 Hooks 出现，函数式组件功能在不断丰富，函数式组件不再需要强调其无状态特性，因此叫 Function Component 更为恰当。

# 2. 概述

原文事先申明：并没有对 Function 与 Classes 进行优劣对比，而仅仅进行特性对比，所以不接受任何吐槽。

> 这两种写法没有好坏之分，性能差距也几乎可以忽略，而且 React 会长期支持这两种写法。

## Capture props

对比下面两段代码。

**Class Component:**

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

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

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

**Function Component:**

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

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

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

（[在线 Demo](https://codesandbox.io/s/pjqnl16lm7)）

这两个组件都描述了同一个逻辑：点击按钮 3 秒后 `alert` 父级传入的用户名。

如下父级组件的调用方式：

```jsx
<ProfilePageFunction user={this.state.user} />
<ProfilePageClass user={this.state.user} />
```

那么当点击按钮后的 3 秒内，父级修改了 `this.state.user`，弹出的用户名是修改前的还是修改后的呢？

**Class Component 展示的是修改后的值：**

<img width=500 src="https://img.alicdn.com/tfs/TB1N8ObMwTqK1RjSZPhXXXfOFXa-950-351.gif">

**Function Component 展示的是修改前的值：**

<img width=500 src="https://img.alicdn.com/tfs/TB1kFCmMzTpK1RjSZKPXXa3UpXa-901-293.gif">

那么 React 文档中描述的 `props` 不是不可变（Immutable） 数据吗？为啥在运行时还会发生变化呢？

原因在于，虽然 `props` 不可变，是 `this` 在 Class Component 中是可变的，因此 `this.props` 的调用会导致每次都访问最新的 `props`。

而 Function Component 不存在 `this.props` 的语法，因此 `props` 总是不可变的。

为了便于理解，笔者补充一些代码注解：

**Function Component:**

```jsx
function ProfilePage(props) {
  setTimeout(() => {
    // 就算父组件 reRender，这里拿到的 props 也是初始的
    console.log(props);
  }, 3000);
}
```

**Class Component:**

```jsx
class ProfilePage extends React.Component {
  render() {
    setTimeout(() => {
      // 如果父组件 reRender，this.props 拿到的永远是最新的。
      // 并不是 props 变了，而是 this.props 指向了新的 props，旧的 props 找不到了
      console.log(this.props);
    }, 3000);
  }
}
```

如果希望在 Class Component 捕获瞬时 Props，可以： `const props = this.props;`，但这样的代码很蹩脚，所以如果希望拿到稳定的 `props`，使用 Function Component 是更好的选择。

## Hooks 也具有 capture value 特性

看下面的代码：

```jsx
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](https://codesandbox.io/s/93m5mz9w24)）

在点击 `Send` 按钮后，再次修改输入框的值，3 秒后的输出依然是 **点击前输入框的值**。这说明 Hooks 同样具有 capture value 的特性。

利用 `useRef` 可以规避 capture value 特性：

```jsx
function MessageThread() {
  const latestMessage = useRef("");

  const showMessage = () => {
    alert("You said: " + latestMessage.current);
  };

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

  const handleMessageChange = e => {
    latestMessage.current = e.target.value;
  };
}
```

只要将赋值与取值的对象变成 `useRef`，而不是 `useState`，就可以躲过 capture value 特性，在 3 秒后得到最新的值。

这说明了利用 Function Component + Hooks 可以实现 Class Component 做不到的 capture props、capture value，而且 React 官方也推荐 [新的代码使用 Hooks 编写](https://reactjs.org/docs/hooks-faq.html#do-i-need-to-rewrite-all-my-class-components)。

# 3. 精读

原文 [how-are-function-components-different-from-classes](https://overreacted.io/how-are-function-components-different-from-classes/) 从一个侧面讲述了 Function Component 与 Class Component 的不同点，之所以将 Function Component 与 Class Component 相提并论，几乎都要归功于 Hooks API 的出现，有了 Hooks，Function Component 的能力才得以向 Class Component 看齐。

关于 React Hooks，之前的两篇精读分别有过介绍：

- [精读《React Hooks》](https://github.com/dt-fe/weekly/blob/master/79.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E3%80%8B.md)
- [精读《怎么用 React Hooks 造轮子》](https://github.com/dt-fe/weekly/blob/master/80.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%80%8E%E4%B9%88%E7%94%A8%20React%20Hooks%20%E9%80%A0%E8%BD%AE%E5%AD%90%E3%80%8B.md)

但是，虽然 Hook 已经发布了稳定版本，但周边生态跟进还需要时间（比如 `useRouter`）、最佳实践整理还需要时间，因此不建议重构老代码。

为了更好的使用 Function Component，建议时常与 Class Component 的功能做对比，方便理解和记忆。

下面整理一些常见的 Function Component 问题：

> 非常建议完整阅读 [React Hooks FAQ](https://reactjs.org/docs/hooks-faq.html#do-i-need-to-rewrite-all-my-class-components)。

## 怎么替代 shouldComponentUpdate

说实话，Function Component 替代 `shouldComponentUpdate` 的方案并没有 Class Component 优雅，代码是这样的：

```jsx
const Button = React.memo(props => {
  // your component
});
```

或者在父级就直接生成一个自带 `memo` 的子元素：

```jsx
function Parent({ a, b }) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  );
}
```

相比之下，Class Component 的写法通常是：

```jsx
class Button extends React.PureComponent {}
```

这样就自带了 `shallowEqual` 的 `shouldComponentUpdate`。

## 怎么替代 componentDidUpdate

由于 `useEffect` 每次 Render 都会执行，因此需要模拟一个 `useUpdate` 函数：

```jsx
const mounting = useRef(true);
useEffect(() => {
  if (mounting.current) {
    mounting.current = false;
  } else {
    fn();
  }
});
```

更多可以查看 [精读《怎么用 React Hooks 造轮子》](https://github.com/dt-fe/weekly/blob/master/80.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%80%8E%E4%B9%88%E7%94%A8%20React%20Hooks%20%E9%80%A0%E8%BD%AE%E5%AD%90%E3%80%8B.md#componentdidupdate)

## 怎么替代 forceUpdate

React 官方文档提供了一种方案：

```jsx
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
  forceUpdate();
}
```

每次执行 `dispatch` 时，只要 `state` 变化就会触发组件更新。当然 `useState` 也同样可以模拟：

```jsx
const useUpdate = () => useState(0)[1];
```

我们知道 `useState` 下标为 1 的项是用来更新数据的，而且就算数据没有变化，调用了也会刷新组件，所以我们可以把返回一个没有修改数值的 `setValue`，这样它的功能就仅剩下刷新组件了。

更多可以查看 [精读《怎么用 React Hooks 造轮子》](https://github.com/dt-fe/weekly/blob/master/80.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%80%8E%E4%B9%88%E7%94%A8%20React%20Hooks%20%E9%80%A0%E8%BD%AE%E5%AD%90%E3%80%8B.md#force-update)

## state 拆分过多

`useState` 目前的一种实践，是将变量名打平，而非像 Class Component 一样写在一个 State 对象里：

```jsx
class ClassComponent extends React.PureComponent {
  state = {
    left: 0,
    top: 0,
    width: 100,
    height: 100
  };
}

// VS

function FunctionComponent {
  const [left,setLeft] = useState(0)
  const [top,setTop] = useState(0)
  const [width,setWidth] = useState(100)
  const [height,setHeight] = useState(100)
}
```

实际上在 Function Component 中也可以聚合管理 State：

```jsx
function FunctionComponent() {
  const [state, setState] = useState({
    left: 0,
    top: 0,
    width: 100,
    height: 100
  });
}
```

只是更新的时候，不再会自动 merge，而需要使用 `...state` 语法：

```jsx
setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
```

可以看到，更少的黑魔法，更可预期的结果。

## 获取上一个 props

虽然不怎么常用，但是毕竟 Class Component 可以通过 `componentWillReceiveProps` 拿到 `previousProps` 与 `nextProps`，对于 Function Component，最好通过自定义 Hooks 方式拿到上一个状态：

```jsx
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return (
    <h1>
      Now: {count}, before: {prevCount}
    </h1>
  );
}

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}
```

通过 `useEffect` 在组件渲染完毕后再执行的特性，再利用 `useRef` 的可变特性，让 `usePrevious` 的返回值是 “上一次” Render 时的。

可见，合理运用 `useEffect` `useRef`，可以做许多事情，而且封装成 CustomHook 后使用起来仍然很方便。

> 未来 `usePrevious` 可能成为官方 Hooks 之一。

## 性能注意事项

`useState` 函数的参数虽然是初始值，但由于整个函数都是 Render，因此每次初始化都会被调用，如果初始值计算非常消耗时间，建议使用函数传入，这样只会执行一次：

```jsx
function FunctionComponent(props) {
  const [rows, setRows] = useState(() => createRows(props.count));
}
```

> `useRef` 不支持这种特性，需要[写一些冗余的函判定是否进行过初始化](https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily)。

掌握了这些，Function Component 使用起来与 Class Component 就几乎没有差别了！

# 4. 总结

Function Component 功能已经可以与 Class Component 媲美了，但目前最佳实践比较零散，官方文档推荐的一些解决思路甚至不比社区第三方库的更好，可以预料到，Class Component 的功能会被五花八门的实现出来，那些没有被收纳进官方的 Hooks 乍看上去可能会眼花缭乱。

总之选择了 Function Component 就同时选择了函数式的好与坏。**好处是功能强大，几乎可以模拟出任何想要的功能**，**坏处是由于可以灵活组合，如果自定义 Hooks 命名和实现不够标准，函数与函数之间对接的沟通成本会更大。**

> 讨论地址是：[精读《Stateless VS Class 组件》 · Issue #137 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/137)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

**special Sponsors**

- [DevOps 全流程平台](https://e.coding.net/?utm_source=weekly)

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
