Visual LabINTERACTIVE LEARNING
框架原理第 7 / 7 章

Hooks 体系

系统学习状态、副作用、引用、缓存和并发相关 Hooks。

React组件Redux
进阶预计 58 分钟查看源文 ↗

hooks

提示

为什么需要hooks?

  1. 原来函数组件不行:因为原本的函数组件,每次渲染时,函数都会重新执行(相当于render函数),所以函数内无法保存自己的状态数据,所以需要hooks来实现函数内保存状态。而原本的class component渲染只需要render函数重新执行,所以可以保存状态,同时函数组件也没有生命周期,所以需要hooks来进行补充
  2. 类组件不行:类组件逻辑复杂,不好理解,this指向不明,成为学习的障碍,组件状态复用困难

提示

注意事项:

  1. 只能在函数组件/自定义hook调用hook
  2. 只能在函数最外层调用hook,不能在函数内块代码(if、for、循环)调用

useState

  • 参数-- 初始化值 ,默认为undefined
  • 返回值 -- 数组,元素1,当前状态值,元素2 设置状态的函数
import { useState } from 'react'

function Counter(props){
  const [ count,setCount ] = useState(0) // 参数-初始化值 ,默认为undefined
  // 返回值 -- 数组,元素1,当前状态值,元素2 设置状态的函数
}

useEffect

主要用于副作用代码,默认情况下会在每次组件渲染完成会自动回调,如操作dom/网络请求/事件监听

也就是不会阻塞渲染,内容显示后才会执行这个回调

  • 参数 Setup: ()=>cleanUp|undefined,dependence
import { useEffect } from 'react'

function Counter(props){
   useEffect(()=>{
   // 副作用操作,默认情况下会在渲染完成时执行
   // 返回一个清理函数 模拟componentWillUnmount 生命周期
   },[]) 
   // 第二个参数是依赖项数组,只有依赖赖项发生改变时重新执行
   // 空数组表示仅执行一次 模拟componentDidMount
   // 不传表示每次渲染都执行 模拟 componetDidUpdate
}

一个函数组件可以有多个useEffect,按照顺序执行,并可以抽取成自定义hook,按照用途组织代码

提示

  1. 渲染时先执行setup,然后存储cleanUp
  2. 卸载时执行cleaUp
  3. dependence依赖数组可以控制更新的触发 模拟生命周期: componentWillUnmount: 返回的清理函数 componentDidMount: dependence 空数组,第一次渲染运行 componetDidUpdate: dependence不传参数,每次运行

useContext

用于获取祖先组件注入的上下文

创建上下文
import { createContext } from 'react'

export const userContext = createContext()
注入上下文
import { userContext } from './context'
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './pages/App';
import { Provider } from 'react-redux'
import store from './store'
import { HashRouter } from 'react-router-dom'

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
        <userContext.Provider value={{name:'test'}}>
            <Suspense fallback={<h3>loading....</h3>}>
                <HashRouter>
                    <App />
                </HashRouter>
            </Suspense>
        </userContext.Provider>
);
使用useContext获取上下文内容
import React, { memo, useContext } from 'react'
import { userContext } from '../context'

const Hooks = memo(() => {
    const user= useContext(userContext)
    return (
        <div>
            <div>Hooks</div>
            <div>{user.name}</div>
        </div>
    )
})

export default Hooks

提示

  1. 获取到的上下文需在祖先组件中进行注入
  2. 上下文发生变化,使用的组件会触发重新渲染
  3. 注入的组件和使用的组件中间的组件,使用了memo来包裹优化情况下,不会触发重新渲染,也不会影响上下文的传递

useCallback

参数1: fn

参数1: depandences

提示

useCallback并不能节省函数定义时的性能开销,也就是说每次函数组件重新渲染的时候,重新执行useCallback,虽然返回的是同一个函数,但是作为参数传递的函数,仍然每次都是重新定义了的,这里的性能没有优化, 优化的地方是,对于使用了useCallback返回函数作为props的子组件,不会每次都触发重新渲染(因为函数是相同的) useCallback在两种情况下是有优化的:

  1. 把一个函数fn传递给memo包裹的子组件,在子组件中使用fn,这时可以避免子组件每次都渲染
  2. 在别的hook中使用了fn,fn作为一个依赖项 总结来说: 当一个函数fn,作为另一个地方的依赖项时,为了避免另一个地方每次都被重新渲染,就要使用useCallback缓存该函数fn 如果每个函数都使用useCallback,会导致处理hook的逻辑变多,属于反向优化
使用
import React, { memo, useCallback, useState } from 'react'

const SubmitBtn = memo((props) => {
    const { handleSubmit } = props
    return (
        <div onClick={handleSubmit}>+1</div>
    )
})
const Hooks = memo(() => {
    const [count,setCount ] = useState()
    const handleSubmit = useCallback(() => {
        console.log('handleSubmit')
        setCount(count+1)
    }, [count])
    return (
        <div>
            <div>Hooks</div>
            <SubmitBtn handleSubmit={handleSubmit}></SubmitBtn>
        </div>
    )
})

export default Hooks
使用ref 进一步优化

useRef返回一个固定的对象,但是其current值可被每次更新,这样可以在回调函数中每次获取最新的值,避免闭包陷阱以及生成新的函数,就不会不会每次更新子组件

import React, { memo, useCallback, useContext, useRef, useState } from 'react'

const SubmitBtn = memo((props) => {
    const { handleSubmit } = props
    return (
        <div onClick={handleSubmit}>+1</div>
    )
})

const Hooks = memo(() => {
    const [count, setCount] = useState()
    const countRef = useRef()
    countRef.current = count
    const handleSubmit = useCallback(() => {
        setCount(countRef.current + 1)
    }, [])
    return (
        <div>
            <div>Hooks</div>
            <SubmitBtn handleSubmit={handleSubmit}></SubmitBtn>
        </div>
    )
})

export default Hooks

提示

这里要注意【闭包陷阱】 如果没有传入依赖参数【上面代码中的 [count] 】 当传入的fn,依赖了某个外部变量时(上面代码中的count),由于useCallback的缓存作用,每次返回的都是第一次运行时传入的函数,也就是说,count取到的值也是第一次运行的值 (形成了闭包时,取到的count就是固定的) 如有传入依赖,依赖改变时,useCallback会返回一个新的函数,也就以当前的count值,生成一个新的闭包

useMemo

const  value =  useMemo(fn, dependencies)
// value 是 fn的执行结果,dependencies不变时,缓存fn的执行结果,

可以使用useMemo实现useCallback

const useCallback = (fn, dependencies) => {
  return useMemo(() => fn, dependencies)
}

提示

useMemo优化的点:

  1. 避免fn每次渲染都被重新执行,优化的是本组件fn执行的开销 -- 主要用途
  2. 当需要传递对象类型的数据给子组件时,避免每次重新渲染定义新的对象导致子组件被重新渲染,这个和useState重复了

useRef

组件的生命周期中,对象是的内存地址不变,主要用途是用来获取dom

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

const Hooks = memo(() => {
    const countRef = useRef()
    return (
       <div>
           <div ref={countRef}>Hooks</div>
       </div>
    )
})

export default Hooks

useLayoutEffect

在内容更新到屏幕上前执行,会阻塞渲染。比useEffect执行时机更前一点

自定义hook

多个组件相同的的逻辑,可以单独抽取出来,实现代码复用

useSelector 和 useDispatch

redux中的hook

useDispatch获取redux中的store的dispatch函数,仅此而已

useSelector的回调函数相当于原来connect高阶函数的 mapStateToProps

useSelector有2个参数,第一个参数是mapStateToProps,第二个参数是新旧state的比较方法,决定是否更新

import React, { memo } from 'react'
import { useDispatch, useSelector,shallowEqual } from 'react-redux'
import { addNumber } from '../store/reducer'

const Hooks = memo(() => {
    const { count } = useSelector((state) => ({ count: state.home.count }),shallowEqual)
    const dispatch = useDispatch()
    function addCount(num) {
        dispatch(addNumber(num))
    }
    return (
        <div>
            <div> count: {count}</div>
            <button onClick={e => addCount(1)}>+</button>
        </div>
    )
})

export default Hooks

提示

性能优化 默认情况下不传入第2个参数,会导致useSelector监听整个store的变化 这个时候传入第二个参数,可以浅层比较前后两个state是否变化,从而决定是否更新当前的组件

useTransition

可以降低某些操作的优先级

const [pending,startTransition] = useTransition()
   startTransition(()=>{
    
   })
   

pending,表示回调函数是否在执行,startTransition里的回调函数是否已经执行完成

startTransition的回调函数会被降低优先级,在其他更新完成后才去调用,避免一些耗时操作去影响界面渲染的更新