react hooks 万字总结,带你夯实基础

13,912 阅读16分钟

前言

自己在掘金上看了也看了很多关于hooks的文章,感觉都讲得不是很详细。而且也有很多的水文。最近自己打算重学react,系统性的再把hooks给学习一遍。

Hooks is what?

  • react-hooks是react16.8以后,react新增的钩子API,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性.
  • Hook是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。

why use Hooks?

类组件的缺点:(来自官网动机

  • 在组件之间复用状态逻辑很难

  • 复杂组件变得难以理解

  • 难以理解的 class 你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,代码非常冗余。

hooks的出现,解决了上面的问题。另外,还有一些其他的优点

  • 增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷
  • react-hooks思想更趋近于函数式编程。用函数声明方式代替class声明方式,虽说class也是es6构造函数语法糖,但是react-hooks写起来函数即是组件,无疑也提高代码的开发效率(无需像class声明组件那样写声明周期,写生命周期render函数等)

Hooks没有破坏性改动

  • 完全可选的。  你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
  • 100% 向后兼容的。  Hook 不包含任何破坏性改动。
  • 现在可用。  Hook 已发布于 v16.8.0。

使用Hooks的规则

1. 只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook

确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

2. 只在 React 函数中调用 Hook

不要在普通的 JavaScript 函数中调用 Hook,你可以:

  • ✅ 在 React 的函数组件中调用 Hook
  • ✅ 在自定义 Hook 中调用其他 Hook

至于为什么会有这些规则,如果你感兴趣,请参考Hook 规则

useState

const [state, setState] = useState(initialState)

  • useState 有一个参数(initialState 可以是一个函数,返回一个值,但一般都不会这么用),该参数可以为任意数据类型,一般用作默认值.
  • useState 返回值为一个数组,数组的第一个参数为我们需要使用的 state,第二个参数为一个改变state的函数(功能和this.setState一样)

来看一个计时器的案例

import React,{useState} from "react";
function Example() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
export default Example;
  • 第一行:  引入 React 中的 useState Hook。它让我们在函数组件中存储内部 state。
  • 第三行:  在 Example 组件内部,我们通过调用 useState Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。我们把变量命名为 count,因为它存储的是点击次数。我们通过传 0 作为 useState 唯一的参数来将其初始化为 0。第二个返回的值本身就是一个函数。它让我们可以更新 count 的值,所以我们叫它 setCount
  • 第七行:  当用户点击按钮后,我们传递一个新的值给 setCount。React 会重新渲染 Example 组件,并把最新的 count 传给它。

使用多个state 变量

 // 声明多个 state 变量
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

不必使用多个 state 变量。State 变量可以很好地存储对象和数组,因此,你仍然可以将相关数据分为一组。

更新State

import React,{useState} from "react";
function Example() {
  const [count, setCount] = useState(0);
  const [person, setPerson] = useState({name:'jimmy',age:22});
  return (
    <div>
      <p>name {person.name} </p>
      // 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将回调函数当做参数传递给 setState。
      // 该回调函数将接收先前的 state,并返回一个更新后的值。
      <button onClick={() => setCount(count=>count+1)}>Click me</button>
      <button onClick={() => setPerson({name:'chimmy'})}>Click me</button>
    </div>
  );
}
export default Example;

setPerson更新person时,不像 class 中的 this.setState,更新 state 变量总是替换它而不是合并它。上例中的person为{name:'chimmy'} 而不是{name:'chimmy',age:22}

useEffect

Effect Hook 可以让你在函数组件中执行副作用(数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用)操作

useEffect(fn, array)

useEffect在初次完成渲染之后都会执行一次, 配合第二个参数可以模拟类的一些生命周期。

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount``componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

useEffect 实现componentDidMount

如果第二个参数为空数组,useEffect相当于类组件里面componentDidMount。

import React, { useState, useEffect } from "react";
function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log("我只会在组件初次挂载完成后执行");
  }, []);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
export default Example;

页面渲染完成后,会执行一次useEffect。打印“我只会在组件初次挂载完成后执行”,当点击按钮改变了state,页面重新渲染后,useEffect不会执行。

useEffect 实现componentDidUpdate

如果不传第二个参数,useEffect 会在初次渲染和每次更新时,都会执行。

import React, { useState, useEffect } from "react";
function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log("我会在初次组件挂载完成后以及重新渲染时执行");
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
export default Example;

初次渲染时,会执行一次useEffect,打印出“我会在初次组件挂载完成后以及重新渲染时执行”。 当点击按钮时,改变了state,页面重新渲染,useEffect都会执行,打印出“我会在初次组件挂载完成后以及重新渲染时执行”。

useEffect 实现componentWillUnmount

effect 返回一个函数,React 将会在执行清除操作时调用它。

useEffect(() => {
    console.log("订阅一些事件");
    return () => {
      console.log("执行清除操作")
    }
  },[]);

注意:这里不只是组件销毁时才会打印“执行清除操作”,每次重新渲染时也都会执行。至于原因,我觉得官网解释的很清楚,请参考 解释: 为什么每次更新的时候都要运行 Effect

控制useEffect的执行

import React, { useState, useEffect } from "react";
function Example() {
  const [count, setCount] = useState(0);
  const [number, setNumber] = useState(1);
  useEffect(() => {
    console.log("我只会在cout变化时执行");
  }, [count]);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click cout</button>
      <button onClick={() => setNumber(number + 1)}>Click number</button>
    </div>
  );
}
export default Example;

上面的例子,在点击 click cout按钮时,才会打印“我只会在cout变化时执行”。 因为useEffect 的第二个参数的数组里面的依赖是cout,所以,只有cout发生改变时,useEffect 才会执行。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。

使用多个 Effect 实现关注点分离

使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。

import React, { useState, useEffect } from "react";
function Example() {
  useEffect(() => {
    // 逻辑一
  });
  useEffect(() => {
    // 逻辑二
  });
   useEffect(() => {
    // 逻辑三
  });
  return (
    <div>
      useEffect的使用
    </div>
  );
}
export default Example;

Hook 允许我们按照代码的用途分离他们,  而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。

useEffect中使用异步函数

useEffect是不能直接用 async await 语法糖的

/* 错误用法 ,effect不支持直接 async await*/
 useEffect(async ()=>{
        /* 请求数据 */
      const res = await getData()
 },[])

useEffect 的回调参数返回的是一个清除副作用的 clean-up 函数。因此无法返回 Promise,更无法使用 async/await

那我们应该如何让useEffect支持async/await呢?

方法一(推荐)

const App = () => {
  useEffect(() => {
    (async function getDatas() {
      await getData();
    })();
  }, []);
  return <div></div>;
};

方法二

  useEffect(() => {
    const getDatas = async () => {
      const data = await getData();
      setData(data);
    };
    getDatas();
  }, []);

useEffect 做了什么

通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。

为什么在组件内部调用 useEffect

将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useContext

概念

const value = useContext(MyContext);

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

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

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

示例

index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
// 创建两个context
export const UserContext = React.createContext();
export const TokenContext = React.createContext();
ReactDOM.render(
  <UserContext.Provider value={{ id: 1, name: "chimmy", age: "20" }}>
    <TokenContext.Provider value="我是token">
      <App />
    </TokenContext.Provider>
  </UserContext.Provider>,
  document.getElementById("root")
);

app.js

import React, { useContext } from "react";
import { UserContext, TokenContext } from "./index";

function Example() {
  let user = useContext(UserContext);
  let token = useContext(TokenContext);
  console.log("UserContext", user);
  console.log("TokenContext", token);
  return (
    <div>
      name:{user?.name},age:{user?.age}
    </div>
  );
}
export default Example;

打印的值如下

41PXCT[XET_ZJ7]79K@~6JX.png

提示

如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>

useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

useReducer

概念

const [state, dispatch] = useReducer(reducer, initialArg, init); useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

注意点

React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch

示例

import React, { useReducer } from "react";
export default function Home() {
  function reducer(state, action) {
    switch (action.type) {
      case "increment":
        return { ...state, counter: state.counter + 1 };
      case "decrement":
        return { ...state, counter: state.counter - 1 };
      default:
        return state;
    }
  }
  const [state, dispatch] = useReducer(reducer, { counter: 0 });
  return (
    <div>
      <h2>Home当前计数: {state.counter}</h2>
      <button onClick={(e) => dispatch({ type: "increment" })}>+1</button>
      <button onClick={(e) => dispatch({ type: "decrement" })}>-1</button>
    </div>
  );
}

useCallback

概念

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

返回一个 [memoized]回调函数。

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

示例

import React, { useState } from "react";
// 子组件
function Childs(props) {
  console.log("子组件渲染了");
  return (
    <>
      <button onClick={props.onClick}>改标题</button>
      <h1>{props.name}</h1>
    </>
  );
}
const Child = React.memo(Childs);
function App() {
  const [title, setTitle] = useState("这是一个 title");
  const [subtitle, setSubtitle] = useState("我是一个副标题");
  const callback = () => {
    setTitle("标题改变了");
  };
  return (
    <div className="App">
      <h1>{title}</h1>
      <h2>{subtitle}</h2>
      <button onClick={() => setSubtitle("副标题改变了")}>改副标题</button>
      <Child onClick={callback} name="桃桃" />
    </div>
  );
}

执行结果如下图 image.png

当我点击改副标题这个 button 之后,副标题会变为「副标题改变了」,并且控制台会再次打印出子组件渲染了,这就证明了子组件重新渲染了,但是子组件没有任何变化,那么这次 Child 组件的重新渲染就是多余的,那么如何避免掉这个多余的渲染呢?

找原因

我们在解决问题的之前,首先要知道这个问题是什么原因导致的?

咱们来分析,一个组件重新重新渲染,一般三种情况:

  1. 要么是组件自己的状态改变
  2. 要么是父组件重新渲染,导致子组件重新渲染,但是父组件的 props 没有改变
  3. 要么是父组件重新渲染,导致子组件重新渲染,但是父组件传递的 props 改变 接下来用排除法查出是什么原因导致的:

第一种很明显就排除了,当点击改副标题 的时候并没有去改变 Child 组件的状态;

第二种情况,我们这个时候用 React.memo 来解决了这个问题,所以这种情况也排除。

那么就是第三种情况了,当父组件重新渲染的时候,传递给子组件的 props 发生了改变,再看传递给 Child 组件的就两个属性,一个是 name,一个是 onClickname 是传递的常量,不会变,变的就是 onClick 了,为什么传递给 onClick 的 callback 函数会发生改变呢?其实在函数式组件里每次重新渲染,函数组件都会重头开始重新执行,那么这两次创建的 callback 函数肯定发生了改变,所以导致了子组件重新渲染。

用useCallback解决问题

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

把函数以及依赖项作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,这个 memoizedCallback 只有在依赖项有变化的时候才会更新。

那么只需这样将传给Child组件callback函数的改造一下就OK了

const callback = () => { setTitle("标题改变了"); };
// 通过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child
<Child onClick={useCallback(callback, [])} name="桃桃" />

这样我们就可以看到只会在首次渲染的时候打印出子组件渲染了,当点击改副标题和改标题的时候是不会打印子组件渲染了的。

useMemo

概念

const cacheSomething = useMemo(create,deps)

  • create:第一个参数为一个函数,函数的返回值作为缓存值。
  • deps: 第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。
  • cacheSomething:返回值,执行 create 的返回值。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。

useMemo原理

useMemo 会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在,但是 deps 中如果有一项改变,就会重新执行 create ,返回值作为新的值记录到 fiber 对象上。

示例

function Child(){
    console.log("子组件渲染了")
    return <div>Child</div> 
}
const Child = memo(Child)
function APP(){
    const [count, setCount] = useState(0);
    const userInfo = {
      age: count,
      name: 'jimmy'
    }
    return <Child userInfo={userInfo}>
}

当函数组件重新render时,userInfo每次都将是一个新的对象,无论 count 发生改变没,都会导致 Child组件的重新渲染。

而下面的则会在 count 改变后才会返回新的对象。

function Child(){
    console.log("子组件渲染了")
    return <div>Child</div> 
}
function APP(){
    const [count, setCount] = useState(0);
    const userInfo = useMemo(() => {
      return {
        name: "jimmy",
        age: count
      };
    }, [count]);
    return <Child userInfo={userInfo}>
}

实际上 useMemo 的作用不止于此,根据官方文档内介绍:以把一些昂贵的计算逻辑放到 useMemo 中,只有当依赖值发生改变的时候才去更新。

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

// 计算和的函数,开销较大
function calcNumber(count) {
  console.log("calcNumber重新计算");
  let total = 0;
  for (let i = 1; i <= count; i++) {
    total += i;
  }
  return total;
}
export default function MemoHookDemo01() {
  const [count, setCount] = useState(100000);
  const [show, setShow] = useState(true);
  const total = useMemo(() => {
    return calcNumber(count);
  }, [count]);
  return (
    <div>
      <h2>计算数字的和: {total}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      <button onClick={e => setShow(!show)}>show切换</button>
    </div>
  )
}

当我们去点击 show切换按钮时,calcNumber这个计算和的函数并不会出现渲染了.只有count 发生改变时,才会出现计算.

useCallback 和 useMemo 总结

简单理解呢 useCallback 与 useMemo 一个缓存的是函数,一个缓存的是函数的返回的结果。useCallback 是来优化子组件的,防止子组件的重复渲染。useMemo 可以优化当前组件也可以优化子组件,优化当前组件主要是通过 memoize 来将一些复杂的计算逻辑进行缓存。当然如果只是进行一些简单的计算也没必要使用 useMemo。

我们可以将 useMemo 的返回值定义为返回一个函数这样就可以变通的实现了 useCallback。useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

useRef

const refContainer = useRef(initialValue); useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变

useRef 获取dom

useRef,它有一个参数可以作为缓存数据的初始值,返回值可以被dom元素ref标记,可以获取被标记的元素节点.

import React, { useRef } from "react";
function Example() {
  const divRef = useRef();
  function changeDOM() {
    // 获取整个div
    console.log("整个div", divRef.current);
    // 获取div的class
    console.log("div的class", divRef.current.className);
    // 获取div自定义属性
    console.log("div自定义属性", divRef.current.getAttribute("data-clj"));
  }
  return (
    <div>
      <div className="div-class" data-clj="我是div的自定义属性" ref={divRef}>
        我是div
      </div>
      <button onClick={(e) => changeDOM()}>获取DOM</button>
    </div>
  );
}
export default Example;

1.png

useRef 缓存数据

useRef还有一个很重要的作用就是缓存数据,我们知道usestate ,useReducer 是可以保存当前的数据源的,但是如果它们更新数据源的函数执行必定会带来整个组件从新执行到渲染,如果在函数组件内部声明变量,则下一次更新也会重置,如果我们想要悄悄的保存数据,而又不想触发函数的更新,那么useRef是一个很棒的选择。

下面举一个,每次换成state 上一次值的例子

import React, { useRef, useState, useEffect } from "react";
function Example() {
  const [count, setCount] = useState(0);

  const numRef = useRef(count);

  useEffect(() => {
    numRef.current = count;
  }, [count]);

  return (
    <div>
      <h2>count上一次的值: {numRef.current}</h2>
      <h2>count这一次的值: {count}</h2>
      <button onClick={(e) => setCount(count + 10)}>+10</button>
    </div>
  );
}
export default Example;

当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。所以,上面的例子中虽然numRef.current的值,已经改变了,但是页面上还是显示的上一次的值,重新更新时,才会显示上一次更新的值。

写在最后

如果文章中有什么错误,欢迎指出。