React之hook函数运用

77 阅读10分钟

1.什么是hooks

  • Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。实际就是函数组件解决没有state,生命周期,逻辑不能复用的一种技术方案
  • "hooks" 直译是 “钩子”,它并不仅是 react,甚至不仅是前端界的专用术语,而是整个行业所熟知的用语。通常指:系统运行到某一时期时,会调用被注册到该时机的回调函数。
  • react 为例,hooks 是:一系列以 “use” 作为开头的方法,它们提供了让你可以完全避开 class式写法在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力

2.命名规范 和 指导思想

  • 通常来说,hooks 的命名都是以 use 作为开头,这个规范也包括了那么我们自定义的 hooks。为什么?因为这是一种约定
  • react 官方文档里,对 hooks 的定义和使用提出了 “一个假设、两个只在” 核心指导思想。
    • 一个假设: 假设任何以 use 开头并紧跟着一个大写字母的函数就是一个 Hook
    • 第一个只在: 只在 React 函数组件中调用 Hook,而不在普通函数中调用 Hook。(Eslint 通过判断一个方法是不是大坨峰命名来判断它是否是 React 函数)
    • 第二个只在: 只在最顶层使用 Hook,而不要在循环,条件或嵌套函数中调用 Hook。
  • 因为是约定,因而 useXxx 的命名并非强制,你依然可以将你自定义的 hook 命名为 byXxx 或其他方式,但不推荐。

3.为什么需要hooks

  • 1.更好的状态复用,解决mixins的各种弊端

  • 2.更好的代码组织

    • 一个页面中,N件事情的代码在一个组件内互相纠缠,代码量庞大的时候影响阅读 维护困难。

    • 那么 Hooks 写法在代码组织上究竟能带来怎样的提升呢?参考下图。

      vue hooks

    • 无论是 vue 还是 react, 通过 Hooks 写法都能做到,将“分散在各种声明周期里的代码块”,通过 Hooks 的方式将相关的内容聚合到一起

    • 这样带来的好处是显而易见的:“高度聚合,可阅读性提升”。伴随而来的便是 “效率提升,bug变少”

  • 3.比 class 组件更容易理解,尤其是 this

    • reactclass 写法中,随处可见各种各样的 .bind(this)。(甚至官方文档里也有专门的章节描述了“为什么绑定是必要的?”这一问题)。很显然,绑定虽然“必要”,但并不是“优点”,反而是“故障高发”地段。
    • 但在Hooks 写法中,你就完全不必担心 this 的问题了。因为:本来无一物,何处惹尘埃。
    • Hooks 写法直接告别了 this,从“函数”来,到“函数”去。
  • 4.友好的渐进式

    • 渐进式的含义是:你可以一点点深入使用。
    • 无论是 vue 还是 react,都只是提供了 Hooks API,并将它们的优劣利弊摆在了那里。并没有通过无法接受的 break change 来强迫你必须使用 Hooks 去改写之前的 class 组件。
    • 你依然可以在项目里一边写 class 组件,一边写 Hooks 组件,在项目的演进和开发过程中,这是一件没有痛感,却悄无声息改变着一切的事情。

4.useState

  • react的所有hooks都是从react中引入后再使用,或者用React.useXxx使用

  • useState传入一个参数作为默认值,返回一个数组,数组中第一个参数为状态,第二个参数为修改状态的方法。

简单体验

import React, { useState } from 'react';

const Index = () => {
    const [count, setCount] = useState(0)
    return (
    <React.Fragment>
        UseState:  {count} &emsp;
        <button onClick={() => setCount(count + 1)}>点我+1</button>
    </React.Fragment>
    );
}

export default Index;

详细一点

import React, { useState } from 'react';

const Index = () => {
    // 创建一个或多个状态
    const [count, setCount] = useState(0)
    const [name, setName] = useState('小明')
    const [age, setAge] = useState(18)
    // 创建对象状态
    const [list, addList] = useState([])
    return (
    <React.Fragment>
        UseState:  {count} &emsp; 姓名:{name} &emsp; 年龄:{age} <br />
        <button onClick={() => setCount(count + 1)}> 点我+1 </button>
        <button onClick={() => setName(name === '小明' ? '小张' : '小明')}> 点我切换姓名 </button>
        <button onClick={() => setAge(age + 1)}> 点我修改年龄 </button><hr />
        
        <button onClick={() => addList([{ name, age, id: Math.random() * 10000 }, ...list])}>
        点我列表+1
        </button>
        {list.map(i => (<div key={i.id}>姓名:{i.name},年龄:{i.age}</div>))}
    </React.Fragment>
    );
}

export default Index;

5.useEffect

  • useEffect以函数形式直接调用,没有返回值,接收两个参数

  • 第一个是回调函数(必传),可以在里面模拟生命周期

    • 第一个参数必传,同时必须是一个函数
    • 该函数中可返回一个函数,返回的函数实现了componentWillUnmount
  • 第二个是一个数组(选传),可以指定需要监听的对象

    • 如果不传,默认所有状态变更(包括父组件和子组件)都会触发useEffect的第一个回调函数

    • 如果传一个空数组,那么只在第一次加载和组件卸载时才会触发useEffect的第一个回调函数

    • 如果在useEffect的第一个回调函数中使用了某个状态,那么第二个参数就不能是空数组,否则会警告

    • 如果数组中传递了参数,那么就只会在该参数发生改变时,才会触发useEffect的第一个回调函数

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

const Index = () => {
    const [count, setCount] = useState(0)
    const [show, setShow] = useState(true)
    return (<div>
        父组件: {count}
        {show ?<Children count={count} setCount={setCount} /> : ''}
        <button onClick={() => setShow(!show)}>点我</button> </div>
    );
}

const Children = (props) => {
    useEffect(() => {
        console.log('加载子组件,开启计时器')

        const timer = setInterval(() => {
            props.setCount(props.count + 1)
        }, 1000)

        return () => {
            console.log('下载子组件,关闭定时器')
            clearInterval(timer)
        }

    }, [props])

    return (<div>子组件:{props.count}</div> )
}

export default Index;

6.useContext

1.首先回忆一下Context

一种组件间通信方式, 常用于【祖组件】与【后代组件】间通信

使用方式

  1. 创建Context容器对象:
const XxxContext = React.createContext()  
  1. 渲染子组时,外面包裹xxxContext.Provider, 通过value属性给后代组件传递数据:
<xxxContext.Provider value={value}>
    子组件
</xxxContext.Provider>
  1. 后代组件读取数据:
//第一种方式:仅适用于类组件 
static contextType = xxxContext  // 声明接收context
this.context // 读取context中的value数据

//第二种方式: 函数组件与类组件都可以
<xxxContext.Consumer>
{   
    // value就是context中的value数据
    value => ( 要显示的内容 )
}
</xxxContext.Consumer>

2.useContext的使用

  • contexts.js
export const CountContext = React.createContext()
  • Parent.js
import React, { useState } from 'react';
import { CountContext } from './contexts'
import Children from './Children'

const Parent = () => {
    const [count, setCount] = useState(0)
    return (
        <CountContext.Provider value={{ count, setCount }}> <Children />
        </CountContext.Provider>
    );
}

export default Parent;
  • Children.js
import React, { useContext } from 'react';
import { CountContext } from './contexts'

const Parent = () => {
    let { count, setCount } = useContext(CountContext)
    return (
        <div onClick={() => setCount(count + 1)}>
            {count}
        </div>
    );
}

export default Parent;

7.useReducer

1.useReducer 基本使用

  • 传入两个参数

    • 第一个是业务函数,该函数接收两个参数
      • 第一个是状态state(初始状态/新的状态)
      • 第二个是自定义配置参数action,用于逻辑处理
    • 第二个是初始化值
  • 返回一个数组,该数组有两个值

    • 第一个值是状态值
    • 第二个值是dispatch函数,调用dispatch函数 传入上面业务函数的action相匹配的规则,即可对state进行修改
import React, { useReducer } from 'react';

const Index = () => {
    const [count, dispatch] = useReducer((state, action) => {
        switch (action) {
            case 'add':
                return state + 1
            case 'sub':
                return state - 1
            default:
                return state
        }
    }, 0)
    return (
        <div style={{ padding: '20px', border: '10x solid #ccc' }}>
            {count}
            <button onClick={() => dispatch('add')}>点我+1</button>
            <button onClick={() => dispatch('sub')}>点我-1</button> </div>
    );
}

export default Index;

2.useReducer + useContext + createContext 实现 redux

以下为项目中该如何使用,(分为3个模块写 个人理解)

  • constant.js > 定义常量,提供给reducer和dispatch调用
  • reducers.js > reducer的集合,存放各种reducer
  • contexts.js > 使用react的createContext创建的context合集,存放各种context
1.创建onstant.js
export const INCREMENT = 'increment' // 加
export const DECREMENT = 'decrement' // 减
export const UPDATE_COLOR = 'update_color' // 改变颜色
2.创建reducers.js
// 引入constant定义的常量
import { INCREMENT, DECREMENT } from './constant'

// 加减法reducer
export const CountReducer = (state, action) => {
    const { type, data } = action
    switch (type) {
        case INCREMENT:
            return state += data
        case DECREMENT:
            return state -= data
        default:
            return state
    }
}
// 改变颜色reducer
export const ColorReducer = (state, action) => {
    const { type, data } = action
    switch (type) {
        case UPDATE_COLOR:
            return data
        default:
            return '#000'
    }
}
3.创建contexts.js
 // 引入核心hook -- createContext
 import { createContext } from 'react'
 // 创建context
 export const CountContext = createContext()
4.上层组件中注册

顶层组件/容器组件/父组件... 都是上层组件

5.子组件中调用dispatch
import React, { useReducer } from 'react';
// 引入reducer
import { CountReducer, ColorReducer } from '../redux/reducers'
// 引入context对象
import { CountContext } from '../redux/context'

const Container = (props) => {
    // 创建并解构 state 及 dispatch --- dispatch可修改任意别名,个人喜欢用setXxx
    const [count, setCount] = useReducer(CountReducer, 0)
    const [color, setColor] = useReducer(ColorReducer, 10)
    // 统一管理,传递给所有子孙组件
    const reduxData = { count, color, setCount, setColor }

    const { children, render } = props
    return (
        <CountContext.Provider value={reduxData}>
            {/* 这里可直接使用state */}
            <div style={{ fontSize: '50px', color }}>{count}</div>

            {/* 作为容器组件时的内容判断 */}
            {children ? children : render ? render : ''}
        </CountContext.Provider>
    );
}

export default Container;

ChangeCount组件

import React, { useContext } from 'react';
import { CountContext } from '../redux/context'
import { DECREMENT, INCREMENT } from '../redux/constant'

const ChangeCount = () => {
    const { setCount } = useContext(CountContext)
    return (
        <div>
            <button onClick={() => setCount({ type: DECREMENT, data: 10 })}>点我+10</button>
            <button onClick={() => setCount({ type: INCREMENT, data: 10 })}>点我-10</button>
        </div>
    );
}

export default ChangeCount;

8.useRef

使用超级简单

import React, { useRef } from 'react';

const Index = () => {
    const inputRef = useRef()
    return (
        <div>
            <input type="text" ref={inputRef} />
            <button onClick={() => alert(inputRef.current.value)}>点我</button>
        </div>
    );
}

export default Index;

9.useMemo

1.调用方式

  • 调用方式和useEffect一样
const a = useMemo(()=>{return 1},[]) 
  • 回调函数中可返回新的数据,该数据可作为计算属性使用(类似vue的computed)
const newName = useMemo(()=>(name + '123'),[name]) 
<div> { newName } <div/>

2.使用场景

假如组件中有两个状态A和B,同时有两个依赖A和B的计算函数(类似vue计算属性computed),当A发生改变时,依赖B的计算函数会被重新调用。

使用useMemo可解决这一问题,useMemo一般用于性能优化,但一定不局限于性能优化。肯定有很多我还没发现的特点。

3.代码示例

import { useState, useMemo } from "react";
export default function App() {
    const [count, setCount] = useState(0);
    const [total, setTotal] = useState(0);

    // 没有使用 useMemo,即使是更新 total, countToString 也会重新计算
    const countToString = (() => {
        console.log("countToString 被调用");
        return count.toString();
    })();

    // 使用了 useMemo, 只有 total 改变,才会重新计算
    const totalToStringByMemo = useMemo(() => {
        console.log("totalToStringByMemo 被调用");
        return total + "";
    }, [total]);

    return (
        <div className="App">
            <h3>countToString: {countToString}</h3>
            <h3>countToString: {totalToStringByMemo}</h3>

            <button onClick={() => setCount((count) => count + 1)} >Count+1</button> &emsp;
            <button onClick={() => setTotal((total) => total + 1)}>Total+1</button>
        </div >
    );
}

10.useCallback

1.调用方式

  • 调用方式和useMemo一样
useCallback(()=>{},[]) 
  • 返回一个回调函数,该函数可用于在其他地方调用,同时会有缓存
const handleCountAddByCallBack = useCallback(() => {
    setCount((count) => count + 1);
}, []);
// 子组件中
this.props.handleCountAddByCallBack()

2.使用场景

子组件调用父组件传递来的函数,去改变父组件的状态时,子组件即使并没有使用父组件的状态,依旧会被重新渲染

使用useCallback包括的函数,传递给子组件使用,则能解决上述问题。一般用于新能优化。

3.代码示例

import React, { useCallback, useEffect, useState } from "react";

export default function App() {
    const [count, setCount] = useState(0);

    // 使用 useCallBack 缓存
    const handleCountAddByCallBack = useCallback(() => {
        setCount((count) => count + 1);
    }, []);


    // 不缓存,每次 count 更新时都会重新创建
    const handleCountAdd = () => {
        setCount((count) => count + 1);
    };

    return (
        <div className="App">
            <h3>CountAddByChild1: {count}</h3>
            <Child1 addByCallBack={handleCountAddByCallBack} add={handleCountAdd} />
        </div>
    );
}

const Child1 = React.memo(function (props) {
    const { add, addByCallBack } = props;

    // 没有缓存,由于每次都创建,memo 认为两次地址都不同,属于不同的函数,所以会触发 useEffect
    useEffect(() => {
        console.log("Child1----addFcUpdate");
    }, [add]);

    // 有缓存,memo 判定两次地址都相同,所以不触发 useEffect
    useEffect(() => {
        console.log("Child1----addByCallBackFcUpdate");
    }, [addByCallBack]);

    return (
        <div>
            <button onClick={props.add}>+1</button>
            <br />
            <button onClick={props.addByCallBack}>+1(addByCallBack)</button>
        </div>
    );
});

11.自定义hook

  • 自定义 Hooks 是一个函数,约定函数名称必须以 use 开头,React 就是通过函数名称是否以 use 开头来判断是不是 Hooks

  • Hooks 只能在函数组件中或其他自定义 Hooks 中使用,否则,会报错!

  • 自定义 Hooks 用来提取组件的状态逻辑,根据不同功能可以有不同的参数和返回值(就像使用普通函数一样)

简单封装一个获取鼠标坐标的hooks

import { useState, useEffect } from 'react'

export const useMousePosition = () => {
    const [position, setPosition] = useState({ x: 0, y: 0 })
    useEffect(() => {
        const updateMouse = (e) => {
            setPosition({ x: e.clientX, y: e.clientY })
        }
        document.addEventListener('mousemove', updateMouse)
        return () => {
            document.removeEventListener('mousemove', updateMouse)
        }
    }, [])
    return position
}