【React系列】React Hooks全面解读

1,635 阅读9分钟

为什么是hook?

名称Function ComponentClass Component
性能90分88分
this
函数编程
复用简单嵌套层级深
生命周期优雅复杂
hook
错误处理
代码顺序有严格顺序
闭包会有闭包问题

基础Hook

一、useState

1.1 基本使用

  • 通过在函数组件里调用它来给组件添加一些内部 state,React 会在重复渲染时保留这个 state
  • useState 唯一的参数就是初始 state
  • 返回一个数组第一项是state,第二项更新 state 的函数
    • 在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同
    • setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列
function Counter(){
  const [number,setNumber] = useState(0);
  return (
      <>
          <p>{number}</p>
          <button onClick={()=>setNumber(number+1)}>+</button>
      </>
  )
}

1.2 函数式更新

  • 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。
function Counter() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setTimeout(() => {
      console.log(count,'setTimeout')
      // 在3000秒内无论点击多少次,count都是当前的状态计算(根据点击次数会执行多次),所以并不会根据点击次数累加
      setCount(count + 1)
    }, 1000);
  }
  function handleClickFn() {
    setTimeout(() => {
      // 点击的次数多少函数就执行几次,  
      // 比如 3000秒点击三次: 相当于定时器生成三个,结果累计相加=3  prevCount上次状态 
      setCount((prevCount) => {
        return prevCount + 1
      })
    }, 3000);
  }
  return (
    <>
      Count: {count}
      <button onClick={handleClick}>handleClick+</button>
      <button onClick={handleClickFn}>handleClickFn+</button>
    </>
  );
}

如果你需要用到上次的值,可以使用函数更新的方式。

1.3 惰性初始 state

  • initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略
  • 若果初始需要复杂计算,initialState可以是一个函数,函数只在初次渲染的时候被调用
function Counter3(){
  const [{name,number},setValue] = useState(()=>{
    return {name:'计数器',number:0};
  });
  return (
      <>
          <p>{name}:{number}</p>
          <button onClick={()=>setValue({number:number+1})}>+</button>
      </>
  )
}

1.4 跳过 state 更新

调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)

需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

也就是说 setState 同一个值, 当前组件可能渲染。这里说的渲染是指函数会被调用一下, console.log 会打印出来, 但不会继续向下渲染, 所以没什么大不了的。

文字不太能说明什么,看下面🍐:

import React,{useState,createContext,useContext, memo, useMemo, useEffect} from 'react';

const _num = [
  {
    count :1
  }
]
export default function Parent(){
  const [show, setShow] = useState(false)
  const [num ,setNum] = useState(_num);
  console.log('渲染了');
    
  useEffect(() => {
    // 这里_num都是同一个值,但是还会会引起页面函数执行。
    setNum(_num)
  }, [show]);

  return (
    <React.Fragment>
      <button onClick={()=>{ setNum(_num)}}>{num[0].count}</button>
      <button onClick={()=>setShow(!show)}>button</button>
    </React.Fragment>
  )
}

因为 callback function 就像泼出去的水, 是收不回来的。函数的调用者想在调用到一半时中断可能比较困难, 除非 setState 内部 throw 异常。

使用总结

😊1. useState的初始值,只在第一次有效

✋2. 按照有序的方式使用usestate不得在循环判断等条件语句使用

😡3. useState 不会自动合并更新对象,更新对象时使用扩展符{...counter,number:6}

二、useEffect

有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。

2.1 基本使用

useEffect有两个参数, 参数一: function(执行项),参数二:array(依赖项)

function Effect(){
  // useEffect 里面的函数会在组件全部挂载完后和组件更新完成后执行(也就是说在paint后)
  // 省略第二个参数每次渲染都会执行(尽量避免此操作)
  useEffect(() => {
      console.log('执行了')
  });
  return (
    <div>
      <button onClick={}>useEffect</button>
    </div>
  )
}

2.2 模拟生命周期componentDidMount

  //  😊 如果空数组,只会在第一次挂载后执行,类似commponentDidMount
 useEffect(() => {},[]);

2.3 依赖某个stateprops

function Effect(){
    const [name,setName] = useState('xy')
    const [age,setAge] = useState(0)
    //  😊 如果数组不为空,则会在数组中依赖的变量改变时重新执行
    useEffect(()=>{
        console.log('name更新了')
    },[name]) // 依赖可以多个
    useEffect(()=>{
        console.log('age更新了')
    },[age]) // 依赖可以多个
    return (
        <>
            <button onClick={()=>{setName('xxxx')}}>{name}<button>
        </>
    )
}

2.4 模拟componentUnMount,清楚副作用

  • 副作用函数还可以通过返回一个函数来指定如何清除副作用 为防止内存泄漏,清除函数会在组件卸载前执行。
  • 另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除

function Effect(){
    const [age,setAge] = useState(0)
    // useEffect每次执行,都会先执行上次return的函数,再执行内部回掉函数
    useEffect(()=>{
        console.log('产生新定时器')
        let time = setInterval(()=>{
            console.log('sss')
        },1000) 
        
        return ()=>{
            console('清理上次的定时器')
            clearInterval(time)
        }
    },[age])
    return(
        <div>
            <button onClick={()=>{setAge(age+1)}}>+</button>
        </div>
    )
}


使用总结

😊1. 要把该函数在 useEffect 中申明,不能放到外部申明,然后再在 useEffect 中调用

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp);
  }

  useEffect(() => {
    doSomething();
  }, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)
}

要记住 effect 外部的函数使用了哪些 propsstate 很难。这也是为什么 通常你会想要在 effect 内部 去声明它所需要的函数。 这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值:

function Example({ someProp }) {
  useEffect(() => {
    function doSomething() {
      console.log(someProp);
    }

    doSomething();
  }, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)
}

只有 当函数(以及它所调用的函数)不引用 propsstate 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。下面这个案例有一个 Bug:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  async function fetchProduct() {
    const response = await fetch('http://myapi/product' + productId); // 使用了 productId prop
    const json = await response.json();
    setProduct(json);
  }
  useEffect(() => {
    fetchProduct();
  }, []); // 🔴 这样是无效的,因为 `fetchProduct` 使用了 `productId`
  // ...
}

推荐的修复方案是把那个函数移动到你的 effect 内部。这样就能很容易的看出来你的 effect 使用了哪些 props 和 state,并确保它们都被声明了:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  useEffect(() => {
    // 把这个函数移动到 effect 内部后,我们可以清楚地看到它用到的值。
    async function fetchProduct() {
      const response = await fetch('http://myapi/product' + productId);
      const json = await response.json();
      setProduct(json);
    }
    fetchProduct();
  }, [productId]); // ✅ 有效,因为我们的 effect 只用到了 productId
  // ...
}

  1. 只在更新时执行effect
  • 手动存储一个布尔值来表示是首次渲染还是后续渲染,然后在effect 中检查这个标识
function effect(){
    const countRenderRef = useRef(false);
      const [num, setNum] = useState(0)
      
      useEffect(function afterRender() {
        if(countRenderRef.current){
          console.log(countRenderRef.current, 'num更新执行的操作')
          // doSomething()
        }else{
          countRenderRef.current = true;
        }
      },[num]);
    
      return (
        <div>
          I've rendered {countRenderRef.current.toString()} times
          <button onClick={()=>{setNum(num+1)}}>{num}</button>
        </div>
      );
}

三、useContext

3.1 基本使用

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
//father.jsx
import React, { useState, useEffect, createContext } from 'react';
import Childern from './children'
// 创建上下文
export const FatherContext = createContext();

export default function Father(){
    const [num,setNum] = useState(0)
    return (
        <>  
            <p>{num}</p>
            <FatherContext.Provider value={{num,setNum}}>
                <Childern/>
            </FatherContext.Provider>
        </>
    )
}


// children.jsx
import React, { useState, useEffect, useContext  } from 'react';
import Father from './father';
export default function Childern(props){
    const { num,setNum } = useContext(FatherContext);
    useEffect(()=>{
        setNum(num+1)
    },[])
    reurn (
        <>
         <p>{num}</p>
        </>
    )
}

3.2 使用总结

useContext详解
😊1. 一般用在组件通讯

额外Hook

四、useReducer

4.1 useReducer使用

  • useState的内部实现就是通过useReducer
  • useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
  • 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。
const [state, dispatch] = useReducer(reducer, initialArg, init);
const initialState = {count: 0};

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

function MyUseReducer() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

4.2 惰性初始化

  • 可以将init函数作为useReducer的第三个参数,初始state将被设置为init(initialArg)
  • 有利于将计算的逻辑提取,同样对重制state也很方便
// 稍加改造
const initialCount = 0;

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 MyUseReducer() {
  const [state, dispatch] = useReducer(reducer, initialCount , init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );


五、useRef

六、useImperativeHandle

七、useLayoutEffect

性能优化

😁从减少代码执行(渲染次数)入手

八、useMemo

8.1 理解React.memo

  • 使用 React.memo ,将函数组件传递给 memo 之后,就会返回一个新的组件,新组件的功能:如果接受到的属性不变,则不重新渲染函数;

举个🍐

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

function Child(props){
  console.log('Child==render',props)

  return (
    <div>
      <p>Child</p>
    </div>
  )
}

// 如果不使用memo  父组件执行,Child也会渲染,即使age值没有变化
Child = memo(Child)

function Father(){
  const [num, setNum] = useState(0)
  const [age, setAge] = useState(10)
  return (
    <>
      <Child age={age}></Child>
      <button onClick={()=>{setNum(num+1)}}>{num}</button>
    </>
  )
}

稍加修改

function Child(props){
  console.log('Child  render',props)

  return (
    <div>
      <p>Child</p>
    </div>
  )
}

Child = memo(Child)

function Father(){
  const [num, setNum] = useState(0)
  const [age, setAge] = useState(10)
    
  // Father执行, tempAge值虽然没变, 此时的tempAge是一个新对象内存地址发生改变,所以Child也会重新渲染
  const tempAge = {age}
  return (
    <>
      <Child person={tempAge}></Child>
      <button onClick={()=>{setNum(num+1)}}>{num}</button>
    </>
  )
}

可以使用useMemo 解决以上问题

8.2 更深入的useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

根据官方文档的介绍我们可理解:在a和b的变量值不变的情况下,memoizedValue的值不变。即:useMemo函数的第一个入参函数不会被执行,从而达到节省计算量的目的。

🍊🍐

function Child(props){
  console.log('Child  render',props)

  return (
    <div>
      <p>Child</p>
    </div>
  )
}

Child = memo(Child)


function Father(){
  const [num, setNum] = useState(0)
  const [age, setAge] = useState(10)

  const tempAge = useMemo(()=>{
    return {age}
  },[age])
  return (
    <>
      <Child person={tempAge}></Child>
      <button onClick={()=>{setNum(num+1)}}>{num}</button>
    </>
  )
}

😂😁😊完美解决!

九、useCallback

9.1 基本使用

const memoizedCallback = useCallback(

    // deeps: 和useEffect一样,如果没有每次渲染时都会运行,空数组只在挂载时运行。
    // 如果有依赖项:有变化则会重新声明回调函数
  () => {
    doSomething(a, b);
  },
  [a, b],
);

根据官网文档的介绍我们可理解:在a和b的变量值不变的情况下,memoizedCallback的引用不变。即:useCallback的第一个入参函数会被缓存,从而达到渲染性能优化的目的。

9.2 深入的useCallback

🍊🍐


function Child(props){
  console.log('Child  render',props)

  return (
    <div>
      <p>Child</p>
      <button onClick={()=>{props.memoized()}}>子age{props.value.age}</button>
    </div>
  )
}

Child = memo(Child)

function Father(){
  const [num, setNum] = useState(0)
  const [age, setAge] = useState(10)

  const tempAge = useMemo(()=>{
    return {age}
  },[age])
  // 如果依赖属性不变,  useCallback第一个参数函数会缓存。  所以修改num,子组件不会渲染。
  const memoizedCallback = useCallback(()=>{
    return setAge(age+1)
  },[age])

  return (
    <>
      <Child value={tempAge} memoized={memoizedCallback}></Child>
      <button onClick={()=>{setNum(num+1)}}>父num{num}</button>
    </>
  )
}

9.3 useMemo和useCallback总结

useCallbackuseMemo都可缓存函数的引用或值,但是从更细的使用角度来说useCallback缓存函数的引用,useMemo缓存计算数据的值。

useCallback第一个参数不会执行,useMemo会执行。

其他:

优化点:项目中使用redux管理,去掉不相关的props,避免不必要的渲染

一把锁的props属性:

错误×

image.png

只使用组件中用到的属性

正确✓

image.png

十、参考

官方文档一定要读

30分钟入门hooks

必读的react hooks

十一、谢谢🙏

有错误地方还请纠正