React hooks知识点整理

  1. React hooks知识点整理
  2. Hook 使用规则
  3. useState
    1. 函数式更新
    2. 惰性初始化
  4. useEffect
  5. useContext
  6. useReducer
  7. useCallback
  8. useMemo
  9. useRef
  10. useImperativeHandle
  11. useLayoutEffect
  12. useDebugValue

React hooks知识点整理

Hook 使用规则

Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用,还可以在自定义Hook中调用其他Hook

useState

const [state, setState] = useState(initialState);

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

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

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

在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state

classstate只能作为对象,但在useState hook中不仅仅可以使用对象,还可以使用数组,数字,字符串等;

export default function CountHooks () {

  const [count, setCount] = useState(0)

  // setState会合并,取最后一次更新的值,因此调用这个函数,每次只会加1,而不是4
  function changeCount () {
    setCount(count + 3)
    setCount(count + 1)
  }

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => { setCount(count + 1) }}>+1</button>
      <button onClick={changeCount}>changeCount</button>
    </div>
  )
}

注意!和class组件的setState一样,会合并调用,谁在最后执行谁,因此上面代码中每次调用changeCount函数,每次count还是加1,不是加4也不是加3

函数式更新

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。

export default function CountHooks () {

  const [count, setCount] = useState(0)

  function changeCount () {
    setCount(count => {
      return count + 1
    })
    setCount(count => {
      return count + 1
    })
  }

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => { setCount(count => count + 1) }}>+1</button>
      <button onClick={changeCount}>+2</button>
      <button onClick={() => setCount(0)}>reset</button>
    </div>
  )
}

惰性初始化

惰性初始化,useState中传入一个函数即可,这个函数只会在初始化的时候调用一次,后面组件重新渲染的时候并不会再次调用,提高性能

如果下面不使用函数,而是直接fib(40),这是函数的调用,这会在每次渲染的时候都执行一遍该函数,每当点击按钮一次,就会重新计算一次,此时页面会夯住。即使后续的重新渲染不会用到,但也会执行。

import React, { useState } from 'react'

function fib (n) {
  if (n == 1 || n == 2) return 1
  return fib(n - 1) + fib(n - 2)
}

export default function Comp () {
  const [result] = useState(() => fib(40))
  const [count, setCount] = useState(0)

  function handleClick () {
    setCount(c => c + 1)
  }

  return (
    <div>
      <p>{result}</p>
      <p>{count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  )
}

如果useState初始值是一个对象或数组,setState并不会进行对象的合并,需要自己使用扩展语法进行扩展,如果下面的代码不对count进行扩展,则label属性会丢失

export default function CountHooks () {

  const [count, setCount] = useState({sum: 0, label: '个数'})

  function changeCount () {
    setCount(count => {
      return {...count, sum: count.sum + 1}
    })
  }

  return (
    <div>
      <p>{count.label}: {count.sum}</p>
      <button onClick={changeCount}>+1</button>
    </div>
  )
}

useEffect

useEffect的用法查看官网React 官网-useEffect

下面通过一个例子解释下其运行机制

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

export default function TimerHooks () {
  console.log('function start run ' + Date.now())
  const [date, setDate] = useState(new Date())

  useEffect(() => {
    console.log('timer hooks 的effect执行了', date);
    let timerId = setInterval(() => {
      setDate(new Date())
    }, 1000)
  });

  console.log('function end run ' + Date.now())
  return (
    <div>
      <p>时间: {date.toLocaleTimeString()}</p>
    </div>
  )
}
  • 组件初次挂载,会开启一个定时器,每隔1s更新一下当前时间
  • 我们什么也不做,此时副作用内部又更新了state,会导致组件更新,又开启定时器,无限循环,性能极差

此时我们给定一个清除副作用的函数

useEffect(() => {
  console.log('timer hooks 的effect执行了', date);
  let timerId = setInterval(() => {
    setDate(new Date())
  }, 1000)

  return () => {
    console.log('取消副作用');
    clearInterval(timerId)
  }
});
  • 组件初次挂载,会开启一个定时器,每隔1s更新一下当前时间
  • 组件更新前,先取消副作用,清除了定时器,然后再重新设置一个定时器

这样做看似没什么问题,但是如果我们有其他状态也变化了呢,此时也会取消定时器,导致异常

我们加一个按钮,修改一下count

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

export default function TimerHooks () {
  console.log('function start run ' + Date.now())
  const [date, setDate] = useState(new Date())
  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log('timer hooks 的effect执行了', date);
    let timerId = setInterval(() => {
      setDate(new Date())
    }, 1000)

    return () => {
      console.log('取消副作用');
      clearInterval(timerId)
    }
  });

  console.log('function end run ' + Date.now())
  return (
    <div>
      <p>时间: {date.toLocaleTimeString()}</p>
      <button onClick={() => { setCount(count + 1) }}>+1</button>
    </div>
  )
}

疯狂点击按钮,此时页面上的事件会停住,因为我们点击按钮,也会导致组件更新,此时先清除定时器,再设置,由于我们点得非常快,定时器一直在被清除,因此页面时间显示会卡住,观察下图时间显示

那此时要怎么做,这时我们可以给useEffect传入第二个参数,是一个依赖数组,我们直接传入一个空数组,此时这个effect只在挂载的时候执行一次,卸载的时候清除副作用,组件再怎么更新就与这个effect无关了

  // ...
  useEffect(() => {
    console.log('timer hooks 的effect执行了', date);
    let timerId = setInterval(() => {
      setDate(new Date())
    }, 1000)

    return () => {
      console.log('取消副作用');
      clearInterval(timerId)
    }
  }, []); // 这里传入一个空数组

此时所有问题就都被解决了

useContext

useContext接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。

使用useContext只是让我们允许在函数组件中能接收到context的值,但仍然需要在上层组件中使用Provider来提供value

import React, { useContext } from 'react'
const myContext = React.createContext()
const Provider = myContext.Provider

export default function ContextHooks () {
  const store = {
    user: {
      name: 'jerry',
      age: 18
    }
  }

  return (
    <Provider value={store}>
      <Child></Child>
    </Provider>
  )
}

const Child = props => {
  console.log(useContext(myContext));
  const { user } = useContext(myContext)
  return <div>
    <h1>user: {user.name}</h1>
  </div>
}

useReducer

useReducer就像一个小型的redux,对于比较复杂的state,我们建议通过使用useReducer来声明状态,而不是useState

我们还可以把dispatch函数向子组件传递,用来修改父组件的状态,不用父组件内部再定义一个回调函数了。

useReduceruseState一样,不仅可以使用对象作为初始值,还可以使用数组,数字,字符串等,和redux的区别是,redux可以将初始状态通过默认参数直接传递给state,但是在useReducer中,初始状态只能通过userReducer的第二个参数传入

useReducer接收两个参数,第一个参数是reducer,第二个是初始值,返回最新的state,和dispatch派发reducer的函数

import React, { useReducer } from 'react'

const initCount = { count: 0 }

function reducer (state, action) {
  switch (action.type) {
    case 'decrement':
      return { count: state.count - 1 }
    case 'increment':
      return { count: state.count + 1 }
    default:
      throw new Error('unknow action type');
  }
}

function ReducerCounter (props) {
  const [state, dispatch] = useReducer(reducer, initCount)

  return (
    <div>
      <h1>{state.count}</h1>
      <button onClick={() => { dispatch({ type: 'decrement' }) }}>-1</button>
      <button onClick={() => { dispatch({ type: 'increment' }) }}>+1</button>
    </div>
  )
}

export default ReducerCounter

useReducer还可以接受第三个参数,就是初始化函数,这个初始化函数将userReducer的第二个初始值入参作为参数,返回一个新的state

import React, { useReducer } from 'react'

function init (initialCount) {
  return { count: initialCount };
}

function reducer (state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter ({ initialCount }) {
  // 这里的初始值是通过props传过来的
  // 这里使用init初始化函数,作用是如果需要根据传过来的数据做处理,然后再设置状态,这样就可以把数据处理方法组件外面来执行
  // reducer第三个参数就是初始化数据的函数,会自动把第二个参数的值传递给该函数,再由该函数返回初始的状态值,
  // 这对于复杂的处理很方便,比如需要对传入的参数进行筛选或者改造,但对于简单的,直接使用第二个参数就行了
  // 而对于有重置功能的组件来说,使用第三个参数也是非常方便的
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      <p>Count: {state.count}</p>
      <button
        onClick={() => dispatch({ type: 'reset', payload: initialCount })}>
        Reset
      </button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  );
}

export default Counter
<Counter initialCount={0}></Counter>

useCallback

useCallback 第一个参数是函数,第二个参数是依赖状态的数组,返回值是函数,因此下面使用的时候要带上()

它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

当组件更新的时候,返回的缓存函数依然会执行,只不过执行的函数是缓存起来的函数,因此值不会发生改变

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。但useMemo的实际用法不是这样,只是说useCallback可以使用useMemo的这种方式代替
useMemo缓存的是返回值,useCallback缓存的是回调函数

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

export default function HooksCallBack (props) {
  const [count, setCount] = useState(0)
  const [price, setPrice] = useState(10)
  const [any, setAny] = useState(1)

  const getTotalCallback = useCallback(() => {
    console.log('函数执行了');
    console.log(any);
    return price * count

    // 如果这里为空数组,那么count和price无论怎么改变,getTotalCallback函数都是缓存下来的函数,下面的总计调用永远都是那个函数,就不会更新
    // 若只有一个,则只有该状态变化的时候,才会执行回调函数
  }, [count, price])

  return (
    <div>
      <p>单价:{price},数量:{count},总计:{getTotalCallback()}</p>
      <button onClick={() => { setPrice(price + 1) }}>单价+1</button>
      <button onClick={() => { setCount(count + 1) }}>数量+1</button>
      <button onClick={() => { setAny(any + 1) }}>测试+1</button>
    </div>
  )
}


可以看到,每次点击函数都会执行,并且只有当【单价】和【数量】有变化的时候,函数才变了,因为我们多次点击测试按钮,每次返回的都是上一次的值,而当点击单价或数量后,这个值才是最新的值。

useMemo

useMemouseCallback的用法类似,只不过useMemo缓存的是值,而useCallback缓存的是函数,useMemo返回的是缓存的具体值;

我们依然可以使用上面相同的例子,转换成useMemo

import React, { useState, useMemo } from 'react'

export default function HooksCallBack (props) {
  const [count, setCount] = useState(0)
  const [price, setPrice] = useState(10)
  const [any, setAny] = useState(1)

  // useMemo返回值即函数的返回值,下面使用的时候直接使用
  const total = useMemo(() => {
    console.log('函数执行了');
    console.log(any);
    return price * count

    // 当不在依赖状态数组里面的状态发生了变化,不会执行fn函数,返回的是上一次执行后的缓存的值
  }, [count, price])

  return (
    <div>
      <p>单价:{price},数量:{count},总计:{total}</p>
      <button onClick={() => { setPrice(price + 1) }}>单价+1</button>
      <button onClick={() => { setCount(count + 1) }}>数量+1</button>
      <button onClick={() => { setAny(any + 1) }}>测试+1</button>
    </div>
  )
}

useRef

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

普通用法和createRef一样,都是通过绑定ref属性来获取DOM元素或者组件实例,区别在于,在class组件中,我们在constructor中创建ref,而constructor仅在组件挂载的时候执行一次,更新阶段是不会执行的,因此在class组件中更新不会重新创建ref,在函数组件中,函数更新每次都会重新执行,因此createRef在函数组件中每次都会重新创建,导致每次都是得到的结果都是初始值,而useRef在函数组件中具有“缓存”的特点,多次执行函数并不会“重置”ref的值。

我们通过下面的例子来看看两者的区别:

export default const UseRefCreateRef = () => {
  const [count, setCount] = useState(0)

  const varUseRef = useRef()
  const varCreateRef = createRef()

  console.log(varUseRef, varCreateRef);
  // 从这里的打印值就可以看出来,每次打印,varUseRef返回的是同一个引用,而createRef每次都会创建一个新的引用

  varUseRef.current = count
  varCreateRef.current = count
  return (
    <>
      <p>count: {count}</p>
      <p>varUseRef: {varUseRef.current}</p>
      <p>varCreateRef: {varCreateRef.current}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </>
  )
}


解释下,每次更新count,都会使组件重新渲染,我们在函数组件里面重新设置varUseRefvarCreateRef的值,但是从结果可以看出来,varCreateRef每次都被重新创建了,而varUseRef每次都保持了相同的引用。

有了这一特性,我们就可以把一些在组件全生命周期中需要保留的东西放在useRef上面,比如一个可修改的“全局变量”,又或是debounce时候定义的定时器也可以使用useRef保存。

useImperativeHandle

我们在使用forwardRef进行转发时,接收到父组件的ref,然后直接将其绑定到子组件的DOM上,这样做并没有什么问题,但是这会带来一些问题,将子组件的DOM直接暴露给父组件,父组件的行为子组件并不可控,可能做一些破坏性的动作。此时我们可以仅暴露操作子组件DOM的方法,而不将整个DOM暴露给父组件,例如我只希望父组件可以操作子组件内部input元素的focus,就可以将其封装成一个函数,暴露给父组件。借助useImperativeHandle,我们就可以进行这种操作。

useImperativeHandle(ref, createHandle, [deps])
  • ref:父组件绑定的ref
  • createHandle:一个函数,返回值是一个对象,这个对象将绑定到传过来的ref.current属性上
  • [deps]:依赖列表,当监听的依赖发生变化,useImperativeHandle 才会重新将子组件的实例属性输出到父组件

useImperativeHandle应该总是配合React.forwardRef一起使用,

例如以下代码:

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

function FancyInput (props, ref) {
  const inputRef = useRef();
  const btnRef = useRef();

  useImperativeHandle(ref, () => ({
    inputFocus: () => {
      inputRef.current.focus();
    },
    getBtnWidth: () => {
      return btnRef.current.offsetWidth;
    }
  }));
  return (
    <>
      <input ref={inputRef}></input>
      <button ref={btnRef}>按钮按钮按钮按钮按钮</button>
    </>
  )
}
FancyInput = React.forwardRef(FancyInput);


export default function HooksRef () {
  const fancyInputRef = useRef()

  return (
    <div>
      <p>useRef</p>
      <button onClick={() => fancyInputRef.current.inputFocus()}>聚焦</button>
      <button onClick={() => alert(fancyInputRef.current.getBtnWidth())}>获取子组件按钮宽度</button>
      <div>
        <FancyInput ref={fancyInputRef} />
      </div>
    </div>
  )
}

useLayoutEffect

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

useEffect在浏览器渲染完成后执行,算是异步执行,这样就不会阻塞浏览器渲染,影响用户体验。应该尽可能使用标准的 useEffect 以避免阻塞视觉更新。

import React, { useLayoutEffect, useEffect } from 'react'

const LayoutEffectComp = () => {
  const ref = React.createRef()

  // 使用useLayoutEffect的时候,在副作用里面操作dom会在浏览器渲染前变化,不会看到页面有闪屏
  useEffect(() => {
    ref.current.style.transform = 'translateX(100px)'
  })

  return (
    <div style={{ width: 100, height: 100, backgroundColor: 'red' }} ref={ref}></div>
  )
}

export default LayoutEffectComp

useEffect换成useLayoutEffect就不会有这种闪动了

useDebugValue

useDebugValue(value)

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。

例如一个简易的useDebounce

import { useRef, useDebugValue } from 'react'

const useDebounce = (func, time) => {
  useDebugValue('someDebounceName')

  let timer = useRef()
  return function (...args) {
    if (timer.current) clearTimeout(timer.current)
    timer.current = setTimeout(() => {
      func(...args)
    }, time)
  }
}

export default useDebounce


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com