React Hooks 万字总结

173 阅读13分钟

前言

在刚接触前端MV* 框架时最开始接触的就是React,当时还是在所谓的'大厂',但当时还早,所以接触的是14这个版本,后来从这边离开以后进入了创业公司,为了快速迭代,技术从React转到了Vue上;

最近打算重学React,自己在网上看了很多关于Hooks的文章,经过系统的学习了一遍,进行总结所以有了这篇文章

Hooks 是什么

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

为什么使用 Hooks

类组件的缺点

  • 在组件之间复用状态逻辑很难
  • 复杂组件变得难以理解
  • 难以理解 class 你必须去理解 JS 中 this 的工作方式,这与其他语言存在巨大差异,还不能忘记绑定事件处理器。没有稳定的语法提案,代码非常冗余

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

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

Hooks 没有破坏性改动

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

使用Hooks的规则

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

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

  1. 只在React函数中调用 Hook

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

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

useState

const [state , setState] = useState(initialState)

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

useEffect

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

useEffect(fn , array)

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

可以把 useEffect Hook 看做是 componentDidMount componentDidUpdatecomponentWillUnmount 这三个函数的组合.

useEffect 实现 componentDidMount

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

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

页面渲染完成后, 会执行一次 useEffect,打印 log, 当点击按钮改变了 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("执行清除操作")
    }
  },[]);

注意:这里不只是组件销毁时才会打印'执行清除操作',每次重新渲染时也都会执行.原因官网解释很清楚了,查吧

控制 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;

数组中有元素时表示此元素的值发生改变后重新渲染.

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

使用 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

使用方法一(推荐)

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

使用方法二

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

useEffect 做了什么

通过使用useEffect这个Hook,你可以告诉React组件需要在渲染后执行某些操作,React会保存你传递的函数,并且在执行DOM更新之后调用它.

为什么在组件内部调用useEffect

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

useContext

概念

const value = useContext(MyContext) ;

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

提示

如果你在接触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 方法.

在某些场景下, 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 版本,该回调函数仅在某个依赖项改变时才会更新.当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染的子组件时,它将非常有用

示例:

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

当点击 '改副标题' 这个 按钮 后,副标题变了 '副标题改变了' ,并且控制台会打印出 '子组件渲染了' ,这就证明了子组件重新渲染了,但是子组件没有任何变化,那么这次 child 组件的重新渲染就是多余的,如果避免渲染呢?

找原因

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

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

  • 要么是组件自己的状态改变
  • 要么是父组件重新渲染,导致子组件重新渲染,但是父组件的 props 没有改变
  • 要么是父组件重新渲染,导致子组件重新渲染,但是父组件传递的 props 改变了

从上述原因来排查:

第一种很明显,当点击按钮时并没有去改变子组件的状态

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

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

用 useCallback 解决问题

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

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

那么保需如下将传给 Child 组件的 callback 函数改造一下就可以了:

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

这样我们就可以看到只会在首次渲染的时候才会渲染.当更改副标题和改标题的时候不会打印了

useMemo

概念

const cacheSomething = useMemo(create , deps)

  • create 第一个参数为一个函数,函数的返回值作为缓存值
  • deps 第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态是否有改变,如果有改变重新执行 useMemo ,得到新的缓存值.
  • 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 属性被初始化为传入的参数,返回的 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;

useRef 缓存数据

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

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 的值已经改变了,但是页面上还是显示的上一次值,重新更新时,才会显示上一次更新的值.