React Hooks 解析(上):基础

一、前言

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

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

二、Hooks 的由来

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

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

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

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

  • 复杂组件难于理解

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

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

  • 难于理解的 Class 组件

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

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

三、什么是 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 供我们使用,下面我们来详细了解一下。

四、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比较),不会触发更新。

五、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的组合。

六、总结

本文介绍了在 React 之前版本中存在的一些问题,然后引入 Hooks 的解决方案,并详细介绍了 2 个最重要的 Hooks:useStateuseEffect的用法及注意事项。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,192评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,858评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,517评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,148评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,162评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,905评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,537评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,439评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,956评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,083评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,218评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,899评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,565评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,093评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,201评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,539评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,215评论 2 358

推荐阅读更多精彩内容