函数组件---React Hooks

781 阅读8分钟

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

Hooks的使用规则

只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。

只在 React 函数中调用 Hook

不要在普通的 JavaScript 函数中调用 Hook。你可以:

✅ 在 React 的函数组件中调用 Hook

✅ 在自定义 Hook 中调用其他 Hook

官方还提供了 linter插件 用来强制hooks的使用规则。

useState useEffect useRef

useState

返回一个 state,以及更新 state 的函数。

在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。

setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。

const ClickCount = () => {
  const [ count, setCount ] = useState(0)
  const [ num, setNam ] = useState(1)
  const handleClick = () => {
    setCount(count+1)
    setNam(num+2)
  }
  return (
    <div>
      <p>count:{count}---num:{num}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  )
}

如果异步更新状态时,类似类组件中setState更新状态的方式,useState也提供了两种更新状态的方式:

  1. 直接使用setCount()
setCount(count+1)
  1. 使用setCount函数式更新
setCount((prevCount) => {
    return prevCount + 1
})

useEffect

该 Hook 接收一个包含命令式、且可能有副作用代码的函数。 在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。 使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。 useEffect相当于类组件中的三个生命周期函数分别是:componentDidMountcomponentDidUpdatecomponentWillUnmount

useEffect传入两个参数:

  1. 第一个参数是一个函数用来执行某些操作。return一个函数用来清除定时器或订阅等资源。--- 模拟componentWillUnmount生命周期
  2. 第二个参数是一个数组,用来设置useEffect函数执行的依赖项,控制useEffect函数是否被重新执行。--- 模拟componentDidUpdate 生命周期 组件刚加载时就会执行 useEffect ,相当于 componentDidMount 生命周期。
 const Fetch = () => {
  const [result, setResult] = useState(null)

    useEffect(()=> {
      fetch('./index.html').then(response=>response.text()).then(res=>{
        // 此时会输出两次,组件刚初始化时会触发一次,
       //当状态改变了会再触发一次分别相当于【componentDidMount和componentDidUpdate】
              
        console.log(111)
        setResult(res)
      })
    }, []) 
    // 如果第二个参数为空数组会使useEffect执行一次,【componentDidMount】
    // [result] 会执行两次取决于result的状态
    // [a,b]有一个状态发生改变都会重新执行



  return (
    <div>
      result: {result}
    </div>
  )
}

const Fetch1 = () => {
  const [count, setCount] = useState(0)
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count+1)
    }, 500);

    // 每次依赖项发生变化后会执行

    return () => {
      //清除上次的定时器
      clearInterval(timer)
    }
  },[count])
  // 如果不添加依赖,会造成状态得不更新,使得react中的capture value不能检测到状态的更新,就不会重新执行useEffect【此时在这个例子中就是定时器不能重复执行】,
  // 如果添加依赖,就会造成useEffect的重复的生成回调函数
  // 解决:useRef

  return (
    <div>
      count: {count}
    </div>
  )
}

useRef

useRef的用法主要有两个:

  1. 用来标记DOM
  2. 用来保存数据 用来标记DOM

首先通过const btnRef = useRef(null)声明一个标记名称,然后在需要标记的DOM上通过ref={btnRef}做标记,最后通过btnRef调用。

const Fetch1 = () => {
  const [count1, setCount1] = useState(0)
 
  const btnRef = useRef(null)
  // const handleOnclick = () => {
  //   setCount1(count1+1)
  // } 
  useEffect(() => {
    console.log(222)
    
    const handleOnclick = () => {
      setCount1(count1+1)
    } 
    btnRef.current.addEventListener('click',handleOnclick, false)

    return () => btnRef.current.removeEventListener('click', handleOnclick,false)
    
  },[count1])
  return (
    <div>
       count1: {count1}
      <hr></hr>
      {/* <button onClick={handleOnclick}>+1</button> */}
      <button ref={btnRef}>+1</button>

    </div>
}

用来保存数据

在一个组件中有什么东西可以跨渲染周期,也就是在组件被多次渲染之后依旧不变的属性?第一个想到的应该是state。没错,一个组件的state可以在多次渲染之后依旧不变。但是,state的问题在于一旦修改了它就会造成组件的重新渲染。

那么这个时候就可以使用useRef来跨越渲染周期存储数据,而且对它修改也不会引起组件渲染。

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

function Counter() {
    const [count, setCount] = useState(0);
    const timer = useRef(null)

    useEffect(() => {
        console.log('usesEffect');  // 只会在加载组件的时候输出一次,使用useRef不会引起组件的重新渲染
        timer.current = setInterval(() => {
            setCount(count => count + 1)
        }, 1000)
        return () => {
            clearInterval(timer.current)
        };
    }, []); 
    
    return (
        <div>
            <p>count: {count}</p>
        </div>
    )
}

export default Counter

memo useMemo useCallback

memo

memo用于在函数式组件中优化功能,通过调用子组件时传递给子组件的数据前后两次是否相同,来决定子组件是否重新渲染。

向子组件传递简单数据类型的值

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

const Parent = () => {
    const [count, setCount] = useState(0)
    const [clickCount, setClickCount] = useState(0)
    return (
      <div>
        {<!-- 点击按钮不会改变传入子组件的数据 -->}
        count: {count}
        <button onClick={()=>{
          setCount(count+1)
        }}>+1</button>
  		
        {<!--点击按钮,改变传入子组件的数据 -->}
        <button onClick={() => {
          setClickCount(clickCount + 1)
        }}>GET CURRENT TIME</button>
        <Child count={clickCount}/>  
      </div>
    )
  }
  const Child = memo((props) => {
      // console.log(props)
      console.log(123)
      const date = new Date()
      return (
      <div>
          <p>当前时间:{date.getHours()}:{date.getMinutes()}:{date.getSeconds()}</p>
      </div>
      )
  }
    // ,(prev, next) => { // memo 的第二个参数前后两种状态,如果相等组件不会更新【+1按钮】,如果不相等组件更新【GET CURRENT TIME】
    //     console.log(prev,next)
    //     console.log(prev.count === next.count)
    //     return prev.count === next.count // 和不写效果一样
    // }
  )
   
  export { Parent }

传递复杂类型的值

const Parent = () => {
    const [count, setCount] = useState(0)
    const [clickCount, setClickCount] = useState(0)
   
   // 将需要传递给子组件的数据,包裹在一个对象中。
    const timeOption = {
      clickCount
    }

    return (
      <div>
        count: {count}
        <button onClick={()=>{
          setCount(count+1)
        }}>+1</button>
  
        <button onClick={() => {
          setClickCount(clickCount + 1)
        }}>GET CURRENT TIME</button>
        {<!-- 给子组件传递一个复杂类型的数据 -->}
        <Child count={timeOption}/>  
      </div>
    )
  }
  const Child = memo((props) => {
    console.log(123)
    const date = new Date()
    return (
      <div>
        <p>当前时间:{date.getHours()}:{date.getMinutes()}:{date.getSeconds()}</p>
      </div>
    )
  }
export { Parent }

如果向子组件传递的是一个对象类型的数据,虽然父组件中clickCount没有发生改变, 但每次父组件中的clickCount没有发生改变但是timeOption对象每次都会重新生成。

即使子组件使用了memo,由于接收到的是对象类型的数据,由于对象是引用类型, 在子组件中得到的prev.count next.count的地址不同也会造成子组件的更新。

解决:

  • 方法一:可以使用prev.count.clickCount 和 next.count.clickCount来return(因为对象里的这两个值是相同的,且是简单数据类型)
  • 方法二:使用useMemo()

useMemo

useMemo 接收两个参数,第一个参数为回调,第二个参数为要依赖的数据。 在useMemo的回调中return一个数据结果,useMemo 会将return 的结果进行缓存。

const Parent = () => {
    const [count, setCount] = useState(0)
    const [clickCount, setClickCount] = useState(0)

    const timeOption = useMemo(() => {
      return {clickCount}
    }, [clickCount])


    return (
      <div>
        count: {count}
        <button onClick={()=>{
          setCount(count+1)
        }}>+1</button>
  
        <button onClick={() => {
          setClickCount(clickCount + 1)
        }}>GET CURRENT TIME</button>
        <Child count={timeOption}/>
  
      </div>
    )
  }
  const Child = memo((props) => {
    console.log(props)
    console.log(123)
    const date = new Date()
    return (
      <div>
        <p>当前时间:{date.getHours()}:{date.getMinutes()}:{date.getSeconds()}</p>
      </div>
    )
  }
)

此时:在父组件中的clickCount没有发生改变, 使用useMemo()可以将这个结果缓存, 在父组件更新状态时不会每次都重新生成timeOption, 因此在memo()的第二个参数中通过比较前后两次的props是相同的, 从而不会触发子组件的更新。

useCallback

useCallback 接收两个参数,第一个参数为回调,第二个参数为要依赖的数据。

useCallback 计算结果是 函数, 主要用于 缓存函数。

     // 当在子组件的输入框中输入内容时,在父组件中,会改变父组件中的text属性,并重新渲染父组件。
     // 因此会重重复向子组件传递handleOnChange,在子组件中会重复打印props.
     // 当使用useCallback时,向子组件传递的handleOnChange是同一个事件,
     // 因此会被缓存,在子组件中不会重复打印父组件传递的方法
const Parent = () => {
    const [text, setText] = useState('')
  
    const handleOnChange = useCallback((e) => {
      setText(e.target.value)
    }, [])
  
    return (
      <div>
        <p>text: {text}</p>
        <Child onChange={handleOnChange}/>
  
      </div>
    )
  }
  
  const Child = memo((props) => {
    console.log(props)
    console.log(123)
    return (
      <div>
        <input type="text" onChange={props.onChange} />
      </div>
    )
  }
)
export { Parent } 

总结

一个 state 的变化整个组件都会被重新刷新,一些函数和数据是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。

memouseMemouseCallback都能用来提高性能,它们的主要区别是:

  • memo 实现组件状态改变前后props是否发生改变,决定组件是否重新加载。它站在整个组件上。
  • useMemo 实现属性的缓存,针对某一个属性的改变,确定子组件是否重新加载,而且它的返回值是一个数值。
  • useCallback 实现方法的缓存,针对某一个方法的改变,确定子组件是否重新加载,它的返回值是一个方法。

useReducer useContext

useReducer useContext

使用 useReducer useContext实现组件之间的数据传递。

import React, { useReducer, useContext } from 'react'

// 创建全局上下文
const Ctx = React.createContext(null)

 
// 创建reducer,接收action并更改状态
const reducer = (state, action) => {
    switch (action.type) {
        case 'ADD':
            return state + 1;
        case 'SUB':
            return state - 1;
        default:
            return state;
    }
}
const Child = () => {

    //如果在子组件中引入useReducer(),可以供子组件使用并修改子组件中的状态

    //const [count, dispatch] = useReducer(reducer, 10)

    //接收上下文中传递的count,dispatch.并在子组件中通过dispatch发送action

    const [count, dispatch] = useContext(Ctx)
    return (
        <div>
            child:
            count: {count}
            <button onClick={() => {dispatch({type: 'ADD'})}}>+1</button>
            <button onClick={() => {dispatch({type: 'SUB'})}}>-1</button>

        </div>
    )
}

const Parent = () => {
    
    //父组件接收子组件更新之后的状态
   
    const [count] = useContext(Ctx)

    return (
        <div>
            parent:{count}
            <Child />
        </div>
    )
}

function App1() {
//    声明一个状态count,并附初始值为20,
//    并声明发送action的方法dispatch,
//    同时指明处理action的reducer   

  const [count, dispatch] = useReducer(reducer, 20)
    return (
     // 使用创建的上下文中的Provider,指定上下文的范围,
     // 同时传递count状态和发送action的dispatch方法,以供子组件使用 
  
    <Ctx.Provider value={[count, dispatch]}>
        <div className="App">
            <Parent />
        </div>
    </Ctx.Provider>
    
  );
}

export default App1;

自定义Hooks

自定义hooks

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

import { useEffect, useState } from 'react';

export const useWindowResize = () => {
    const [width, setWidth] = useState('0px')
    const [height, setHeight] = useState('0px')
    useEffect(() => {
        setWidth(document.documentElement.clientWidth + 'px')
        setHeight(document.documentElement.clientHeight + 'px')
    
      }, [])
    
      useEffect(() => {
        const handleResize = () => {
          setWidth(document.documentElement.clientWidth + 'px')
          setHeight(document.documentElement.clientHeight + 'px')
        }
        window.onresize = handleResize
        return () => {
          window.onresize = null
        }
      }, [])

      return [width,height]
}


import React from 'react'

import { useWindowResize } from './hooks'

const Parent = () => {
  const [width, height] = useWindowResize() // 也可以传递参数,
  return(
    <div>
      <p>size: {width}*{height}</p>
    </div>
  )
}

更多 Hooks

  1. react-redux中提供的:useSelector useDispatch
  2. react-router-dom中提供的:useHistory useLocation useParams useRouteMatch