深入浅出Hooks

Hooks-介绍

  • hooks 解释
    • Hooks:钩子、钓钩、钩住 ,HooksReact v16.8 中的新增功能
  • hooks 作用
    • 函数组件提供状态、生命周期等原本 class 组件中提供的 React 功能
    • 可以理解为通过 Hooks 为函数组件钩入 class 组件的特性
    • 注意:Hooks 只能在函数组件中使用,自此,函数组件成为 React 的新宠儿
  • React v16.8 版本前后,组件开发模式的对比
    • React v16.8 以前: class 组件(提供状态) + 函数组件(展示内容)
    • React v16.8 及其以后:
      1. class 组件(提供状态) + 函数组件(展示内容)
      2. Hooks(提供状态) + 函数组件(展示内容)
      3. 混用以上两种方式:部分功能用 class 组件,部分功能用 Hooks+函数组件

总结

  • 虽然有了 Hooks,但 React 官方并没有计划从 React 库中移除 class
  • 有了 Hooks 以后,不能再把函数组件称为无状态组件了,因为 Hooks 为函数组件提供了状态

Hooks-解决的问题

  1. 组件的状态逻辑复用问题
    • 在 Hooks 之前,组件的状态逻辑复用经历了:mixins(混入)、HOCs(高阶组件)、render-props 等模式
    • (早已废弃)mixins 的问题:1 数据来源不清晰 2 命名冲突
    • HOCs、render-props 的问题:重构组件结构,导致组件形成 JSX 嵌套地狱问题
  2. class 组件自身的问题
    • 选择:函数组件和 class 组件之间的区别以及使用哪种组件更合适
    • 需要理解 class 中的 this 是如何工作的
    • 相互关联且需要对照修改的代码被拆分到不同生命周期函数中
    • 相比于函数组件来说,不利于代码压缩和优化,也不利于 TS 的类型推导

Hooks-渐进策略

  1. 什么是渐进式策略(项目开发场景)文档
    • 不推荐直接使用 Hooks 大规模重构现有组件
    • 推荐:新功能用 Hooks,复杂功能实现不了的,也可以继续用 class
    • 找一个功能简单、非核心功能的组件开始使用 hooks
  2. 在 hooks 中开发会使用那些知识?
    • class 组件相关的 API 在 hooks 中不可用
      • class 自身语法,比如,constructor、static 等
      • 钩子函数,componentDidMountcomponentDidUpdatecomponentWillUnmount
      • this 相关的用法
    • 原来学习的 React 内容还是要用的
      • JSX:{}onClick={handleClick}、条件渲染、列表渲染、样式处理等
      • 组件:函数组件、组件通讯
      • React 开发理念:单向数据流状态提升
      • 解决问题的思路、技巧、常见错误的分析等

总结

  1. react 没有计划从 React 中移除 class
  2. react 将继续为 class 组件提供支持
  3. 可以在项目中同时使用 hooks 和 class

useState-基本使用

useState 作用

  • 为函数组件提供状态
  • 它是一个 hook,就是一个特殊的函数,让你在函数组件中获取状态等 React 特性。
  • 名称上看 hook 都以use开头useXxx

useState 语法

// 参数:状态初始值(数值、字符串、数组,对象)
// 返回值:stateArray 是一个数组
const stateArray = useState(0);
// 索引 0 表示:状态值(state)
const state = stateArray[0];
// 索引 1 表示:修改状态的函数(setState)
const setState = stateArray[1];

useState 使用步骤

  1. 导入 useState hook
  2. 调用 useState 函数,并传入状态的初始值
  3. useState 函数的返回值中,拿到状态和修改状态的函数
  4. 在 JSX 中展示状态
  5. 在按钮的点击事件中调用修改状态的函数,来更新状态
import { useState } from 'react';

const Count = () => {
  // stateArray 是一个数组
  const stateArray = useState(0);
  const state = stateArray[0];
  const setState = stateArray[1];

  return (
    <div>
      {/* 展示状态值 */}
      <h1>状态为:{state}</h1>
      {/* 点击按钮,让状态值 +1 */}
      <button onClick={() => setState(state + 1)}>+1</button>
    </div>
  );
};

useState 写法正确姿势

  • 数据函数需要符合业务语义,修改状态的函数名称以 set 开头,后面跟上状态的名称。
import { useState } from 'react';

const Count = () => {
  // 解构:
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>计数器:{state}</h1>
      <button onClick={() => setState(state + 1)}>+1</button>
    </div>
  );
};

总结:

  • useState提供初始化值,返回数组。

    • 数组[0] 状态数据
    • 数组[1] 修改状态函数
  • 参考写法:const [count, setCount] = useState(0)

useState-读取和修改状态

读取状态

  • 读取状态:useState 提供的状态,是函数内部的局部变量,可以在函数内的任意位置使
const UserCom = () => {
  const [user, setUser] = useState({ name: 'jack', age: 18 });
  return (
    <div>
      <p>姓名:{user.name}</p>
      <p>年龄:{user.age}</p>
    </div>
  );
};

修改状态

  • setCount(newValue) 是一个函数,参数表示:新的状态值
  • 调用该函数后,将使用新的状态值替换旧值
  • 修改状态后,因为状态发生了改变,所以该组件会重新渲染
const UserCom = () => {
  const [user, setUser] = useState({ name: 'jack', age: 18 });
  const onAgeAdd = () => {
    setUser({
      ...user,
      age: user.age + 1,
    });
  };
  return (
    <div>
      <p>姓名:{user.name}</p>
      <p>年龄:{user.age}</p>
      <button onClick={onAgeAdd}>年龄+1</button>
    </div>
  );
};

总结:

  • 修改状态的时候,一定要使用新的状态替换旧的状态

useState-组件更新过程

组件初始化时候的事情

  1. 从头开始执行该组件中的代码逻辑
  2. 调用 useState(0) 将传入的参数作为状态初始值,即:0
  3. 渲染组件,此时,获取到的状态 count 值为: 0

setState 后发生的事情

  1. 点击按钮,调用 setCount(count + 1) 修改状态,因为状态发生改变,所以,该组件会重新渲染
  2. 组件重新渲染时,会再次执行该组件中的代码逻辑
  3. 再次调用 useState(0),此时 React 内部会拿到最新的状态值而非初始值,比如,该案例中最新的状态值为 1
  4. 再次渲染组件,此时,获取到的状态 count 值为:1
import { useState } from 'react';
const Counter = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>计数器:{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
};

总结:

  • useState 的初始值(参数)只会在组件第一次渲染时生效,以后的每次渲染,useState 获取到都是最新的状态值。

useState-使用原则

定义多个状态的原则

  • 调用 useState Hook 多次即可,每调用一次 useState Hook 可以提供一个状态
  • useState Hook 多次调用返回的 [state, setState],相互之间,互不影响
  • 尽量按照业务来定义数据,不要全部定义在一起,因为是替换,不是合并

hook 函数书写位置原则

  • React Hooks 只能直接出现在 函数组件 中
  • React Hooks 不能嵌套在 if/for/其他函数 中
  • 原理:React 是按照 Hooks 的调用顺序来识别每一个 Hook,如果每次调用的顺序不同,导致 React 无法知道是哪一个 Hook

可以通过开发者工具进行查看组件的 hooks

01.053a5fe6

总结:

  • 只能在函数组件中使用,不能嵌套在 分支循环语句 中,react 存储 hooks 状态按顺序存储。

useEffect-副作用

  1. side effect 副作用专业解释
    • 在计算机科学中,如果一个函数或其他操作修改了其局部环境之外的状态变量值,那么它就被称为有副作用
    • 在函数组件中:职责就是根据状态渲染 UI,其他的事情都是副作用
  2. 通过生活例子,理解副作用
    • 999 感冒灵,主作用:用于感冒引起的头痛,发热,鼻塞,流涕,咽痛等
    • 副作用:可见困倦、嗜睡、口渴、虚弱感
  3. 使用函数组件常见的副作用
    • 对于 React 组件来说,主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用(比如,手动修改 DOM)
    • 常见的副作用(side effect):数据(Ajax)请求、手动修改 DOM、localStorage、console.log 操作等
    • 当你想要在函数组件中,处理副作用(side effect)时,就要使用 useEffect Hook 了

总结:

  • 对于 react 组件来说,除了渲染 UI 之外的其他操作,都可以称之为副作用

useEffect-基本使用

语法介绍

  • 参数:回调函数(称为 effect),就是在该函数中写副作用代码
  • 执行时机:该 effect 会在组件第一次渲染以及每次组件更新后执行
  • 相当于 componentDidMount + componentDidUpdate
useEffect(() => {
  // 书写副作用代码
});

例:count 更新的时候显示到标题

import { useEffect } from 'react';

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

  useEffect(() => {
    document.title = `当前已点击 ${count} 次`;
  });

  return (
    <div>
      <h1>计数器:{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
};

总结:

  • 在函数组件处理副作用 useEffect(()=>{}) 组件初始化,更新的时候执行

useEffect-依赖项

默认使用 useEffect 的问题

  • useEffect(()=>{}) 只要状态发生更新 useEffect 的 effect 回调就会执行
  • 如果组件中有另外一个状态,另一个状态更新时,刚刚的 effect 回调也会执行

useEffect 依赖项的使用

  • 跳过不必要的执行,只在 count 变化时,才执行相应的 effect
  • useEffect(()=>{},[依赖项]) 依赖项的值变化才会执行 effect
import { useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    document.title = `当前已点击 ${count} 次`;
  }, [count]);

  return (
    <div>
      <h1>计数器:{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(!loading)}>切换 loading</button>
    </div>
  );
};

总结:

  • useEffect(()=>{},[依赖项]) 依赖项可以指定某些状态变化再去执行副作用

useEffect-不要对依赖项撒谎

  • useEffect 完全指南:https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/
  • useEffect 回调函数(effect)中用到的数据(比如,count)就是依赖数据,就应该出现在依赖项数组中
  • 如果 useEffect 回调函数中用到了某个数据,但是,没有出现在依赖项数组中,就会导致一些 Bug 出现
  • 所以,不要对 useEffect 的依赖撒谎
const App = () => {
  const [count, setCount] = useState(0);

  // 错误演示:
  useEffect(() => {
    document.title = '点击了' + count + '次';
  }, []);

  return (
    <div>
      <h1>计数器:{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
};

总结:

  • 副作用中使用的状态,需要写在依赖项中

useEffect-依赖是一个空数组

  1. useEffect 的第二个参数,还可以是一个空数组([])
    1. 表示只在组件第一次渲染后执行 effect
    2. 该 effect 只会在组件第一次渲染后执行,因此可以执行像事件绑定等只需要执行一次的操作。
    3. 相当于 class 组件的 componentDidMount 钩子函数的作用
useEffect(() => {
  const handleResize = () => {};
  window.addEventListener('resize', handleResize);
}, []);

注意:

  • 跟 useState Hook 一样,一个组件中也可以调用 useEffect Hook 多次
  • 推荐:一个 useEffect 只处理一个功能,有多个功能时,使用多次 useEffect

useEffect-清除副作用

  • effect 的返回值是可选的,可省略。也可以返回一个清理函数,用来执行事件解绑等清理操作
  • 清理函数的执行时机:
    • 清理函数会在组件卸载时以及下一次副作用回调函数调用的时候执行,用于清除上一次的副作用。
    • 如果依赖项为空数组,那么会在组件卸载时会执行。相当于组件的componetWillUnmount
useEffect(() => {
  const handleResize = () => {};
  window.addEventListener('resize', handleResize);


  // 这个返回的函数,会在该组件卸载时来执行
  // 因此,可以去执行一些清理操作,比如,解绑 window 的事件、清理定时器 等
  return () => window.removeEventListener('resize', handleResize);
}, []);

useEffect-使用总结

// 1
// 触发时机:1 第一次渲染会执行 2 每次组件重新渲染都会再次执行
// componentDidMount + ComponentDidUpdate
useEffect(() => {});

// 2(使用频率最高)
// 触发时机:只在组件第一次渲染时执行
// componentDidMount
useEffect(() => {}, []);

// 3(使用频率最高)
// 触发时机:1 第一次渲染会执行 2 当 count 变化时会再次执行
// componentDidMount + componentDidUpdate(判断 count 有没有改变)
useEffect(() => {}, [count]);

// 4
useEffect(() => {
  // 返回值函数的执行时机:组件卸载时
  // 在返回的函数中,清理工作
  return () => {
    // 相当于 componentWillUnmount
  };
}, []);

useEffect(() => {
  // 返回值函数的执行时机:1 组件卸载时 2 count 变化时
  // 在返回的函数中,清理工作
  return () => {};
}, [count]);

Hooks进阶

自定义hook

  • 函数需要是 use 开头,否则react不认为是 hooks
  • 自定义 hook 就是对状态和逻辑的封装,将来可以复用

例:

  • 在App.js组件实现一个记录鼠标移动坐标的功能
  • 把实现功能的逻辑和状态封装在一个函数中
  • 将函数放在一个单独的js文件中,导入给App.js使用

未封装前

App.js

import { useState, useEffect } from "react"

const App = () => {

  const [mouse, setMouse] = useState({ x: 0, y: 0 })
  useEffect(() => {
    const handelMouseMove = (e) => {
      setMouse({ x: e.pageX, y: e.pageY })
    }
    document.addEventListener('mousemove', handelMouseMove)
    return () => {
      document.removeEventListener('mousemove', handelMouseMove)
    }
  }, [])

  return <div className="app">根组件 {JSON.stringify(mouse)}</div>
}

export default App

封装hooks之后

hook.js

import { useState, useEffect } from "react"

export const useMouse = () => {
  const [mouse, setMouse] = useState({ x: 0, y: 0 })
  useEffect(() => {
    const handelMouseMove = (e) => {
      setMouse({ x: e.pageX, y: e.pageY })
    }
    document.addEventListener('mousemove', handelMouseMove)
    return () => {
      document.removeEventListener('mousemove', handelMouseMove)
    }
  }, [])
  return mouse
}

App.js

import { useMouse } from "./hooks" 

const App = () => {

  const mouse = useMouse()

  return <div className="app">根组件 {JSON.stringify(mouse)}</div>
}

export default App

useRef-基本使用

使用useRef可以获取dom元素,组件也可以通过它获取。

  • 导入 useRef 函数从 react
  • 创建ref对象 const ref = useRef(null)
  • 给需要获取的标签上 ref={ref} 绑定ref对象
  • 渲染完毕后,可以通过 ref.current 获取dom元素

例:组件初始化自动获取焦点功能

import { useRef, useEffect } from "react";

const App = () => {
  const inputRef = useRef(null);

  useEffect(() => {
    // input DOM元素
    console.log(inputRef.current); 
    // input 获取焦点
    inputRef.current.focus()
  }, []);

  return (
    <div className="app">
      根组件:
      <input type="text" ref={inputRef} />
    </div>
  );
};

export default App;

useContext-基本使用

大致步骤:

  • 通过 createContext 创建context对象
  • 通过 Provider 组件包裹根组件,注入数据
  • 后代 组件中使用 useContext 使用数据
  1. 通过 createContext 创建context对象

    context.js

    import { createContext } from "react";
    export default createContext({})
    
  2. 通过 Provider 组件包裹根组件,注入数据

    App.jsx

    import { useState } from "react";
    import Context from "./context";
    import Child from './Child'
    
    const App = () => {
      const [count, setCount] = useState(0)
      return (
        <Context.Provider value={count}>
          <div className="app">
            根组件:{count} <button onClick={() => setCount(count + 1)}>打豆豆</button>
            <hr />
            <Child />
          </div>
        </Context.Provider>
      );
    };
    export default App;
    
  3. 后代 组件中使用 useContext 使用数据

    Child.jsx

    import { useContext } from "react"
    import Context from './context'
    const Child = () => {
      const count = useContext(Context)
      return <div>后代组件:{count}</div>
    }
    
    export default Child
    

总结:

  • 创建context对象和Provider注入数据和之前一样,使用数据只需要 useContext 即可。

欢迎留言