React Hooks 解析(上):基础

欢迎关注个人公众号睿Talk,获取我最新的文章:
clipboard.pngjavascript

1、前言

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

这个系列分上下两篇,这里是下篇的传送门:
React Hooks 解析(下):进阶react

2、Hooks 的由来

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

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

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

Hooks能够在不改变组件层级关系的前提下,方便的重用带状态的逻辑。设计模式

  • 复杂组件难于理解

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

Hooks能够封装相关联的业务逻辑,让代码结构更加清晰。网络

  • 难于理解的 Class 组件

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

Hooks能够在不引入 Class 的前提下,使用 React 的各类特性。函数

3、什么是 Hooks

Hooks are functions that let you “hook into” React state and lifecycle features from function components

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

React 提供了一些预约义好的 Hooks 供咱们使用,下面咱们来详细了解一下。

4、State Hook

先来看一个传统的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比较),不会触发更新。

5、Effect Hook

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

下面的Class Component例子中,反作用代码写在了componentDidMountcomponentDidUpdate中:

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

能够看到componentDidMountcomponentDidUpdate中的代码是同样的。而使用 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 渲染后执行,不会阻塞页面渲染。它同时具有componentDidMountcomponentDidUpdatecomponentWillUnmount三个生命周期函数的执行时机。

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

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 都传进去。

当反作用只须要在组件挂载的时候和卸载的时候执行,第二个参数能够传一个空数组[],实现的效果有点相似componentDidMountcomponentWillUnmount的组合。

6、总结

本文介绍了在 React 以前版本中存在的一些问题,而后引入 Hooks 的解决方案,并详细介绍了 2 个最重要的 Hooks:useStateuseEffect的用法及注意事项。原本想一篇写完全部相关的内容,但发现坑有点深,只能分两次填了:)