# Hooks

Hooks 是一种函数,该函数允许您从函数式组件 “勾住(hook into)” React 状态和生命周期功能。

有状态组件,就可以使用函数式组件来定义了。

# 类组件和函数组件

类组件:

import React, { Component } from 'react'
export default class Example extends Component {
    state = {
        count: 0
    }
    addCount() {
        this.setState({ count: this.state.count+1 })
    }
    render() {
        return (
            <div>
                <p>You clicked {this.state.count}</p>
                <button onClick={this.addCount.bind(this)}>Click me</button>
            </div>
        )
    }
}

函数组件:

import React, { useState } from 'react'
function Example() {
  //解构赋值 声明一个新的状态变量,我们将其称为 "count"
    const [count, setCount] = useState(0)
    return (
        <div>
            <p>You Click {count} Times</p>
            <button onClick={() => {setCount(count + 1);console.log(useState);}}>Click me</button>
        </div>
    )
}
export default Example
  • useState 返回一对:current state(状态)值和一个允许你更新它的函数
  • 所以用数组解构赋值进行自定义状态。
  • React 假定如果多次调用 useState ,则在每次渲染期间以相同的顺序执行

React 依赖于调用 Hooks 的顺序:(不同的状态的记录是通过useState的执行顺序

// ------------
// 第一次渲染
// ------------
useState('Mary')           // 1. 用'Mary'初始化名称状态变量
useEffect(persistForm)     // 2. 添加一个 effect 用于持久化form
useState('Poppins')        // 3. 使用 'Poppins' 初始化 surname 状态变量
useEffect(updateTitle)     // 4. 添加一个 effect 用于更新 title
// -------------
// 第二次渲染
// -------------
useState('Mary')           // 1. 读取 name  状态变量(忽略参数)
useEffect(persistForm)     // 2. 替换 effect 以持久化 form
useState('Poppins')        // 3. 读取 surname 状态变量(忽略参数)
useEffect(updateTitle)     // 4. 替换 effect 用于更新 title

# 使用State Hook

  • hooks在class中不起作用
  • 如果我们想在状态中存储多个不同的值,我们将调用 useState()多次
  • hooks需要一个一个的设置状态值,不能将前后的状态进行合并操作。

# 初始化state:

import { useState } from 'react';
function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);
  //使用useState(x)来初始化一个状态

# 读取state:

<p>You clicked {count} times</p>
//函数组件直接读取state值

# 更新state:

<button onClick={() => setCount(count + 1)}>
  Click me
</button>
//使用setXxx()来更新

# 使用Effect Hook

可以将 useEffect Hook 视为 componentDidMount、componentDidUpdate 和 componentWillUnmount 的组合。

通过使用Effect Hook ,您可以告诉 React 在渲染后要做些什么,React 将记住传递的函数(我们将把它称为 “effect” ),然后在执行DOM更新后调用它。(类似与类组件中this.setState的第二个回调参数)

  • 每次渲染后 useEffect 都会运行。(异步的不影响视图更新)
  • 组件内调用 useEffect,Hooks支持JavaScript闭包。
  • 可以使用多个useEffect,进行组件的逻辑分解。
import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

如果需要设置在卸载组件的时候调用的函数需要在useEffect传入的函数中return一个卸载时调用的函数:(即需要清理effects,如果不需要清理effects就不需要返回值)

  • 每次重新渲染,都会执行useEffect
  • 如果要运行 effect 并仅将其清理一次(在装载和卸载时),则可以将空数组([])作为第二个参数传递。
useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

# 通过跳过 Effects 来优化性能:

  • useEffect 可选的第二个参数是一个数组
  • 如果要运行 effect 并仅将其清理一次(在装载和卸载时),则可以将空数组([])作为第二个参数传递。
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
//如果count渲染前和渲染后都是一样的值,react会跳过这个effect
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, []); // Only re-run the effect if count changes
//只会执行一次effect,相当于类组件生命周期componentDidMount和componentWillUnmount

如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直持有其初始值

function Demo(){
  const [count,setCount]=useState(0)
  useEffect(()=>{
    setInterval(()=>{
      console.log(count)//每次都是初始值0
      setCount(count+1)
    },500)
  },[])//不依赖于任何值,只会执行一次
  return (<div>count:{count}</div>)//始终显示1
}

在这里插入图片描述

# 总结:

  • 每次渲染后 useEffect 都会运行。(异步的不影响视图更新)
  • 通过useEffect返回一个函数,来实现组件卸载unMount的生命周期(每次重新渲染都会执行useEffect,所以每次重新渲染也就都会执行return 的函数)
  • 通过useEffect传入第二个参数为一个数组,用于监听状态的变化,实现拦截优化执行useEffect
    • [state1,..,stateN],状态变化即执行当前的useEffect,否则不执行
    • [],仅在执行一次首次渲染时useEffect和最后卸载组件时useEffect return

# Hook规则

# 1】只在顶层调用Hook:
  • 不要在循环,条件或嵌套函数中调用 Hook 。 相反,总是在 React 函数的顶层使用 Hooks。
# 2】只在 React Functions 调用 Hooks:
  • 不要在常规 JavaScript 函数中调用 Hook
  • 在 React 函数式组件中调用 Hooks
  • 从自定义 Hooks 调用 Hooks

确保组件中的所有 stateful (有状态)逻辑在其源代码中清晰可见。

# 3】组件逻辑代码分割

单个组件中使用多个 State 或 Effect Hook:

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

React 如何知道哪个 state(状态) 对应于哪个 useState 调用呢?

  • React 依赖于调用 Hooks 的顺序
// ------------
// 第一次渲染
// ------------
useState('Mary')           // 1. 用'Mary'初始化名称状态变量
useEffect(persistForm)     // 2. 添加一个 effect 用于持久化form
useState('Poppins')        // 3. 使用 'Poppins' 初始化 surname 状态变量
useEffect(updateTitle)     // 4. 添加一个 effect 用于更新 title

// -------------
// 第二次渲染
// -------------
useState('Mary')           // 1. 读取 name  状态变量(忽略参数)
useEffect(persistForm)     // 2. 替换 effect 以持久化 form
useState('Poppins')        // 3. 读取 surname 状态变量(忽略参数)
useEffect(updateTitle)     // 4. 替换 effect 用于更新 title

// ...
# 4】打破规则实例:
// 🔴 我们在条件语句中使用Hook,打破了第一条规则
if (name !== '') {
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });
}
//导致hooks调用顺序发生改变

我们需要保证所有的Hook能正常执行。所以解决上面这个问题就需要把条件放在Hook中:

useEffect(function persistForm() {
  // 👍 我们不再违反第一条规则了
  if (name !== '') {
    localStorage.setItem('formData', name);
  }
});

# 使用useContext

组件共享状态传值

createContext(defaultValue)

  • 可选传入参数:默认value值
  • 返回值是一个context对象,里面有Provider、Consumer

useContext(contextObj)

  • 参数接收一个 context 对象(React.createContext 的返回值)
  • 返回值是provider提供的共享value
import React, { useState, createContext, useContext } from 'react'

const CountContext = createContext()
function Child() {
  //直接使用useContext接收context对象,获取其中的value值
    let count = useContext(CountContext)
    return (
        <div>{count}</div>
    )
}
function Parent() {
    const [count, setCount] = useState(0)
    return (
        <div>
            <p>You Click {count} Times</p>
            <button onClick={() => { setCount(count + 1); }}>Click me</button>
            <CountContext.Provider value={count}><Child /></CountContext.Provider>
        </div>
    )
}
export default Parent

也可以使用consumer

import React, { useState, createContext, useContext } from 'react'
//创建context对象
const CountContext = createContext()
function Child() {
    return (
        <div>{/*使用context对象的消费者,回调函数获取值*/}
            <CountContext.Consumer>{value => {return value}}</CountContext.Consumer>
        </div>
    )
}
function Parent() {
    const [count, setCount] = useState(0)
    return (
        <div>
            <p>You Click {count} Times</p>
            <button onClick={() => { setCount(count + 1); }}>Click me</button>
            <CountContext.Provider value={count}><Child /></CountContext.Provider>{/*使用context对象的提供者,传入value属性*/}
        </div>
    )
}
export default Parent

在这里插入图片描述

# 使用useReducer

使用JavaScript原生实现reducer(纯函数)

function countReducer(state,action){
  switch(action.type){
    case 'add':
      return state+1
    case 'sub':
      return state-1
    default:
      return state
  }
}

useReducer(callback,initialValue)

  • 第一个参数是一个回调函数,回调函数中传入形参state和action
  • 第二个参数是state的初始值
  • 返回值是一个数组,数组第一位是计算后的状态state,第二位是dispatch分发函数:[state,dispatch]
import React, { useReducer } from 'react'
function ReducerDemo() {
  //数组解构,第一个是改变后的状态,第二个是dispatch方法
    const [count, dispatch] = useReducer((state, action) => {
        switch (action) {
            case 'add':
                return state + 1;
            case 'sub':
                return state - 1;
            default:
                return state;
        }
    }, 0)
    return (<div>
        <h2>现在的分数是{count}</h2>
        <button onClick={() => dispatch('add')}>Increment</button>
        <button onClick={() => dispatch('sub')}>Decrement</button>
    </div>)
}
export default ReducerDemo

在这里插入图片描述

# 结合使用useReducer和useContext

useReducer和useContext可实现小型的Redux

import React, { createContext, useContext, useReducer } from 'react'
let ColorContext = createContext()//创建context对象
function ShowArea() {
    let color = useContext(ColorContext)//使用context对象中的value
    return <div style={{ color: color }}>字体颜色是{color}</div>
}
function Buttons() {
  //使用useReducer进行计算state
    let [state, dispatch] = useReducer((state, action) => action, 'blue')
    return (
        <ColorContext.Provider value={state}>
            {/*使用context对象的Provide动态传入value*/}
            <button onClick={() => dispatch('red')}>红色</button>
            <button onClick={() => dispatch('yellow')}>黄色</button>
            <ShowArea />
        </ColorContext.Provider>)
}
export default Buttons

在这里插入图片描述

# 使用useMemo解决子组件重复执行的问题

shouldComponentUpdate组件更新之前进行状态对比,判断子组件是否需要更新。

useMemo类似于shouldComponentUpdate阻止子组件渲染。

useMemo(callback,[state])用于缓存状态或属性

  • 第一个参数是需要每次渲染时执行的函数
  • 可选的第二个参数是一个数组,数组中传入需要监听变化的状态state
    • 如果该状态发生变化,则执行函数,否则不执行。
    • 如果是空数组,不依赖于任何状态,则第一次渲染时才进行缓存,之后不再触发缓存。
import React, { useState, useMemo } from 'react'
function Parent() {
    const [state1, setState1] = useState('x')
    const [state2, setState2] = useState('y')
    return (<>
        <button onClick={() => { setState1(new Date().getTime()) }}>按钮1</button>
        <button onClick={() => { setState2(new Date().getTime()) }}>按钮2</button>
        <Child name={state1}>{state2}</Child>
    </>)
}
function Child({ name, children }) {
    function ChangeState1(name) {
        console.log('xxxxxx');
        return name
    }
    return (<>
        <div>{ChangeState1(name)}</div>
      //此时我们不管是点击按钮1还是按钮2,都会重复执行打印'xxxxxx',因为子组件都会都会重新进行渲染,执行函数
        <div>{children}</div>
    </>)
}
export default Parent

使用useMemo来根据状态变化优化子组件重复渲染,提升性能(类似shouldComponentUpdate)

import React, { useState, useMemo } from 'react'
function Parent() {
    const [state1, setState1] = useState('x')
    const [state2, setState2] = useState('y')
    return (<>
        <button onClick={() => { setState1(new Date().getTime()) }}>按钮1</button>
        <button onClick={() => { setState2(new Date().getTime()) }}>按钮2</button>
        <Child name={state1}>{state2}</Child>
    </>)
}
function Child({ name, children }) {
    function ChangeState1(name) {
        console.log('xxxxxx');
        return name//useMemo缓存的是name这个属性值
    }
    return (<>
        <div>{useMemo(() => ChangeState1(name), [name])}</div>
      //使用useMemo包装子组件渲染时同时会执行的函数,第二个参数是需要监听变化的状态变量。
        <div>{children}</div>
    </>)
}
export default Parent

此时我们点击'按钮1',依然会打印‘xxxxxx’,因为传入子组件的name发生了改变。此时我们再点击'按钮2'就不会执行打印‘xxxxxx’,虽然传入子组件的状态children发生改变,但name没有发生改变;

# 使用useRef获取DOM和保存变量

useRef(initialValue)

  • 返回一个可变的ref对象,其 .current 属性被初始化为传入的参数(initialValue)。
  • 返回的ref对象放在需要获取dom元素的标签上,即可使用.current
  • .current可以进行保存变量

# 获取DOM值:

import React, { useRef,createRef } from 'react'
function Example6() {
    let inputRef = useRef(null)
  //let inputRef = createRef(null)
  //使用createRef效果一模一样
    function getInput() {
        console.log(inputRef.current.value)//绑定ref的输入框的value值
    }
    return (
        <>
            <input ref={inputRef} type="text" name="" id="" />
            <button onClick={getInput}>按钮</button>
        </>
    )
}
export default Example6

# 保存变量:.current保存变量

import React, { useState, useRef, createRef, useEffect } from 'react'
function Example6() {
    let inputRef = useRef()
    // let inputRef = createRef()
    const [text, setText] = useState('xxx')
    useEffect(() => {
        inputRef.current = text//使用.current来保存变量
        console.log('inputRef:' + inputRef.current)
    })
    return (
        <>
            <input ref={inputRef} type="text" value={text} onChange={(e) => setText(e.target.value)} />
        </>
    )
}
export default Example6

# createRef 与 useRef 的区别

createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。

例子:

import React, { useState, useRef, createRef } from 'react'
function Diff() {
    const [renderIndex, setRenderIndex] = useState(0)
    const UseRef = useRef()//useRef默认传入null
    if (!UseRef.current) {//所以会执行一次,每次重新渲染,使用之前的变量,所以.current就不为空,保存了之前的变量。
        UseRef.current = renderIndex
        console.log(UseRef.current)//1
    }
    const CreateRef = createRef()//createRef不接受参数
    if (!CreateRef.current) {//并不会保存之前变量,而是每次重新渲染再次执行createRef创建空的.current
        CreateRef.current = renderIndex
    }

    return (<>
        <p>renderIndex:{renderIndex}</p>
        <p>UseRef.current:{UseRef.current}</p>
        <p>CreateRef.current:{CreateRef.current}</p>
        <button onClick={() => setRenderIndex(renderIndex + 1)}>渲染</button>
    </>)
}
export default Diff

在这里插入图片描述

# 使用useCallback缓存方法

useCallback(fn,[state]):用于缓存方法

  • 传入需要缓存的方法或函数(区别于useMemo)。
  • 返回一个 memoized 回调函数。
useCallback(fn, deps) 相当于 useMemo(() => value, deps)

useMemouseCallback 接收的参数都是一样,第一个参数为回调 第二个参数为要依赖的数据

  • 如果第二个参数是空数组,不依赖于任何状态,则只有第一次渲染组件时进行缓存,之后不再缓存。

共同作用:

  • 仅仅 依赖数据 发生变化, 才会重新计算结果,也就是起到缓存的作用。

两者区别:

  • useMemo 计算结果是 return返回的值, 主要用于 缓存计算结果的值 ,应用场景如: 需要 计算的状态
  • useCallback 计算结果是 函数, 主要用于 缓存函数,应用场景如: 需要缓存的函数,因为函数式组件每次任何一个 state 的变化 整个组件 都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。

# 自定义Hooks函数

自定义Hooks函数是由各种hook API进行融合的函数

例如:下面这个实时获取页面大小的Hooks函数

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

function useWinSize() {
  //定义一个状态
    const [size, setSize] = useState({
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
    })
    //使用useCallback进行缓存方法,(当数组中的依赖,变化时才再次触发更新)
    const onResize = useCallback(() => {
        setSize({
            width: document.documentElement.clientWidth,
            height: document.documentElement.clientHeight
        })
    }, [])//空数组,不依赖于任何值,只有第一次渲染时进行缓存一次
    useEffect(() => {//组件每次挂载时,进行执行
        window.addEventListener('resize', onResize)
        return () => {//当组件卸载时,进行清理
            window.removeEventListener('resize', onResize)
        }
    }, [])//空数组,不依赖于任何值,只有第一次渲染时,执行一次生命周期
    return size//返回修改后的状态
}
function Example() {
    const size = useWinSize()//使用自定义Hooks函数
    return (
        <div>页面的size是{size.width}X{size.height}</div>
    )
}
export default Example

在这里插入图片描述

Last Updated: 8/1/2021, 1:43:20 PM