react hooks 万字总结

前言

本身在掘金上看了也看了不少关于hooks的文章,感受都讲得不是很详细。并且也有不少的水文。最近本身打算重学react,系统性的再把hooks给学习一遍。html

Hooks is what?

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

why use Hooks?

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

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

  • 复杂组件变得难以理解编程

  • 难以理解的 classapi

你必须去理解 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 函数中调用 Hookbabel

不要在普通的 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的值,已经改变了,可是页面上仍是显示的上一次的值,从新更新时,才会显示上一次更新的值。

写在最后

若是文章中有什么错误,欢迎指出。

相关文章
相关标签/搜索