重学 React 之 Hook

·  阅读 564
重学 React 之 Hook

重学 React 之基础认知
重学 React 之三大属性
重学 React 之生命周期
重学 React 之路由

Hook 简介

什么是 React Hook?

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

我们知道在 React 中,函数组件中是不能使用 state的,不了解的可以翻我前几篇文章,都有说到哦。而 Hook 的出现,就可以让你在函数组件中使用 state 以及其他的 React 特性。

关于 Hook 出现的具体原因,可以参考官方说明:动机

再说 Hook 的本质,其实就是函数,它可以让你在函数组件里“钩入” React state 及生命周期等特性。Hook 不能在 class 组件中使用,如果你完全熟悉了 Hook 的使用,你可以仅用函数组件来编写 React 应用。

除了 React 内置的 Hook ,我们也可以使用一些第三方封装的 Hook 插件,甚至自己封装 Hook

需要注意 React 16.8.0 是第一个支持 Hook 的版本,本章 React 用例版本是 v17.0.2。如果你想测试效果,React 版本需要大于等于 v16.8.0

useState

useState 是 React 内置的一个 Hook,它允许你在 React 函数组件中添加 state 的 Hook,我们还是以传统的计数器的案例来介绍它,在这之前,我们先看一个对比的示例,就是用 class 方式来写一个计数器组件:

class Counter extends React.Component {
  state = {
    count: 0,
  };
  add = () => {
    this.setState({ count: this.state.count + 1 });
  };
  render() {
    return (
      <div>
        <p>count: {this.state.count}</p>
        <button onClick={this.add}>+1</button>
      </div>
    );
  }
}
复制代码

这段简单的代码就完成了一个计数器逻辑,但是如果使用函数组件来写就不行了,因为函数组件不能使用 state。不过现在可以借助 useState 钩子函数来帮我们在函数组件中完成同样的功能,使用示例:

// 需要引入 useState 钩子
import React, { useState } from "react";

function Counter() {
  // 此处声明 count,是 state 变量
  const [count, setCount] = useState(0);

  function add() {
    setCount(count + 1);
  }
  return (
    <div>
      <p>count: {count}</p>
      <button onClick={add}>+1</button>
    </div>
  );
}
复制代码

上面代码使用了 useState 来开发一个计数器功能,相比于 class 方式,我觉得更简洁优雅了。 useState 使用语法是:

const [xxx,setXxx] = useState(initValue)
复制代码

useState 方法传入 state 变量的初始值作为参数,同时返回包含两个元素的数组,数组第一项是内部当前状态值,第二项是更新状态值的函数。等号左边我们使用数组解构的方式,自定义两项值的名称,这两个名称都是任意的,没有特殊要求,不过一般都是取语义的名称。

以上例说明,setCount 是更新 count 状态的方法,它有两种使用方式,一种就是上面使用的那样直接传入新的状态值作为参数,另一种是传入一个函数作为参数,函数的参数是旧的状态值,返回值是新的状态值,如下:

// setCount(count + 1);

setCount((count) => count + 1);
复制代码

以上就是 useState 的使用,是不是很简单,如果定义多个 state 变量,那就多写一行而已:

const [count, setCount] = useState(0);
const [name, setName] = useState('Tom');
复制代码

useEffect

useEffect 也是 React 内置的 Hook,根据官方说明,useEffect 可以让你在函数组件中执行副作用操作,所谓“副作用”操作,其实就是在 React 渲染或更新 Dom 时执行一些额外的操作,比如 ajax 请求,设置订阅或者启动定时器之类的。一般这些副作用操作都是放在生命周期钩子中执行,但是函数组件中是没有生命周期的,而 useEffect 的作用就是模拟 class 组件中的生命周期。

useEffect 钩子传入一个函数作为参数,函数中就可以执行副作用操作,还是在上面计数器的代码例子上,我们引入 useEffect,执行打印 count 的副作用操作。代码如下:

// 引入 hook
import React, { useEffect, useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  
  // 使用 useEffect,执行打印 count 操作
  useEffect(() => {
      console.log(count);
  })

  function add() {
    setCount(count + 1);
  }
  return (
    <div>
      <p>count: {count}</p>
      <button onClick={add}>+1</button>
    </div>
  );
}
复制代码

浏览器执行上述代码时,在页面初始化时会打印 0,同时在进行计数操作时,会实时打印 count 的状态值。回想一下如果我们要在 class 组件中实现这样的效果要怎么做,其实就是分别在生命周期 componentDidMountcomponentDidUpdate 中执行打印 count 的操作就行。

由此可见,这里 useEffect 已经模拟出了两个生命周期的作用了。但是也有一个问题,在 class 组件中,我们可以自由控制生命周期的钩子数量,假如我不希望在页面更新时执行副作用操作,那去掉 componentDidUpdate 这部分代码即可,需要就再加上。而 useEffect 是默认两个周期阶段都执行了,有什么办法不让更新阶段执行操作吗?

答案是肯定的,useEffect 可以控制执行周期,而且我觉得这部分设计的很巧妙。如果你不希望在页面更新的时候执行副作用操作,那么就给 useEffect 传入第二个参数,传一个空数组 [] 即可,如下:

useEffect(() => {
    console.log(count);
}, []); // 这里传入第二个参数,为一个空数组
复制代码

此时刷新浏览器,会先打印一次 0,当你再进行计数操作时,不会再打印更新的值了,也就是不再执行更新阶段的作用。是不是很神奇,同时又觉得有点奇怪,为什么要传一个空数组呢?聪明的童鞋应该能想到一些了,如果数组里传入 count 变量会如何?当你把 count 放到数组中去时,页面更新时又会执行打印操作了。

  useEffect(() => {
    console.log(count);
  }, [count]); // 传入 count
复制代码

准确的说其实是当 count 改变时,更新操作又执行了。这个数组有点像监听器,当你传一个空数组时,就不执行更新阶段副作用操作,而当你传入具体的变量时,只有当传入的变量更新时,才执行更新阶段的操作。默认不传数组时,只要有任何的状态更新,都执行更新阶段的副作用操作。

还有一个生命周期阶段也很重要,那就是 componentWillUnmount,我们有时需要在组件卸载时清除掉一些东西,最常见例如销毁定时器。

useEffect 可以返回一个函数,这个函数的执行时机就是组件销毁前。为了演示,我改写上面定时器的例子,应用定时器将计数器改成每秒自动加 1,同时加入组件销毁按钮:

import React, { useEffect, useState } from "react";
import ReactDom  from 'react-dom';

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

  useEffect(() => {
    // 使用定时器更新 count,每秒 +1
    setInterval(() => setCount((count) => count + 1), 1000);
  }, []);
  
  // 定义组件销毁方法
  function onUnmount() {
    ReactDom.unmountComponentAtNode(document.getElementById("root"));
  }
  return (
    <div>
      <p>count: {count}</p>
      <button onClick={onUnmount}>销毁</button>
    </div>
  );
}
复制代码

例子中的代码已加入定时器,浏览器执行时能够每秒对 count 进行加一操作,如果此时点击销毁按钮,会发现浏览器有一个警告

image.png

这其实就是因为组件销毁了,但是定时器程序还在执行导致的,所以我们需要在组件卸载的时候也一并销毁定时器。我们可以在 useEffect 中返回一个函数,在这个函数中销毁定时器,改写如下:

useEffect(() => {
    // 将定时器存到一个变量
    const timer = setInterval(() => setCount((count) => count + 1), 1000);
    // 返回一个函数,此函数会在组件卸载之前调用
    return () => {
        // 清理定时器
        clearInterval(timer)
    }
}, []);
复制代码

这样我们就模拟出了 componentWillUnmount 生命周期了,是不是很巧妙。总结就是你可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 三个函数的组合。

React 中不仅仅只有这两个 Hook,更多的可以参考:Hook API 索引。这里篇幅有限就不一一介绍了,举一反三,基本上了解了上面两个 Hook,就能看懂别的 Hook

自定义 Hook

Hook 本质上就是 js 函数,我们完全可以自定义特定功能的 Hook。自定义 Hook 的一个必要条件就是名称必须 use 开头。我们可以使用已有的 Hook,再度封装一个新的 Hook,比如我们要定义一个 useMountHook,表示只在组件初始化时执行的 Hook,可以这样的封装:

import { useEffect } from "react";

const useMount = (fn) => {
  useEffect(() => {
    fn && fn()
  }, [])
}
复制代码

哈哈,有点过于简单了,不过这样一封装更语义话了一些啊,说的是一个思路。由于我本人也没有大量实践,就不再多赘述了。

总结

关于 Hook 就讲这么多了吧,毕竟工作中没用过 React,也讲不出太多花。也有一些第三方的 hooks 库可以使用,这里推荐一个阿里出品的 ahooks,我看了一下功能很齐全啊,感兴趣的可以看看。

本篇是重学 React 系列最后一篇文章了,基本上讲完了 React 入门的所有东西,除了这篇竟然只有四篇,不得不说 React 学起来可比 Vue 快多了,不过想要用的好,细节上东西的肯定还有很多需要自己实践出来,不是几篇文章能讲清的。我没有再写关于 Redux 的了,因为我懒,我的掘金第一篇文章就是写的 Redux,不想再写了,还有就是 Redux 本来就不是 React 内的东西,是一个非强制性的工具,真要用到时再看也无妨。

就这样吧,今年的最后一篇记录。忘记了今天竟然是圣诞节,哈哈。

如有错误,欢迎指出。

分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改