Hooks认识(上)

500 阅读5分钟

一、前言

  • React Hooks 是从 v16.8 引入的又一开创性的新特性。第一次了解这项特性的时候,真的有一种豁然开朗,发现新大陆的感觉。我深深的为 React 团队天马行空的创造力和精益求精的钻研精神所折服。本文除了介绍具体的用法外,还会分析背后的逻辑和使用时候的注意事项,力求做到知其然也知其所以然。

二、Hooks 的由来

Hooks的出现是为了解决 React 长久以来存在的一些问题:

  • 带组件状态的逻辑很难重用

为了解决这个问题,需要引入render props或higher-order components这样的设计模式,如react-redux提供的connect方法。这种方案不够直观,而且需要改变组件的层级结构,极端情况下会有多个wrapper嵌套调用的情况。

Hooks可以在不改变组件层级关系的前提下,方便的重用带状态的逻辑。

  • 复杂组件难于理解

大量的业务逻辑需要放在componentDidMount和componentDidUpdate等生命周期函数中,而且往往一个生命周期函数中会包含多个不相关的业务逻辑,如日志记录和数据请求会同时放在componentDidMount中。另一方面,相关的业务逻辑也有可能会放在不同的生命周期函数中,如组件挂载的时候订阅事件,卸载的时候取消订阅,就需要同时在componentDidMount和componentWillUnmount中写相关逻辑。

Hooks可以封装相关联的业务逻辑,让代码结构更加清晰。

  • 难于理解的 Class 组件

JS 中的this关键字让不少人吃过苦头,它的取值与其它面向对象语言都不一样,是在运行时决定的。为了解决这一痛点,才会有剪头函数的this绑定特性。另外 React 中还有Class Component和Function Component的概念,什么时候应该用什么组件也是一件纠结的事情。代码优化方面,对Class Component进行预编译和压缩会比普通函数困难得多,而且还容易出问题。

Hooks可以在不引入 Class 的前提下,使用 React 的各种特性。

三、什么是 Hooks

  • 上面是官方解释。从中可以看出 Hooks 是函数,有多个种类,每个 Hook 都为Function Component提供使用 React 状态和生命周期特性的通道。Hooks 不能在Class Component中使用

四、什么是 Hooks

  • 先来看一个传统的Class Component:
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

使用 State Hook 来改写会是这个样子:

import React, { useState } from 'react';

function Example() {
  // 定义一个 State 变量,变量值可以通过 setCount 来改变
  const [count, setCount] = useState(0);

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

可以看到useState的入参只有一个,就是 state 的初始值。这个初始值可以是一个数字、字符串或对象,甚至可以是一个函数。当入参是一个函数的时候,这个函数只会在这个组件初始渲染的时候执行:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

useState的返回值是一个数组,数组的第一个元素是 state 当前的值,第二个元素是改变 state 的方法。这两个变量的命名不需要遵守什么约定,可以自由发挥。要注意的是如果 state 是一个对象,setState 的时候不会像Class Component的 setState 那样自动合并对象。要达到这种效果,可以这么做:

setState(prevState => {
  // Object.assign 也可以
  return {...prevState, ...updatedValues};
});

从上面的代码可以看出,setState 的参数除了数字、字符串或对象,还可以是函数。当需要根据之前的状态来计算出当前状态值的时候,就需要传入函数了,这跟Class Component的 setState 有点像。

另外一个跟Class Component的 setState 很像的一点是,当新传入的值跟之前的值一样时(使用Object.is比较),不会触发更新。

五、Effect Hook

解释这个 Hook 之前先理解下什么是副作用。网络请求、订阅某个模块或者 DOM 操作都是副作用的例子,Effect Hook 是专门用来处理副作用的。正常情况下,在Function Component的函数体中,是不建议写副作用代码的,否则容易出 bug。

下面的Class Component例子中,副作用代码写在了componentDidMount和componentDidUpdate中:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

可以看到componentDidMount和componentDidUpdate中的代码是一样的。而使用 Effect Hook 来改写就不会有这个问题:

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

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

useEffect会在每次 DOM 渲染后执行,不会阻塞页面渲染。它同时具备componentDidMount、componentDidUpdate和componentWillUnmount三个生命周期函数的执行时机。

此外还有一些副作用需要组件卸载的时候做一些额外的清理工作的,例如订阅某个功能:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

在componentDidMount订阅后,需要在componentWillUnmount取消订阅。使用 Effect Hook 来改写会是这个样子:

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    
    // 返回一个函数来进行额外的清理工作:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

当useEffect的返回值是一个函数的时候,React 会在下一次执行这个副作用之前执行一遍清理工作,整个组件的生命周期流程可以这么理解:

组件挂载 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 组件卸载

上文提到useEffect会在每次渲染后执行,但有的情况下我们希望只有在 state 或 props 改变的情况下才执行。如果是Class Component,我们会这么做

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

使用 Hook 的时候,我们只需要传入第二个参数:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 只有在 count 改变的时候才执行 Effect

第二个参数是一个数组,可以传多个值,一般会将 Effect 用到的所有 props 和 state 都传进去。

当副作用只需要在组件挂载的时候和卸载的时候执行,第二个参数可以传一个空数组[],实现的效果有点类似componentDidMount和componentWillUnmount的组合

作为学习传播 讲解透彻:segmentfault.com/a/119000001…