Why React Hooks

4,281

一、前言

1.1 为何要优先使用 SFC(Stateless Function Component)

Stateless Function Component:

const App = (props) => (
  <div>Hello, {props.name}</div>
)

Class Component:

class App extends React.Component {
  render() {
    return (
      <div>Hello, {this.props.name}</div>
    )
  }
}

上面是两个最简单的 function component 和 class component 的对比,首先从行数上来看,3 << 7。

再看 babel 编译成 es2015 后的代码:

Function Component:

"use strict";

var App = function App(props) {
  return React.createElement("div", null, "Hello, ", props.name);
};

Class Component:

去除了一堆 babel helper 函数

"use strict";

var App =
/*#__PURE__*/
function (_React$Component) {
  _inherits(App, _React$Component);

  function App() {
    _classCallCheck(this, App);

    return _possibleConstructorReturn(this, _getPrototypeOf(App).apply(this, arguments));
  }

  _createClass(App, [{
    key: "render",
    value: function render() {
      return React.createElement("div", null, "Hello, ", this.props.name);
    }
  }]);

  return App;
}(React.Component);

Function Component 仅仅是一个普通的 JS 函数,Class Component 因为 ES2015 不支持 class 的原因,会编译出很多和 class 相关的代码。

同时因为 Function Component 的特殊性,React 底层或许可以做 更多的性能优化

总的来说,以下点:

  • 更容易阅读和单测
  • 写更少的代码,编译出更加精简的代码
  • React Team 可正对这种组件做更加的性能优化

1.2 恼人的 bind(this)

在 React Class Component 中,我们一定写过很多这样的代码

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
    	name: 'rccoder',
    	age: 22
    },
    this.updateName = this.updateName.bind(this);
    this.updateAge = this.updateAge.bind(this);
  }
  
  render() {
    <div onClick={this.updateName}	
</div>
  }
}

当然这个错不在 React,而在于 JavaScript 的 this 指向问题,简单看这样的代码:

class Animate {
  constructor(name) {
    this.name = name;
  }
  getName() {
    console.log(this);
    console.log(this.name)
  }
}

const T = new Animate('cat');
T.getName();  // `this` is Animate Instance called Cat

var P = T.getName;
P(); // `this` is undefined

这个例子和上面的 React 如出一辙,在没有 bind 的情况下这样写会 导致了 this 是 global this,即 undefined。

解决它比较好的办法就是在 contructor 里面 bind this。

在新版本的 ES 中,有 Public Class Fields Syntax 可以解决这个问题,即:

class Animate {
  constructor(name) {
    this.name = name;
  }
  getName = () => {
    console.log(this);
    console.log(this.name)
  }
}

const T = new Animate('cat');
T.getName();  // `this` is Animate Instance called Cat

var P = T.getName;
P(); // `this` is Animate Instance called Cat

箭头函数不会创建自己的 this,只会依照词法从自己的作用域链的上一层继承 this,从而会让这里的 this 指向恰好和我们要的一致。

即使 public class fileds syntax 借助 arrow function 可以勉强解决这种问题,但 this 指向的问题依旧让人 “恐慌”。

1.2 被废弃的几个生命周围

React 有非常多的生命周期,在 React 的版本更新中,有新的生命周期进来,也有一些生命周期官方已经渐渐开始认为是 UNSAFE。目前被标识为 UNSAFE 的有:

  • componentWillMount
  • componentWillRecieveProps
  • componentWillUpdate

新引入了

  • getDerivedStateFromProps
  • getSnapshotBeforeUpdate

getDerivedStateFromPropsgetSnapshotBeforeUpdate 均是返回一个处理后的对象给 componentDidUpdate,所有需要操作的逻辑都放在 componentDidUpdate 里面。

原则上:

  • getDerivedStateFromProps + componentDidUpdate 可以替代 componentWillReceiveProps 的所有正常功能;
  • getSnapshotBeforeUpdate + componentDidUpdate 可以替代 componentWillUpdate 的所有功能。

具体的 原因迁移指南 可以参考 React 的官方博客:Update on Async Rendering,有比较详实的手把手指南。

最后,你应该依旧是同样的感觉,Class Component 有如此多的生命周期,显得是如此的复杂。


说了上面一堆看似和题目无关的话题,其实就是为了让你觉得 “Function Component 大法好”,然后再开心的看下文。


二、什么是 React Hooks

终于来到了和 Hooks 相关的部分,首先我们看下 什么是 Hooks

2.1 什么是 Hooks

首先来看下我们熟知的 WebHook:

Webhooks allow you to build or set up GitHub Apps which subscribe to certain events on GitHub.com. When one of those events is triggered, we'll send a HTTP POST payload to the webhook's configured URL. Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. You're only limited by your imagination.

—— GitHub WebHook 介绍

核心是:When one of those events is triggered, we'll send a HTTP POST payload to the webhook's configured URL

2.2 什么是 React Hooks

那 React Hooks 又是什么呢?

Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.

看上去和 WebHook 区别不小,实际上也不小,怎么解释呢?

React Hook 在 Function Component 上开了一些 Hook,通过内置的一些 Hook 可以让 Function Component 拥有自己的 state 和 生命周期,同时可以在 Hook 中操作它。此外通过组合内置的 Hook + 自己的业务逻辑 就可以生成新的 Custom Hook,以方便优雅复用一些业务逻辑。

三、Hooks 之前的一些问题

3.1 不同组件间逻辑复用问题

很多时候,视图表现不同的组件都期望拥有一部分相同的逻辑,比如:强制登录、注入一些值等。这个时候我们经常会使用 HOC、renderProps 来包装这层逻辑,总的来说都会加入一层 wrapper,使组件的层级发生了变化,随着业务逻辑复杂度的增加,都会产生 wrapper 地狱的问题。

3.2 复杂组件阅读困难问题

随着业务逻辑复杂度的增加,我们的组件经常会在一个生命周期中干多见事,比如:在 componentDidMount 中请求数据、发送埋点等。总之就是在一个生命周期中会写入多个完全不相关的代码,进而造成各种成本的隐形增加。

假如因为这些问题再把组件继续抽象,不仅工作量比较繁杂,同时也会遇到 wrapper 地狱和调试阅读更加困难的问题。

3.3 class component 遇到的一些问题

从人的角度上讲,class component 需要关心 this 指向等,大多经常在使用 function component 还是 class component 上感到困惑;从机器的角度上讲,class component 编译体积过大,热重载不稳定

四、Hooks 有哪些功能

上述提到的三个问题,Hooks 某种意义上都做了自己的解答,那是如何解答的呢?

目前 Hooks 有: State Hook、Effect Hook、Context Hook、以及 Custom Hook。

4.1 State Hook

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useState 是 State Hook 的 API。入参是 initialState,返回一个 数组,第一值是 state,第二个值是改变 state 的函数。

如果 initialState 的提高需要消耗大量的计算力,同时不期望这些计算阻塞后面要干的事情的话。initialState 可以是个函数,会在 render 前调用达到 Lazy Calc 的效果。

useState(() => {
  // ... Calc
  return initialState;
})

同时为了避免不必要的性能开销,在设置 State 的时候如果两个值是相等的,则也不会触发 rerender。(判断两个值相等使用的是 Object.is

4.2 Effect Hook

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
    return () => {
      ... // Similar to componentWillUnMount。Named as clear up effect
    }

  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect 相当于 class Component 中 componentDidMountcomponentDidUpdatecomponentWillUnmount 三个生命周期的大综合,在组件挂载、更新、卸载的时候都会执行 effect 里面的函数。用 “after render” 理解是最好的。

值的注意的是,Effect Hook 中的内容不会像 componentDidMountcomponentDidUpdate 一样阻塞渲染。如果不期望这种表现,可是用来 API 表现一样的 useLayoutEffect。(常见的计算器快速增加问题)

在一个 Function Component 里,和 useState 一样可以可以使用多次 useEffect,这样在组织业务逻辑的时候,就可以按照业务逻辑去划分代码片段了(而不是 Class Component 中只能按照生命周期去划分代码片段)。

Effect Hook 的执行实际是 “after render”,为了避免每个 render 都执行所有的 Effect Hook,useEffect 提供了第二个入参(是个数组),组件 rerender 后数组中的值发生了变化后才会执行该 Effect Hook,如果传的是个空数组,则只会在组件第一次 Mount 后和 Unmount 前调用。这层优化理论上是可以在编译时去做的,React Team 后期可能会做掉这层。

4.3 Custom Hook

import React, { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

useFriendStatus 就是一个典型的 Custom Hook,他利用 useState 和 useEffect 封装了 订阅朋友列表,设置朋友状态,组件卸载时取消订阅 的系列操作,最后返回一个表示是否在线的 state;在使用的时候,就可以像内置 Hook 一样使用,享用封装的系列逻辑。

在 Hooks 内部,即使在一个 Function Component 中,每个 Hooks 调用都有自己的隔离空间,能保证不同的调用之间互不干扰。

useFriendStatususe 开头是 React Hooks 的约定,这样的话方便标识他是一个 Hook,同时 eslint 插件也会去识别这种写法,以防产生不必要的麻烦。

同时如果 useXXXinitialState 是个变量,然后这个变量发生变化的时候,该 Hook 会自动取消之前的消息订阅,重新进行 Hooks 的挂载。也就是说,在上面的例子中,如果 props.friend.id 发生变化,useFriendStatus 这个 Hooks 会重新挂载,进而 online 状态也会正常显示。

4.4 Context Hook

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
  // ...
}

useContext 的入参是某个 Provider 提供的 context,如果 context 发生变化的话,返回值也会立即发生变化。

4.5 Reduce Hook

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter({initialState}) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

如果 State 的变化有比较复杂的状态流转,可以使用 useReducer 让他更加 Redux 化,以便让这层逻辑更加清晰。同时 Reduce Hook 也提供 Lazy Calc 的功能,有需求的时候可以去使用它。

除此之外,内置的 Hooks 还有 useCallbackuseMemouseRefuseImperativeHandleuseLayoutEffectuseDebugValue。可以去 这里 了解更多的用法。

五、例子对比

该例子是 Dan 在 React Conf 上的例子,算是非常有代表性的了:

视频地址:React Today and Tomorrow and 90% Cleaner React With Hooks

同时这里有一些 Hooks,看看实现和所解决的问题能更深的去了解 Hooks 的魅力:react hooks codesandbox

六、到底什么时候会用到 React Hook

  • 更喜欢写 Function Component
  • 想让 Function Component 用有 state 和生命周期
  • 厌恶 class component 中按照 生命周期 去拆分代码区块(而不是按照 业务逻辑 拆分代码区块)
  • 想提炼不同 UI 组件的共有业务逻辑又不想因为 HOC 或者 renderProps 陷入 wrapper 地狱

七、引入的问题

7.1 奇怪的 useEffect

useEffect 可以覆盖 componentDidMount,ComponentDidUpdate 和 componentWillUnmount 三个生命周期的操作,但从某种意义上说,实现 componentWillUnMount 的操作是有点让人窒息的,如果是一个没看过文档的人,绝对不知道要这么操作。

useEffect(() => {
  // cDM or cDU
  return () => {
    // cWU
  }
})

7.2 底层实现导致逻辑上的问题

React Hook 在内部实现上是使用 xxx,因为使用 React Hook 有两个限制条件

  • 只能在顶层调用 Hooks,不能在循环、判断条件、嵌套的函数里面调用 Hooks。 这样才能保证每次都是按照顺序调用 Hooks,否则从 tuple 里拿到的值就不一定是你要的那个 tuple 里的值了,Effect 也是同样的道理。具体原因大概是内部维护了一个队列来表示 Hooks 执行的的顺序,而顺序正式定义的时候定义的,如果不再最顶层,可能会导致执行时 Hooks 的顺序和定时时的不一致,从而产生问题,更加详细的可以参考 React 官方的解释:explanation
  • 只允许 Function Component 和 Custom Hooks 调用 React Hook,普通函数不允许调用 Hooks。

React Team 为此增加了 eslint 插件:eslint-plugin-react-hooks,算是变通的去解决问题吧。

八、常见疑问

8.1 为什么 useState 返回的是个数组

这里的数组,从某种意义上说叫 tuple 会更加容器理解一些,可惜 JavaScript 中目前还没这种概念。

同时,如果返回的是个 Object 又会怎么样呢?

let { state: name, setState: setName } = useState('Name');
let { state: surname, setState: setSurname } = useState('Surname');

看起来好像是更糟糕的

8.2 性能问题

shouldComponentUpdate 使用 useMemo 即可,参考:How to memoize calculations?

同时 Hooks 的写法避免的 Class Component 创建时大量创建示例和绑定事件处理的开销,外加用 Hooks 的写法可以避免 HOC 或者 renderProps 产生深层嵌套,对 React 来说处理起来会更加轻松。

8.3 能覆盖 Class Component 的所有生命周期么?

getSnapshotBeforeUpdatecomponentDidCatch 目前覆盖不到

8.4 prevState 如何获取

借助 useRef 获取

如果有其他问题,不妨去 React Hooks FQA 看看,大概率里面涵盖了你想知道的问题。

九、参考资料

原文地址:github.com/rccoder/blo… (去这交流更方便哦~)