React Hooks - 复习

532 阅读11分钟

React Hook的使用

前言

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。 在早期使用react的时候,会发现使用class创建的组件十分的臃肿,而且复用性差。在后面副作用和状态越来越多的时候会变得越来越难以改动。因此,学习hooks有利于我们构建更加精简且复用性更好的代码。

一、useState

1、基本使用方法

  • 使用useState会返回两个东西,一个是你创建的状态state,一个是更新state的函数setstate
  • 下文中的[count, setCount]是数组解构,当然你也可以使用别的名字
  • setState中更新的方法有两种,一种是直接更新,不依赖于之前的状态;另一种是函数式更新,依赖于之前的状态。
import { useState } from "react";
const Test = () => {
  const [count, setCount] = useState(0);
  return (
    <>
      <div>{count}</div>
      
      <!-- 方式一,直接更新 -->
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        +
      </button>
      
      <!-- 方法二,函数式更新 -->
      <button
        onClick={() => {
          setCount((prevCount) => prevCount - 1);
        }}
      >
        -
      </button>
    </>
  );
};

export default Test;

2、有多个状态就用多个useState

  • 多个state之间并不共享状态
import { useState } from "react";
const Test = () => {
  const [count, setCount] = useState(0);
  const [counter, setCounter] = useState(0);
  return (
    <>
      <div>{count}</div>
      <button onClick={() => {setCount(count + 1);}} >
        +
      </button>
      <div>{counter}</div>
      <button onClick={() => {setCounter(counter + 10);}} >
        +
      </button>
    </>
  );
};

export default Test;

3、当状态为引用类型时

要注意useState中的setstate并不会执行浅合并,而是直接替换

import { useState } from "react";
const Test = () => {
  const [customer, setCustomer] = useState({ id: 0, name: "hello", age: 18 });
  return (
    <>
      <div>
        {customer.id} - {customer.name} - {customer.age}
      </div>
      
      <!-- 错误的修改方法,直接修改age -->
      <button
        onClick={() => {
          setCustomer({ age: customer.age + 1 });
        }}
      >
        error-change-age
      </button>
      
      <!-- 正确的修改方法,将之前的state展开,然后将需要修改的属性直接覆盖 -->
      <button
        onClick={() => {
          setCustomer({ ...customer, age: customer.age + 1 });
        }}
      >
        true-change-age
      </button>
    </>
  );
};

export default Test;

4、惰性初始化state

initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

二、useEffect

useEffect有种类似类组件中的componentDidMountcomponentDidUpdatecomponentWillUnmount三者结合的作用。其主要作用是处理副作用函数,清除副作用函数和对某些状态进行监听。

1、基本使用方法

基本形式如下

  • effect为副作用函数,一般填写挂载成功后以及依赖参数input变动时执行的动作
  • cleanup为返回函数,一般填写组件被卸载前执行的动作以及配套effect的清除函数
  • input为依赖参数,可传入空数组
useEffect(() => {
    effect
    return () => {
      cleanup
    }
  }, [input])

执行的顺序

  • 挂载阶段,从上到下读取各个不同的useEffect,然后按先后顺序将effect推入一个队列中(先进先出)。在组件挂载完成后执行队列中的effect,并获取返回函数,将返回函数推入另一个队中。
  • 更新阶段,从上到下读取各个不同的useEffect,然后先后顺序将effect推入一个队列中(先进先出)。在组件完成更新后先执行队列中的返回函数cleanup,然后再执行队列中的effect。执行时获取新的返回函数队列。
  • 卸载阶段,只执行返回函数队列

现在我们来验证一下

import { useState, useEffect } from "react";
const App = () => {
  const [show, setShow] = useState(true);
  return (
    <>
    <!-- 负责控制Counter的卸载与挂载 -->
      <button
        onClick={() => {
          setShow(!show);
        }}
      >
        toggle
      </button>
      {show && <Counter />}
    </>
  );
};

const Counter = () => {
  const [count, setCount] = useState(0);
  
  // 挂载完成后和更新完成后才执行
  useEffect(() => {
    console.log("useEffect - 1 ");
    return () => {
      console.log("useEffect - 1 - 返回");
    };
  });

  useEffect(() => {
    console.log("useEffect - 2 ");
    return () => {
      console.log("useEffect - 2 - 返回");
    };
  });

  useEffect(() => {
    console.log("useEffect - 3 ");
    return () => {
      console.log("useEffect - 3 - 返回");
    };
  });

// 这句是挂载前执行
  console.log("挂载");

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

export default App;

关于依赖参数

  • 组件更新时,副作用函数effect和返回函数cleanup在执行前会判断对应的useEffect中的依赖参数是否发生变化,没有的话就不执行。但是要注意无论有没有依赖参数,effect默认会在组件挂载完成后执行
  • 如果没有依赖参数,那么挂载完成后以及任意的更新后都会执行effect
  • 如果依赖参数为空数组[],那么该effect只会执行一次

ok,我们知道了这些注意点,就能模拟类组件中的生命周期函数。

2、模拟类组件声明周期函数以及监听器

1)、componentDidMount

首先来明确一下,componentDidMount中,在组件挂载完成后执行,且只执行一次

  useEffect(() => {
    console.log("模仿componentDidMount");
  }, []);

2)、componentWillUnmount

在组件即将卸载的时候执行

useEffect(() => {
    return () => {
      console.log("模仿componentWillUnmount");
    };
  }, []);

3)、componentDidUpdate

在这里我会使用一个hooks叫useRef,我们会用它来记录是否是挂载阶段,来让useEffect不要在挂载完成后就执行,只让函数在任意更新完成后执行。

const Counter = () => {
  const [count, setCount] = useState(0);
  const [numb, setNumb] = useState(10); // 添加了新的state
  const fisrtRend = useRef(true);	// 使用它来记录是否是挂载阶段

  useEffect(() => {
    if (fisrtRend.current) {
      fisrtRend.current = false;
      return;
    }
    console.log("模拟componentDidUpdate");
  });

  return (
    <>
      <div>count - {count}</div>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        +
      </button>

      <div>numb - {numb}</div>
      <button
        onClick={() => {
          setNumb(numb + 10);
        }}
      >
        +
      </button>
    </>
  );
};

4)、监听器

作用类似vue3的watchEffect吧,只监听特定的数据,在数据变动时做出一些操作

const Counter = () => {
  const [count, setCount] = useState(0);
  const fisrtRend = useRef(true);

  useEffect(() => {
    if (fisrtRend.current) {
      fisrtRend.current = false;
      return;
    }
    console.log("现在的count是" + count);
  }, [count]);

  return (
    <>
      <div>count - {count}</div>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        +
      </button>
    </>
  );
};

3、小总结

  • 可以把useEffect Hook看做 componentDidMountcomponentDidUpdatecomponentWillUnmount这三个函数的组合
  • 与 componentDidMount 或 componentDidUpdate 不同,使用useEffect调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。

三、useRef

  • 在类组件的时候,我们使用ref都是使用React.createRef来创建。
  • 但是,在hooks中,我们通过useRef来创建,利用它来关联实例或者记录组件更新前的一些状态
  • useRef 返回一个可变的 ref 对象,其.current属性被初始化为传入的参数(initialValue)。返回的ref 对象在组件的整个生命周期内保持不变
  • 当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染

1、获取dom节点

你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以<div ref={myRef} />形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。

import { useState, useRef } from "react";
const Test = () => {
  const [count, setcount] = useState(0);
  const refEl = useRef();

  return (
    <>
      <p ref={refEl}>{count}</p>
      <button
        onClick={() => {
          console.log(refEl.current);
        }}
      >
        获取p标签
      </button>
      <br />
      <button
        onClick={() => {
          setcount(count + 1);
        }}
      >
        +
      </button>
    </>
  );
};

export default Test;

2、记录更新前的一些状态

为什么可以做到? 因为它创建的是一个普通 Javascript 对象,而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象

验证一下

import { useState, useEffect, useRef } from "react";
const Test = () => {
  const [count, setcount] = useState(0);
  const prevCount = useRef(count);

// 即使count改变了,但是prevCount.current不会变
  useEffect(() => {
    console.log(`count: ${count}, prevCount: ${prevCount.current}`);
  }, [count]);

  return (
    <>
      <p>{count}</p>
      <br />
      <button
        onClick={() => {
          setcount(count + 1);
        }}
      >
        +
      </button>
    </>
  );
};

export default Test;

唯一可以改变的方法,就是手动去修改prevCount.current的值

  useEffect(() => {
    console.log(`count: ${count}, prevCount: ${prevCount.current}`);
    // 手动修改, 每次变更之后将prevCount.current的值修改为现在count的值
    prevCount.current = count;
  }, [count]);

四、useMemo

  • useMemo有点类似vue中的计算属性,根据某个依赖参数返回某些属性,只在依赖参数改变时才重新计算memoized值
  • 如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值
  • 传入 useMemo 的函数会在渲染期间执行,请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴

1、为什么要使用

我们知道父组件的更新,会导致子组件的更新。在类组件的时候,我们有PureComponentshouldComponentUpdate这两种手段可以进行优化,但是在函数组件中,我们无法使用这两个api。取而代之的,是useMemo

没有使用useMemo

import { useState } from "react";
const Test = () => {
  const [count, setcount] = useState([0]);

  return (
    <>
      <button
        onClick={() => {
          setcount([...count, count.length]);
        }}
      >
        add
      </button>
      {count.map((item) => (
        <Child data={item} key={item} />
      ))}
    </>
  );
};

const Child = ({ data }) => {
  function updater() {
    console.log(`${data} - 渲染`);
    return "Child" + data;
  }

  return (
    <>
      <div>{updater()}</div>
    </>
  );
};

export default Test;

2、如何使用

api与useEffect完全相同,但是注意不要和useEffect搞混

import { useState, useMemo } from "react";
const Test = () => {
  const [count, setcount] = useState([0]);

  return (
    <>
      <button
        onClick={() => {
          setcount([...count, count.length]);
        }}
      >
        add
      </button>
      {count.map((item) => (
        <Child data={item} key={item} />
      ))}
    </>
  );
};

const Child = ({ data }) => {

  const updater = useMemo(() => {
    console.log(`${data} - 渲染`);
    return "Child" + data;
  }, [data]);


  return (
    <>
    <!-- 注意到这里并不是updater(), 因为它会自动执行 -->
      <div>{updater}</div>
    </>
  );
};

export default Test;

3、最终真的实现了PureComponent的效果了吗?

我们在挂载的时候进行打印来看看子组件究竟重新渲染了没

import { useState, useEffect, useMemo } from "react";
const Test = () => {
  const [count, setcount] = useState([0]);

  return (
    <>
      <button
        onClick={() => {
          setcount([...count, count.length]);
        }}
      >
        add
      </button>
      {count.map((item) => (
        <Child data={item} key={item} />
      ))}
    </>
  );
};

const Child = ({ data }) => {
  const updater = useMemo(() => {
    console.log(`${data} ------ useMemo `);
    return "Child" + data;
  }, [data]);

  console.log(` ---- ${data} - 挂载 `);

  return (
    <>
      <div>{updater}</div>
    </>
  );
};

export default Test;

4、结论

可以看到,子组件还是更新了。那么在这里useMemo究竟有什么用?答案是虽然子组件还是重新加载了,但是会产生新的虚拟dom,新旧虚拟dom之间会进行对比,发现两者并没有变化,所以就不更新dom了。

五、Memo

  • React.memo 为高阶组件
  • 如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果
  • 默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现
  • 比较函数的参数为(prevProps, nextProps)

1、对上面的例子使用memo进行优化

import { useState, useMemo, memo } from "react";
const Test = () => {
  const [count, setcount] = useState([0]);

  return (
    <>
      <button
        onClick={() => {
          setcount([...count, count.length]);
        }}
      >
        add
      </button>
      {count.map((item) => (
        <MemoChild data={item} key={item} />
      ))}
    </>
  );
};

const Child = ({ data }) => {
  const updater = useMemo(() => {
    console.log(`${data} ------ useMemo `);
    return "Child" + data;
  }, [data]);

  console.log(` ---- ${data} - 挂载 `);

  return (
    <>
      <div>{updater}</div>
    </>
  );
};

// 注意高阶组件的命名要遵守pascal命名
const MemoChild = memo(Child);

现在能看到使用memo优化后符合了我们的预期。

2、关于第二参数

这里和shouldComponentUpdate有点不同,如果返回true则不执行更新,如果返回false则执行更新

import { useState, memo } from "react";
const Test = () => {
  const [count, setcount] = useState(0);

  return (
    <>
      <button
        onClick={() => {
          setcount(count + 1);
        }}
      >
        add
      </button>
      <MemoChild1 data={count} />
      <MemoChild2 data={count} />
    </>
  );
};

const Child = ({ data }) => {
  return (
    <>
      <div>{data}</div>
    </>
  );
};

const MemoChild1 = memo(Child, (prevProps, props) => {
	//	不执行更新
  console.log("memo1", prevProps, props);
  return true;
});
const MemoChild2 = memo(Child, (prevProps, props) => {
	//	执行更新
  console.log("memo2", prevProps, props);
  return false;
});

export default Test;

六、useContext

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value 决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染

使用useContext实现counter

import { useState,  createContext, useContext } from "react";

const MyCustomContext = createContext();

const Test = () => {
  const [count, setCount] = useState(0);
  return (
    <>
      <MyCustomContext.Provider value={{ count }}>
        <Child />
      </MyCustomContext.Provider>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        +
      </button>
    </>
  );
};

const Child = () => {
  return (
    <>
      <GrandChild />
    </>
  );
};

const GrandChild = () => {
	//	不需要使用consumer就可以直接拿到{count: 0}
  const { count } = useContext(MyCustomContext);

  return (
    <>
      <p>GrandChild - {count}</p>
    </>
  );
};

export default Test;

七、useReducer

  • useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
  • 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。
  • 并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

1、基本使用

import { useReducer} from "react";

function reducer(state, action) {
  switch(action.type) {
    case "INCREMENT":
      return {count: state.count + 1}
    case "DECREMENT": {
      return {count: state.count - 1}
    }
    default :{
      return state
    }
  }
}

const initialState = {count: 0};

const Test = () => {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <>
        <div>Count: {state.count}</div>
        <button onClick={()=>{dispatch({type: "INCREMENT"})}}>+</button>
        <button onClick={()=>{dispatch({type: "DECREMENT"})}}>-</button>
    </>
  );
};

export default Test;

2、惰性初始化

  • 你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)
  • 这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利
import { useReducer } from "react";

//	initialState
const count = 0;

//	第三参数 init
function init(initialState) {
  return { count: initialState };
}

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT": {
      return { count: state.count - 1 };
    }
    case "RESET": {
      return { count };
    }
    default: {
      return state;
    }
  }
}

const Test = () => {
  const [state, dispatch] = useReducer(reducer, count, init);
  return (
    <>
      <div>Count: {state.count}</div>
      <button
        onClick={() => {
          dispatch({ type: "INCREMENT" });
        }}
      >
        +
      </button>
      <button
        onClick={() => {
          dispatch({ type: "DECREMENT" });
        }}
      >
        -
      </button>
      <button
        onClick={() => {
          dispatch({ type: "RESET" });
        }}
      >
        RESET
      </button>
    </>
  );
};

export default Test;

八、自定义hooks

  • 自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook
  • 与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)
  • 在两个组件中使用相同的 Hook 会共享 state 吗?不会。
  • 自定义 Hook 如何获取独立的 state?每次调用 Hook,它都会获取独立的 state。

我们来自己实现一个简单的hook,形式类似useState,作用是设置一个div标签显示当前屏幕的scrollY以及设置点击时跳到顶部。

import { useEffect, useState } from "react";

const useCustom = () => {
  const [y, setY] = useState(window.scrollY);
  useEffect(() => {
    window.onscroll = () => {
      setY(window.scrollY);
    };
    return () => {
      window.onscroll = null;
    };
  }, []);
  return [
    y.toFixed(3),
    (newY) => {
      window.scrollTo(0, newY);
      setY(newY);
    },
  ];
};

const Test = () => {
  const arr = new Array(20).fill(0);
  const style = {
    width: "100px",
    height: "100px",
    background: "red",
    margin: "5px",
  };

  const style2 = {
    position: "fixed",
    left: 0,
    top: "500px",
    width: "100px",
    background: "black",
    color: "white",
  };

  const [y, setY] = useCustom();

  return (
    <>
      <div
        style={style2}
        onClick={() => {
          setY();
        }}
      >
        {y}
      </div>
      {arr.map((item, index) => (
        <div key={index} style={style}></div>
      ))}
    </>
  );
};

export default Test;