React Hook

262 阅读7分钟

React Hook

常用的一些hook,以及hook的源码解析

学习中的一些拙劣看法,如有错误,欢迎指正~~

useState

本质上也利用了useReducer

简单使用

const [state, setState] = useState({
    age: 18,
    name: 'daniel',
})

setState((s) => ({
  ...s,
  age: 20
}))

<div>{state.name}: {state.age}</div>

useState源码

源码解析

自定义简单实现

// 本质上useState也是用useReducer实现的

let hooksState = []; // 用来保存hooks
let hooksIndex = 0;  // 用来指定下标

function useState(initialState) {
    // 初始值可能是一个函数
    let initState = typeof initialState === 'function' ? initialState() : initialState
    
    // 把老的值取出来,如果没有,就用 新的值
    hooksState[hooksIndex] = hooksState[hooksIndex] || initState
    
    // 定义一个内部变量保存当前的值
    const currentIndex = hooksIndex;
    
    const setState = (newState) => {
        if (typeof newState === 'function') {
            newState = newState(hooksState[currentIndex])
        }
        hooksState[currentIndex] = newState
        
        // 更新
        render()
    }
    
    // 让hooksIndex 加1,用于下一个hook
    return [hooksState[hooksIndex++], setState];
}


useEffect

副作用,在浏览器渲染完成之后执行,React 更新 DOM 之后运行一些额外的代码

简单使用

// 如果没有依赖项,会在每一次渲染后都会执行
useEffect(() => { console.log('每一次都执行') })

// 只会首次渲染执行一次
useEffect(() => {
    console.log('初始化渲染执行一次')
},[])

// 在依赖项改变之后才执行, 包括首次渲染也会执行
useEffect(() => {
    console.log('num 改变就执行',‘初始化也会执行’)
    
    // 清楚一些副作用,返回一个函数
    return () => {
        // 页面卸载的时候执行,相当于componentWillUnmount 如果有依赖项,依赖项改变就执行
            console.log('清楚effect')
    }
    
},[num])

useEffect源码

源码解析

自定义简单实现

function useEffect(callback, deps) {
    if (hooksState[hooksIndex]) {
    
    const [ lastDestroyFunction, lastDeps ] = hooksState[hooksIndex]
    
    // 对比依赖项有没有变化
    cosnt allDepsChange = deps && deps.every((item, index) => item === lastDeps[index])
    if (allDepsChange) {
        hooksIndex++
    }else {
       lastDestroyFunction && lastDestroyFunction()
       setTimeout(() => {
          const destroyFunction = callback()
          hooksState[hooksIndex++] = [destroyFunction, deps]
       })
    }  
          
    } else { // 第一次渲染
      
      setTimeout(() => {
          const destroyFunction = callback()
          hooksState[hooksIndex++] = [destroyFunction, deps]
      })
    }
}


useRef

useRef 返回一个可变的对象,其current 属性被初始化为传入的参数,它可以很方便地保存任何可变值

当我们使用useState的时候,setState之后,并不能立刻拿到最新的值,因为是一个闭包,拿到的是上一次的值,只有在页面再次渲染之后才能拿到最新值,这个时候,可以利用useRef 的current 将值保存,就能立刻拿到最新的值了

demo

const Counter = () => {
  const [num, setNum] = useState(0);
  const numRef = useRef(0)

  const changeNum = () => {
    setNum(num + 1)
    numRef.current = numRef.current + 1;
    
    console.log(num, '----->num') // 第一点击是 0
    console.log(numRef.current, '----->numRef.current')  // 第一次点击是 1
  }

  return (
    <>
      <h2>num: {num}</h2>
      <h2>Ref {numRef.current}</h2>
      <button onClick={changeNum}>点击num</button>
    </>
  )
}

useRef源码

源码解析

自定义简单实现

function useRef(initialState) {
   hooksState[hooksIndex] = hooksState[hooksIndex] || { current: initialState }
   return hooksState[hooksIndex++]
}

useContext

当有的值需要跨组件传值的时候,比如说父组件给子组件的子组件传值,一种方法就是利用props依次往下传,还有就是利用context,创建一个context,然后利用provider提供值,利用consumer消费值,也可以利用useContext 传入context 实例获取到context上的值

看一个简单使用

创建一个context 新增一个ctx.jsx文件

import React from 'react'
export const contextTheme = React.createContext(null)

父组件 给孙组件传 color 和 setColor

// 父组件
import ChildTest from './ChildTest'
import { contextTheme } from './ctx.jsx'

const DrawingBoard: React.FC = () => {
  const [ color, setColor ] = useState('red')
  return (
    <div>
       <contextTheme.Provider value={{ color, setColor  }}>  // 利用context传数据
          <ChildTest ref={childRef} />
       </contextTheme.Provider>
      </div>
    </div>
  )
}

export default DrawingBoard


// 子组件 
import ChildChildTest from './ChildChildTest'

const ChildTest: React.FC<Props> = (props) => {
  return (
    <div>
      <ChildChildTest />
    </div>
  )
}

子组件的子组件

方式一: 利用 contextTheme.Consumer 来消费

import { contextTheme } from './ctx'  // 引入contextTheme
interface Props {}

const ChildChildTest: React.FC<Props> = (props) => {

  const changColor = (setColor) => {
    setColor('blue')
  }

  return (
    <div>
      <contextTheme.Consumer>   // 消费父组件的 color 和 setColor
        {
          ({color, setColor}) => (
            <>
              <span> 孙子组件 {color}</span>
              <Button onClick={() => changColor(setColor)} style={{ background: `${color}` }} >孙组件按钮</Button> 
          </>
          )
        }
      </contextTheme.Consumer>

    </div>
  )
}
export default ChildChildTest;

孙组件 方式二 (写法比方式一简单灵活)

利用useContext 来消费

import React, from 'react'
import { contextTheme } from './ctx'

interface Props {}

const ChildChildTest: React.FC<Props> = (props) => {

   const values = useContext(contextTheme)

   const { color, setColor } = values;

   const changColor = () => {
     setColor('blue')
   }

  return (
    <div>
      <span> 孙子组件 {color}</span>
      <Button onClick={changColor} style={{ background: `${color}` }} >孙组件按钮</Button> 
    </div>
  )
}
export default ChildChildTest;

useContext源码

源码解析

自定义简单实现

function useContext(context) {
   return context._currentValue
}

createContext源码

源码解析

自定义简单实现

function createContext(initialValue = {}) {
   const context = { Consumer, Provider }
   
   function Provider(props) {
       context._currentValue = context._currentValue || initialValue;
       Object.assign(context._currentValue, props.value)
       return props.children
   }
   
   function Consumer(props) {
       return props.children(context._currentValue)
   }
   
   return context
}

useCallback

当父组件渲染的时候,里面的子组件也会被渲染一遍,为了避免这些不必要的渲染,我们可以使用React.memo 来包裹子组件,当子组件的props改变的时候才会进行渲染,注意: react.memo是进行浅比较,所以引用类型的props无法使用它来优化,来看一个小例子

// 父组件
const Counter = () => {
  const [num, setNum] = useState(0);
  console.log('父组件render')
  const changeNum = () => {
    setNum(num + 1)
  }
  return (
    <>
      <h2>父组件</h2>
      <h2>{num}</h2>
      <button onClick={changeNum}>点击num</button>    // 父组件点击会触发子组件渲染
      <Count num={1} />  
      <Count num={{}} />  // 这个会渲染两次
    </>
  )
}


// 子组件
import React from 'react';
const Count = (props) => {
  console.log('子组件render')  // 当props.num 是引用类型的时候,React.memo无效, 还是会打印两次
  return (
    <div>
      <div style={{ background: 'red' }}>{`父组件给子组件传的值: ${props.num}`}</div>
    </div>
  );
}
export default React.memo(Count);

引出useCallBack 返回一个 memoized 回调函数。该回调函数仅在某个依赖项改变时才会更新

useCallBack(fn, [dep]) 相当于 useMemo(() => fn, [dep])

看下面的例子

// 子组件

const Count = (props) => {
  const { testChange } = props;
  console.log('子组件render')
  return (
    <div>
      <div style={{ background: 'red' }}>{`父组件给子组件传的值: ${props.num}`}</div>
      <button onClick={() =>  testChange('啦啦啦')}>子组件传值给父组件</button>
    </div>
  );
}
export default Count;

// 父组件
const Counter = () => {

  const [num, setNum] = useState(0);
  const [age, setAge] = useState(0);
 
  console.log('父组件render')

  const changeNum = () => {
    setNum(num + 1)
  }

  const func = (data) => {
    console.log('子组件调用父组件给他的函数进行传值',data)
  }

  return (
    <>
      <h2>父组件</h2>
      <h2>num: {num}</h2>
      <h2>age: {age}</h2>
     
      <button onClick={changeNum}>点击num</button>
      <button onClick={() => setAge(age +1)}>点击age</button>
      <Count num={num} testChange={func} />  // 正常情况下,不管更新num还是age子组件都会渲染 
       // 使用useCallBack 只有num改变的时候才会渲染子组件,改变age并不会重新渲染,即使有传给子组件的函数也不会更新, 这里也可以抽离到最上面
      {
        useCallback(<Count num={num} testChange={func} />, [num] ) 
      }
        
      // 如果利用useMemo 就相当于
      {
        useMemo(() => (<Count num={num} testChange={func} />), [num] )
      }
    </>
  )
}

1.如果props 只是传一个基本类型的值,可以利用react.memo 优化。

2.如果传了基本类型的值和一个函数,也可以利用useCallback 缓存函数,然后加上React.memo 进行优化, 或者是直接将组件利用useCallback缓存。就不需要react.memo了

3.如果是引用类型的props和函数,可以直接利用useCallback 或者 useMemo 缓存。或者 useCallBack 缓存函数,useMemo 缓存值,看情况使用

useCallback源码

源码解析

自定义简单实现

useMemo

返回一个 memoized 值。

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。 具体看useCallback 两者相同

  {
    useCallback(<Count num={num} testChange={func} />, [num] ) 
  }

  // 如果利用useMemo 就相当于
  {
    useMemo(() => (<Count num={num} testChange={func} />), [num] )
  }

useCallback是根据依赖项(deps)缓存第一个参数callback

useMemo是根据依赖项(deps)缓存第一个参数callback执行后的值。

useCallback会重新返回一个函数体,而useMemo返回的是一个缓存计算数据的值

当依赖项变化时,usecallback会重新创建一个函数体,而useMemo不会。

useLayoutEffect

会在浏览器渲染完成之前执行,阻塞浏览器渲染,可以解决页面闪烁问题,也可能造成页面卡顿,使用方法与useEffect 一样

官网介绍: 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

尽可能使用标准的 useEffect 以避免阻塞视觉更新。

useLayoutEffect源码

源码解析

自定义简单实现

 function useLayoutEffect(callback, deps) {
     if (hooksState[hooksIndex]) {
     
     const [ lastDestroyFunction, lastDeps ] = hooksState[hooksIndex]
     
     // 对比依赖项有没有变化
     cosnt allDepsChange = deps && deps.every((item, index) => item === lastDeps[index])
     if (allDepsChange) {
         hooksIndex++
     }else {
        lastDestroyFunction && lastDestroyFunction()
        queueMicrotask(() => {
           const destroyFunction = callback()
           hooksState[hooksIndex++] = [destroyFunction, deps]
        })
     }  
           
     } else { // 第一次渲染
       
       queueMicrotask(() => {
           const destroyFunction = callback()
           hooksState[hooksIndex++] = [destroyFunction, deps]
       })
     }
 }

useReducer

利用useReducer来管理复杂的状态

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

react.docschina.org/docs/hooks-…

demo

import React, { useReducer } from 'react'

const Test = (props) => {

  // useReducer的初始值
  const store = {
    count: 1
  }

  const reducer = (state:any, action:any) => {
        console.log(state, '------state----->')  // 1
    switch (action.type) {
      case 'increment':
         return { count: state.count + 1 }
      case 'decrement':
         console.log(action.count, '---->action.count') // 4
        return {...state, count: action.count};
      default:
        throw new Error();
    }
  }


  const [state, dispatch] = useReducer(reducer, store);

  const testIncream = () => {
    dispatch({type: 'increment'})
  }

  const testDecream = () => {
    dispatch({count: 4, type: 'decrement'})  // 可以给dispatch传递不同的参数
  }

  return (
    <div>
      <div>testReducer {state.count}</div>
      <button onClick={testIncream}>-</button>
      <button onClick={testDecream}>+</button>
    </div>
  )
}
export default Test

useReducer源码

源码解析

自定义简单实现

 function useReducer(reducer, initialState) {
  hooksState[hooksIndex] = hooksState[hooksIndex] || (typeof initialState === 'function' ? initialState() : initialState)
  let currentIndex = hooksIndex
  function dispatch(action) {
    let lastState = hooksState[currentIndex]
    let nextState;
     if (typeof action === 'function') {
      nextState = action(lastState)
    }
    if (reducer) {
      nextState = reducer(nextState, action)
    } 
    // hooksState[currentIndex] = reducer ? reducer(hooksState[currentIndex], action) : action;
   
    hooksState[currentIndex] = nextState
    scheduleUpdate()
  }
  return [hooksState[hooksIndex++], dispatch]
}


useImperativeHandle

与forwardRef配合使用转发ref,(props不能传ref) 父组件调用子组件的ref实例方法,

父组件

const childRef = useRef<fun | null>(null)
    console.log(childRef, 'childRef')

    const parentAddNum = () => {
      console.log('点击父组件')
      childRef.current!.addChildnum()
    }
    
 <ChildTest ref={childRef} />
 <Button onClick={parentAddNum}> 父组件 </Button>

子组件将addChildnum 方法传给父组件让父组件调用

子组件代码

import React, { useState, forwardRef, useImperativeHandle } from 'react'
import { Button } from 'antd'

interface Props {}

const ChildTest: React.FC<Props> = forwardRef((props, ref) => {

  const [ num, setNum ] = useState<number>(0)
  const [ age, setAge ] = useState<number>(0)

  const addNum = () => {
    setNum(num + 1)
  }

  useImperativeHandle(ref, () => ({
    addChildnum: () => setNum(num + 1)
  }))

  return (
    <div>
      <span> 子组件的数字: {num}</span>
      <Button onClick={addNum}>子组件的按钮</Button>
      <Button onClick={() => setAge(age+1)}>子组件age的按钮</Button>
    </div>
  )
}
)

export default ChildTest;

useImperativeHandle源码

源码解析

自定义简单实现

function useImperativeHandle(ref, handle) {
    ref.current = handle()
}

forwardRef 源码

自定义简单实现

function forwardRef(FunctionComponent) {
    return class extends Component {
        render() {
            return FuncionComponent(this.props, this.ref) // this是类的实例
        }
    }
}

自定义Hook

抽离一些重复的逻辑片段

具体可以看另一篇文章,利用自定义hook封装一些代码片段

juejin.cn/post/694730…

看到这里了,点个赞吧👍 ~~