React_Hook

322 阅读12分钟

HOOK简介

HOOK是React16.8.0之后出现的

HOOK(钩子)本质上是一个函数(命名上总是以use开头),该函数可以挂载任何功能

组件分为两种:无状态组件(函数组件)、类组件

类组件中的麻烦:

  1. this指向问题

  2. 繁琐的生命周期

  3. 其他问题...

HOOK专门用于增强函数组件的功能(不能再类组件中使用HOOK),使之理论上可以成为类组件的替代品

官方强调:没有必要更改已经完成的类组件,官方目前没有计划取消类组件,只是鼓励使用函数组件

一、State Hook

State Hook是一个在函数组件中使用的函数(useState),用于在函数组件中使用状态

useState

  • 使用该函数需要传递一个参数,这个参数的值表示状态的初始值
  • 这个函数的返回值是一个数组,该数组长度一定为2
    • 第一项:当前状态的值
    • 第二项:改变状态的函数

一个函数组件中可以有多个状态,即可以多次使用useState,这种做法非常有利于横向切分关注点。

// 举个例子:
import React, { useState } from 'react'     // 记得要先引入useState函数
export default function App() {
    // 声明了一个叫"data"的状态
    const [data, setData] = useState({  // 这里的const []是ES6中的解构赋值,和HOOK无关
        x: 1,
        y: 2
    });
    return <div>
        <p >
            x: {data.x},y:{data.y}
            <button onClick={() => {
                // 更新状态,直接替换掉之前的对象,而不是合并(混合)
                setData({
                    ...data,
                    x: data.x + 1
                })
            }}>x+1</button>
        </p>
    </div>
}

和类组件中setState的第一个参数可以传入函数一样。useState返回的函数的参数也可以传入一个函数,并且函数的参数是可信任的(实时的)

// 举个例子:
import React, { useState } from 'react'
export default function App() {
    console.log("App render")
    const [n, setN] = useState(0); //使用一个状态,该状态的默认值是0
    return <div>
        <span>{n}</span>
        <button onClick={() => {
            // 点击按钮我们需要多次调用setN来改变状态,即点击按钮一次后,n + 2
            
            // 1.如果像下面这样写,点击按钮后,n只加1
            // setN(n + 1) //不会立即改变,事件运行完成之后一起改变
            // setN(n + 1) //此时,n的值仍然是0(不是实时的)
            
            // 2.所以可以setN当中写入函数,函数的参数preN是可信任的
            setN(prevN => prevN - 1);   // 注意:该函数也不会立即执行
            //传入的函数,在事件完成之后统一运行。但是这里的prevN是可信任的(实时的)
            setN(prevN => prevN - 1);
        }}>+</button>
    </div>
}

注意:

  1. useState最好写到函数的起始位置,便于阅读
  2. useState严禁出现在代码块(判断、循环)中
  3. useState返回的函数(数组的第二项),引用不变(节约内存空间)
  4. 使用函数改变状态,若数据和之前的状态完全相等(使用Object.is比较),不会导致重新渲染,以达到优化效率的目的。
  5. 使用函数改变状态,传入的值不会和原来的数据进行合并,而是直接替换。
  6. 如果要实现强制刷新组件
    1. 类组件:使用forceUpdate函数
    2. 函数组件:使用一个空对象的useState
  7. 如果某些状态之间没有必然的联系,应该分化为不同的状态,而不要合并成一个对象。这一点也是useState的优点。如果使用类组件,不管状态之间有没有联系,都要写在一起
  8. 和类组件的状态一样,函数组件中改变状态可能是异步的(在DOM事件中),多个状态变化会合并以提高效率,此时,不能信任之前的状态,而应该使用回调函数的方式改变状态。如果状态变化要使用到之前的状态,尽量传递函数。

二、Effect Hook

Effect Hook:用于在函数组件中处理副作用

副作用:

  1. ajax请求
  2. 计时器
  3. 其他异步操作
  4. 更改真实DOM对象
  5. 本地存储
  6. 其他会对外部产生影响的操作

函数:useEffect,该函数接收一个函数作为参数,接收的函数就是需要进行副作用操作的函数

import React, { useState, useEffect } from 'react'
export default function App() {
    const [n, setN] = useState(10);
    useEffect(() => {
        if (n === 0) {
            return;
        }
        //没一次渲染完成后,需要根据当前n的值,1秒后重新渲染
        setTimeout(() => {  // 特别注意:这里不能使用setInterval,好好理解为什么不能
            setN(n - 1);
        }, 1000)
    }, [n])
    return (
        <div>
            <h1>{n}</h1>
            <button onClick={() => {
                setN(n + 1);
            }}>n+1</button>
        </div>
    )
}

细节

  1. 副作用函数的运行时间点,是在页面完成真实的UI渲染之后。因此它的执行是异步的,并且不会阻塞浏览器
    1. 与类组件中componentDidMount和componentDidUpdate的区别
    2. componentDidMount和componentDidUpdate,更改了真实DOM,但是用户还没有看到UI更新,同步的。
    3. useEffect中的副作用函数,更改了真实DOM,并且用户已经看到了UI更新,异步的。
  2. 每个函数组件中,可以多次使用useEffect,但不要放入判断或循环等代码块中。
  3. useEffect中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数
    1. 该函数运行时间点,在每次运行副作用函数之前
    2. 首次渲染组件不会运行
    3. 组件被销毁时一定会运行
  4. useEffect函数,可以传递第二个参数
    1. 第二个参数是一个数组
    2. 数组中记录该副作用的依赖数据
    3. 当组件重新渲染后,只有依赖数据与上一次不一样的时,才会执行副作用
    4. 所以,当传递了依赖数据之后,如果数据没有发生变化
      1. 副作用函数仅在第一次渲染后运行
      2. 清理函数仅在卸载组件后运行
  5. 副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化。
  6. 副作用函数在每次注册时,会覆盖掉之前的副作用函数,因此,尽量保持副作用函数稳定,否则控制起来会比较复杂。

三、自定义Hook

State Hook: useState Effect Hook:useEffect

自定义Hook:将一些常用的、跨越多个组件的Hook功能,抽离出去形成一个函数,该函数就是自定义Hook,自定义Hook,由于其内部需要使用Hook功能,所以它本身也需要按照Hook的规则实现:

  1. 函数名必须以use开头
  2. 调用自定义Hook函数时,应该放到顶层

例如:

  1. 很多组件都需要在第一次加载完成后,获取所有学生数据
  2. 很多组件都需要在第一次加载完成后,启动一个计时器,然后在组件销毁时卸载

使用Hook的时候,如果没有严格按照Hook的规则进行,eslint的一个插件(eslint-plugin-react-hooks)会报出警告

四、Context Hook

useContext:

  • 接收一个context对象(React.createContext的返回值)
  • 返回该context的当前值

以前的写法:

function Test() {
    return <ctx.Consumer>
        value => <h1>{value}</h1>
    </ctx.Consumer>
}

const ctx = React.createContext();
export default function App() {
    return (
        <div>
            <ctx.Provider value="abc">
                <Text />
            </ctx.Provider>
        </div>
    )
}

现在的写法:

import React, { useContext } from 'react';

function Test() {
    const value = useContext(ctx);  // 使用上下文
    return <h1>{value}</h1>
}

五、Callback Hook

函数名:useCallback

用于得到一个固定引用值的函数,通常用它进行性能优化

该函数有两个参数:

  1. 第一个参数为函数,useCallback会固定该函数的引用,只要依赖项没有发生变化,则始终返回之前函数的地址
  2. 第二个参数为数组,记录依赖项

该函数返回:引用相对固定的函数地址

// 举个例子:先看一个问题
import React, { useState } from 'react'
class Test extends React.PureComponent {    // 这里继承纯组件,属性不变不渲染
    render() {
        console.log("Test Render")  // 渲染时输出
        return <div>
            <h1>{this.props.text}</h1>
            <button onClick={this.props.onClick}>改变文本</button>
        </div>
    }
}
export default function App() {
    console.log("App Render")    // 渲染时输出
    const [txt, setTxt] = useState(1)
    const [n, setN] = useState(0)
    return (
        <div>
            <Test text={txt} onClick={() => {
                setText(txt + 1)
            }} />
            <input type="number"
                value={n}
                onChange={e => {
                    setN(parseInt(e.target.value))
                }}
            />
        </div>
    )
}
// 以上代码运行之后,我们改变改变input框中的值,打印以下内容:
// App render
// Test render
// 为啥?Test组件的属性text没有变化,为什么还会重新渲染?这是因为onClick的值变了
// 当App重新渲染时,onClick的函数引用变了。这完全不是我们想要的效果!

// 函数的地址每次渲染都发生了变化,导致了子组件跟着重新渲染,若子组件是经过优化的组件,
// 则可能导致优化失效
// 使用useCallback:
import React, { useState, useCallback } from 'react'
class Test extends React.PureComponent {  
    render() {
        console.log("Test Render")  // 渲染时输出
        return <div>
            <h1>{this.props.text}</h1>
            <button onClick={this.props.onClick}>改变文本</button>
        </div>
    }
}
export default function App() {
    console.log("App Render")    // 渲染时输出
    const [txt, setTxt] = useState(1)
    const [n, setN] = useState(0)
    const handleClick = useCallback(() => {
        setText(txt + 1)
    },[txt])
    return (
        <div>
            <Test text={txt} onClick={handleClick} />
            <input type="number"
                value={n}
                onChange={e => {
                    setN(parseInt(e.target.value))
                }}
            />
        </div>
    )
}
// 改变input框中n的值,值打印:App render
// 因为useCallback中函数的依赖项txt没有发生变化,所以handleClick的引用没有发生变化

六、Memo Hook

用于保持一些比较稳定的数据,通常用于性能优化。有助于比避免在每次渲染时都进行高开销的计算。

useMemo函数:

  • 第一个参数传入一个函数,当依赖项不发生变化时,函数不会执行。如果变化,会将函数执行后的返回值作为useMemo自身的返回值
  • 第二个参数传入依赖项

如果React元素本身的引用没有发生变化,一定不会重新渲染

import React, {useState} from 'react';
function Item(props) {
    return (
        <li>{props.value}</li>
    )
}
export default function App() {
    const [range, ] = useState({
        min: 1,
        max: 10000
    })
    const [n, setN] = useState(0);
    const list = [];
    for(let i = range.min; i <= range.max; i ++) {
        list.push(<Item key={i} value={i}></Item>)
    }
    return  (<div>
        <ul>{list}</ul>
        <input type="number" 
            value={n} 
            onChange={e => {
                setN(parseInt(e.target.value));
            }}
        />
    </div>)
}
// 每次改变输入框中的值,都会重新渲染10000次<Item>组件,这显然会消耗大量性能
// 使用useMemo:
import React, { useState, useMemo } from 'react'

function Item(props) {
    return <li>{props.value}</li>
}
export default function App() {
    const [range,] = useState({ min: 1, max: 10000 })
    const [n, setN] = useState(0)
    const list = useMemo(() => {
        const list = [];
        for (let i = range.min; i <= range.max; i++) {
            list.push(<Item key={i} value={i}></Item>)
        }
        return list;
    }, [range.min, range.max])
   // 当依赖的值不发生变化时,useMemo里面的函数就不会执行。还是使用原来的数据
    return (
        <div>
            <ul>
                {list}
            </ul>
            <input type="number"
                value={n}
                onChange={e => {
                    setN(parseInt(e.target.value))
                }}
            />
        </div>
    )
}

大家可能会发现:Memo Hook 和 Callback Hook 很相似,参数第一项都传递函数,第二项都传递依赖项。那它们有什么区别嘛?

Callback Hook 只能用来固定函数,当依赖项不变时,传入的函数的引用不会变化;而Memo Hook虽然第一个参数也是函数,但是固定的是函数的返回值,所以Memo Hook 可以固定任何数据。

七、Ref Hook

useRef函数:

  1. 一个参数:默认值
  2. 返回一个固定的对象,对象为{current: 值}
import React, { useState, useRef } from 'react'
// 如果用React.createRef创建一个ref,那么每次重新渲染
// ref都被得到一个新的对象。
// 但是,其实ref完全没必要是一个新的对象,这样降低效率。是同一个对象即可。
// 所以,可以通过useRef生成一个固定的对象
export default function App() {
    const inpRef = useRef();
    const [n, setN] = useState(0)
    return (
        <div>
            <input ref={inpRef} type="text" />bei
            <button onClick={() => {
                console.log(inpRef.current.value)
            }}>得到input的值</button>

            <input type="number"
                value={n}
                onChange={e => {
                    setN(e.target.value)
                }} />
        </div>
    )
}
import React, { useState, useRef, useEffect } from 'react'
// 本来可以在函数外面定义一个timer,然后在函数里面使用
// 但是这样的话,如果这个组被使用了很多次,那么这些组件将共用一个timer,
// 当其中一个timer被注销,其他组件就都不能使用timer了。

// 但是写在里面,每次重新渲染,都会重新生成一个计时器

// 为了解决这个问题,写在里面,还要让每次渲染的计时器是相同的。
// 这就用到了useRef,他就是用来生成一个固定的对象的
export default function App() {
    const [n, setN] = useState(10)
    const timerRef = useRef()
    useEffect(() => {
        if (n === 0) {
            return;
        }
        timerRef.current = setTimeout(() => {
            console.log(n)
            setN(n - 1)
        }, 1000)
        return () => {
            clearTimeout(timerRef.current);
        }
    }, [n])
    return (
        <div>
            <h1>{n}</h1>
        </div>
    )
}

八、ImperativeHandle Hook

可以让你在使用ref时自定义暴露给父组件的实例值。

函数:useImperativeHandleHook

import React, { useRef, useImperativeHandle } from 'react'

function Test(props, ref) {
    useImperativeHandle(ref, () => {
        //如果不给依赖项,则每次运行函数组件都会调用该方法
        //如果使用了依赖项,则第一次调用后,会进行缓存,只有依赖项发生变化时才会重新调用函数
        //相当于给 ref.current = 1
        return {  
            method(){   
                console.log("Test Component Called")
            }
        }
    }, [])
    return <h1>Test Component</h1>
}
const TestWrapper = React.forwardRef(Test)
export default function App() {
    const testRef = useRef();
    return (
        <div>
            <TestWrapper ref={testRef} />
            <button onClick={() => {
                testRef.current.method();
            }}>点击调用Test组件的method方法</button>
        </div>
    )
}

九、LayoutEffect Hook

useEffect:浏览器渲染完成后,用户看到新的渲染结果之后运行 useLayoutEffectHook:完成了DOM改动,但还没有呈现给用户就开始运行。其用法和useEffect用法、用途都一致,只是运行时间点不同

应该尽量使用useEffect,因为它不会导致渲染阻塞,如果出现了问题,再考虑使用useLayoutEffectHook

// 举个例子:
import React, { useState, useLayoutEffect, useRef } from 'react'
export default function App() {
    const [n, setN] = useState(0)
    const h1Ref = useRef();
    useLayoutEffect(() => {
        h1Ref.current.innerText = Math.random().toFixed(2);
    })
    return (
        <div>
            <h1 ref={h1Ref}>{n}</h1>
            <button onClick={() => {
                setN(n + 1)
            }}>+</button>
        </div>
    )
}

十、DebugValue Hook

useDebugValue:用于将自定义Hook的关联数据显示到调试栏

如果创建的自定义Hook通用性比较高,可以选择使用useDebugValue方便调试