30分钟React Hooks

带你30分钟把React Hooks玩出花

这里先做一个知识共识,在笔记中出现的部分技术名称描述:

  • 函数式组件 => Function Component
  • 类组件 => Class Component
  • 工具函数 => Util Function
  • 钩子 => React Hook
  • 初始值 => initialValue

产生背景

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性;
React Hook 是一种特殊的函数,本质上是函数式组件(返回 Dom 或 Dom 及 State ),也可以仅仅是一个工具函数(传入配置项返回封装后的数据处理逻辑)。

React v16.7.0-alpha中第一次引入了 Hooks 的概念,在 v16.8.0 版本被正式发布。React Hooks 在React 中只是对 React Hook 的概念性的描述,在开发中我们用到的实际功能都应该叫做 React Hook

剖析React Hook

React Hooks 的出现使函数式组件变得焕然一新,其带来的最大的变化在于给予了函数式组件类似于类组件生命周期的概念,扩展和增强了函数式组件的应用范围。

目前函数式组件基本用于纯展示组件,一旦函数式组件耦合有业务逻辑,就需要通过 Props 的传递,通过子组件触发父组件方法的方式来实现业务逻辑的传递,Hooks 的出现使得函数组件也有了自己的状态与业务逻辑,简单逻辑在自己内部处理即可,不再需要通过 Props 的传递,使简单逻辑组件抽离更加方便,也使使用者无需关心组件内部的逻辑,只关心 Hooks 组件返回的结果即可。

在我看来(可能理解欠缺,纯属个人理解),Hooks 组件的目标并不是取代类组件,而是增加函数式组件的使用率,明确通用工具函数与业务工具函数的边界,鼓励开发者将业务通用的逻辑封装成 React Hooks 而不是工具函数。之所以把总结放在前面,是想让大家在看后面的内容时有一个整体的概念去引导大家去思考 React Hooks 具体给函数式组件带来了什么变化。

Hooks 知识

Hook 不会影响你对 React 概念的理解。 恰恰相反,Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。稍后我们将看到,Hook 还提供了一种更强大的方式来组合他们。

官方提供的钩子

Hook概览官方提供的钩子共分为两种,分为基本钩子以及拓展钩子

  • 基本钩子共有:useStateuseEffectuseContext
  • 额外的钩子有:useCallbackuseReduceruseMemouseRefuseLayoutEffectuseImperativeHandleuseDebugValue
不使用钩子的写法
  • useState
    该钩子用于创建一个新的状态,参数为一个固定的值或者一个有返回值的方法。钩子执行后的结果为一个数组,分别为生成的状态以及改变该状态的方法,通过解构赋值的方法拿到对应的值与方法。

使用方法如下:

import React, {useState} from 'react';
function App() {
 const [count,
   changeCount] = useState(0);

 return (
   <div className="App">
     {count
}
     < button onClick= { () => { changeCount(Math.ceil(Math.random() * 1000)); } }>改变数据</button>
   </div>
 );
}

export default App;
  • useEffect

Effect Hook 可以让你在函数组件中执行副作用操作

顾名思义,执行副作用钩子。主要用于以下两种情况:

  • 1、函数式组件中不存在传统类组件生命周期的概念,如果我们需要在一些特定的生命周期或者值变化后做一些操作的话,必须借助 useEffect 的一些特性去实现。
  • 2、useState 产生的 changeState 方法并没有提供类似于 setState 的第二个参数一样的功能,因此如果需要在 State 改变后执行一些方法,必须通过 useEffect 实现。

该钩子接受两个参数,第一个参数为副作用需要执行的回调,生成的回调方法可以返回一个函数(将在组件卸载时运行);第二个为该副作用监听的状态数组,当对应状态发生变动时会执行副作用,如果第二个参数为空,那么在每一个 State 变化时都会执行该副作用。
使用方法如下:

import React, { useState, useEffect } from "react";
import { message } from "antd";
import "antd/dist/antd.css";
function App() {
  const [count, changeCount] = useState(0);
  useEffect(() => {
    message.info(`count发生变动,最新值为${count}`);
  }, [count]);
  return (
    <div className="App">
      {count}
      <button
        onClick={() => {
          changeCount(Math.ceil(Math.random() * 1000));
        }}
      >
        改变数据
      </button>
    </div>
  );
}

export default App;

在这里插入图片描述

在上面代码中我们实现了在 useEffect 这个钩子适用情况中的第二种情况,那么如何使用该钩子才能实现类似于类组件中生命周期的功能呢?既然第一个参数是副作用执行的回调,那么实现我们所要功能的重点就应该在第二个参数上了。
componentDidMount && componentWillUnmout:这两个生命周期只在页面挂载/卸载后执行一次。前面讲过,所有的副作用在组件挂载完成后会执行一次 ,如果副作用存在返回函数,那么返回的函数将在卸载时运行。借助这样的特性,我们要做的就是让目标副作用在初始化执行一次后再也不会被调用,于是只要让与该副作用相关联的状态为空,不管其他状态如何变动,该副作用都不会再次执行,即实现了 componentDidMount 与 componentWillUnmout。

import React, { useState, useEffect } from "react";
import { message } from "antd";
import "antd/dist/antd.css";

interface IChild {
  visible: any;
}
const Child: React.FC<IChild> = (props) => {
  useEffect(() => {
    message.info("我只在页面挂载时打印");
    return () => {
      message.info("我只在页面卸载时打印");
    };
  }, []);
  return props.visible?"true":"false";
};
const App = () => {
  const [visible, changeVisible] = useState(true);
  return (
    <div>
      <span style={{ display: visible ? "block" : "none" }}>
        {visible && <Child visible={visible} />}
      </span>
      <button
        onClick={() => {
          changeVisible(!visible);
        }}
      >
        改变visible
      </button>
    </div>
  );
};

export default App;

componentDidUpdate:该生命周期在每次页面更新后都会被调用。那么按照之前的逻辑,就应该把所有的状态全部放在第二个状态中,但是这样的话新增/删除一个状态都需要改变第二参数。其实,如果第二个参数为空,那么在每一个 State 变化时都会执行该副作用,那么如果要实现 componentDidUpdate 就非常简单了。

在类组件中,如果在 componentDidMount 中多次调用 setState 设置一个值(当然不推荐这样做),并在成功的回调中打印该值,那么最后的结果很可能会打印很多个相同的最后一次设置的值。是因为类的 setState 是一个类异步的结果,他们会将所有变动的内容进行收集然后在合适的时间去统一赋值。
而在 useEffect 中,所有的变量的值都会保留在该副作用执行的时刻,类似于 for 循环中的 let 或者 闭包,所有的变量都维持在副作用执行时的状态,也有人称这个为 Capture Value。

  • useContext
    接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

别忘记 useContext 的参数必须是 context 对象本身:

  • 正确: useContext(MyContext)
  • 错误: useContext(MyContext.Consumer)
  • 错误: useContext(MyContext.Provider)

调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。

如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。
useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

import React, { useState, useContext } from "react";
import { message } from "antd";
import "antd/dist/antd.css";

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee",
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222",
  },
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props: any) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      样式来源于主题上下文!
    </button>
  );
}

export default App;

在这里插入图片描述

  • useCallback
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

返回一个 memoized 回调函数。

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

const [count1, changeCount1] = useState(0);
const [count2, changeCount2] = useState(10);

const calculateCount = useCallback(() => {
  if (count1 && count2) {
    return count1 * count2;
  }
  return count1 + count2;
}, [count1, count2])

useEffect(() => {
    const result = calculateCount(count, count2);
    message.info(`执行副作用,最新值为${result}`);
}, [calculateCount])

在上面的例子中通过 useCallback 的使用生成了一个回调,useCallback 的使用方法和 useEffect 一致,第一个参数为生成的回调方法,第二个参数为该方法关联的状态,任一状态发生变动都会重新生成新的回调。
通过上面代码的使用,我们将 count1 / count2 的值与一个叫做 calculateCount 的方法关联了起来,如果组件的副作用中用到计算 count1 和 count2 的值的地方,直接调用该方法即可。
其中和直接使用 useEffect 不同的地方在于使用 useCallback 生成计算的回调后,在使用该回调的副作用中,第二个参数应该是生成的回调。其实这个问题是很好理解的,我们使用 useCallback 生成了一个与 count1 / count2 相关联的回调方法,那么当关联的状态发生变化时会重新生成新的回调,副作用监听到了回调的变化就会去重新执行副作用,此时 useCallback 和 useEffect 是按顺序执行的, 这样就实现了副作用逻辑的抽离。

  • useRef
    useRef 接受一个参数,为 ref 的初始值。类似于类组件中的 createRef 方法 ,该钩子会返回一个对象,对象中的 current 字段为我们 指向的实例 / 保存的变量,可以实现获得目标节点实例或保存状态的功能。
    useRef 保存的变量不会随着每次数据的变化重新生成,而是保持在我们最后一次赋值时的状态,依靠这种特性,再配合 useCabllback 和 useEffect 我们可以实现 preProps/preState 的功能。
import React, { useState, useRef, useEffect } from "react";
import { message } from "antd";
import "antd/dist/antd.css";

function App() {
  const [count, changeCount] = useState(0);
  const [count1, changeCount1] = useState(0);
  // 创建初始值为空对象的prestate
  const preState = useRef({});
  // 依赖preState进行判断时可以先判断,最后保存最新的state数据
  useEffect(() => {
    preState.current = {
      count,
      count1,
    };
  });
  return (
    <div>
      <p>
        {count}:{count1}
      </p>
      <button onClick={() => changeCount(count + 1)}>按钮1</button>

      <button onClick={() => changeCount1(count1 + 1)}>按钮2</button>
    </div>
  );
}

export default App;

其他的几个都很简单,官网上也做了详细的讲解,这里不再做研究,下面我们尝试做自己的Hook

自己封装一个Hook

最基本的钩子也就是返回包含了更多逻辑的 State 以及改变 State 方法的钩子。拿计数器来说,其最基本的就是返回当前的数字以及减少/增加/重置等功能,明确完功能后可以开始动手做了

import React, { useState } from "react";

function useAutoHook(value: any) {
  const [count, changeCount] = useState(value);

  const decrease = () => {
    changeCount(count - 1);
  };

  const increase = () => {
    changeCount(count + 1);
  };

  const reset = () => {
    changeCount(0);
  };

  return [count, { decrease, increase, reset }];
}
function App() {
  const [count, controlCount] = useAutoHook(10);
  return (
    <div>
      {count}
      <button onClick={controlCount.increase}>新增</button>
      <button onClick={controlCount.decrease}>减少</button>
      <button onClick={controlCount.reset}>重置</button>
    </div>
  );
}

export default App;

在上面的例子中,我们将在 useCounter 这个钩子中创建了一个关联了 initialValue 的状态,并创建减少/增加/重置的方法,最终将其通过 return 返回出去。这样在其他组件需要用到该功能的地方,通过调用该方法拿到其返回值,即可实现对 useCounter 组件封装逻辑的复用。

封装一个返回DOM的Hook

返回 DOM 其实和最基本的 Hook 逻辑是相同的,只是在返回的数据内容上有一些差异,具体还是看代码,以一个 Modal 框为例。

import React, { useState } from "react";
import { Modal } from "antd";

function useModal() {
  const [visible, changeVisible] = useState(false);

  const toggleModalVisible = () => {
    changeVisible(!visible);
  };

  return [
    <Modal
      visible={visible}
      onOk={toggleModalVisible}
      onCancel={toggleModalVisible}
    >
      弹窗内容
    </Modal>,
    toggleModalVisible,
  ];
}

export default function HookDemo() {
  const [modal, toggleModal] = useModal();
  return (
    <div>
      {modal}
      <button onClick={toggleModal}>打开弹窗</button>
    </div>
  );
}

总结

钩子用法作用
useStateconst [state, changeState] = useState(initialValue)用于生成状态以及改变状态的方法
useEffectuseEffect(fn, […relativeState])用于生成与状态绑定的副作用
useCallbackuseCallback(fn, […relativeState])用于生成与状态绑定的回调函数
useMemouseMemo(fn, […relativeState])用于生成与状态绑定的组件/计算结果
useRefconst newRef = useRef(initialValue)用于 获取节点实例 / 数据保存

在官方提供的 Hook 中,除了基本的 useState 与 useRef 外,其他钩子都存在第二个参数,第一个方法的执行与第二个参数相互关联。于是我们可以得出一个结论,在使用了 Hook 的函数式组件中,我们在使用副作用/引用子组件时都需要时刻注意对代码进行性能上的优化。
最后附上React Hooks FQA

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值