React的各个Hook

58 阅读5分钟

下面讨论的有:

  • useState
  • useContext
  • useRef
  • useReducer
  • userEffect
  • userLayoutEffect
  • useforwardRef
  • 自定义Hook

useState

笔者在使用useState时候发现很麻烦,而且难以理解。可是如果自己动手模拟useState,就能很容易理解useState的两个特性。

  1. 使用useState的顺序必须固定,不允许有条件的调用useState
  2. setN不是直接修改变量的值,而是创建一个新的变量替换旧的变量,背后的由于过时的闭包(Stale Closure)导致的

+1.jpg

以+1为例,下面是使用useState的正常代码

// index.js
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { useState } from "react";

function App() {
  const [n, setN] = useState(0);
  return (
    <div className="App">
      {n}
      <button onClick={()=>setN(n+1)}>+1</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

但是如果我们用myUseState来模拟useState,可能写出下面的代码

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

const myUseState = (inital) => {
  let n = inital;
  const setN = (state) => {
    n = state;
    root.render(
      <StrictMode>
        <App />
      </StrictMode>
    );
  };
  return [n, setN];
};
function App() {
  console.log('app')
  const [n, setN] = myUseState(0);
  return (
    <div className="App">
      {n}
      <button onClick={() => setN(n + 1)}>+1</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

你会发现页面上的n并没有变化,这是因为每次渲染执行App时,都会把n重新新初始化为0。所以你应该在函数为声明这个变量,例如用_n,并判断一下当只有_n为undefined的时候才赋值为初始值。

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

let _n;
const myUseState = (inital) => {
  if(_n===undefined){_n =inital}
  const setN = (state) => {
    _n = state;
    root.render(
      <StrictMode>
        <App />
      </StrictMode>
    );
  };
  return [_n, setN];
};
function App() {
  const [n, setN] = myUseState(0);
  return (
    <div className="App">
      {n}
      <button onClick={() => setN(n + 1)}>+1</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

这样你就成功实现了。

但是呢,这个myUseState还是太简单了,他没有办法管理多个数据。难道将把_n挂在一个对象上吗,如果你去实现一下,你会发现这种数据结构其实更符合数组。我们来实现以下,比方说state就是这样一个数组,存放_n和_m

+1+1.jpg

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

let state = [];
let index = 0;
const myUseState = (inital) => {
  const currentIndex = index;
  if (state[currentIndex] === undefined) {
    state[currentIndex] = inital;
  }
  const setState = (value) => {
    state[currentIndex] = value;
    console.log(state);
    index = 0
    root.render(
      <StrictMode>
        <App />
      </StrictMode>
    );
  };
  index++;
  return [state[currentIndex], setState];
};
function App() {
  const [n, setN] = myUseState(0);
  const [m, setM] = myUseState(0);
  return (
    <div className="App">
      {n}
      <button onClick={() => setN(n + 1)}>+1</button>
      {m}
      <button onClick={() => setM(m + 1)}>+1</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

成功实现了这个案例,进一步可以发现既然说数据时按数组的形式存放的,那么

  • 使用useState的顺序必须固定,不允许有条件的调用useState

比方说一一共两个数据n和m,如果说m在第一回没有调用useState,那么m就排第一个。第二回n调用了useState,那么变成n排第一个,可是m已经数据存放在第一个了,这就乱套了。

另外啊还应该了解,setN不直接改变n,而生成一个新的变量去替代。比方说下面的例子,如果我们点击3秒后打印n,然后点n+1,你会发现打印出来的仍然时0。

  • 这是因为setN不是直接修改变量的值,而是创建一个新的变量替换旧的变量

所以打印出的时旧的变量,值为0。

3s.jpg

import { StrictMode, useState } from "react";
import { createRoot } from "react-dom/client";
function App() {
  let [n,setN] = useState(0)

  return <div className="App">
  {n}
  <button onClick={()=>setN(n+1)}>+1</button>
  <button onClick={()=>setTimeout(()=>console.log(n),3000)}>
    3秒后打印n</button>
  </div>;
}
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

怎么解决这个问题呢,setN(n=>n+1)setN这样写就可以解决这个问题,当你setN传一个函数相当于一个操作,他不管现在用的什么变量,不获取当前变量而形成闭包,他只管给当前的变量。

另外,每个实例都挂着一个独立的state。

useRef

修改数据的方式不是仅仅有useState,还可使用useRef。但是下面的例子会像你证明:

  • useRef虽然能修改n,但是页面却没有响应

尽管你可以手动的渲染,但这是不推荐的

import { StrictMode, useRef } from "react";
import { createRoot } from "react-dom/client";
function App() {
  let n = useRef(0);
  return (
    <div className="App">
      {n.current}
      <button onClick={() => n.current++}>+1</button>
      <button onClick={() => console.log(n)}>打印n</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

useContext

useContext是useState的扩展版。useState只能组件内使用,不会波及子组件。而利用useContext可以让子组件共用useContext。

useReducer

useReducer是useState的复杂版,说复杂是因为当你随着需要大量状态操作时,useReducer会让状态管理变得容易。下面的例子可以把数据操作汇总到一起。

useReducer.jpg

import { StrictMode, useReducer } from "react";
import { createRoot } from "react-dom/client";
const initial = { n: 0 };
const reducer = (state, action) => {
  if (action.type === "add") {
    return { n: state.n + action.number };
  } else if (action.type === "multi") {
    return { n: state.n * action.number };
  } else {
    throw new Error("unexpect type");
  }
};
function App() {
  const [state, dispatch] = useReducer(reducer, initial);
  const { n } = state;
  return (
    <div className="App">
      {n}
      <button onClick={() => dispatch({ type: "add", number: 1 })}>+1</button>
      <button onClick={() => dispatch({ type: "multi", number: 2 })}>*2</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

在理解了useContext和useReducer之后,我们就可以利用他们来替代Redux。各个组件间状态管理,具体见codeSandbox

<App>
<User/>
<Books />
<Movies/>
</App>

reduex.jpg

useEffect

首先useEffect会在页面渲染第一次执行,然后呢:

  • useEffect(effect)只要重新渲染就执行
  • useEffect(effect,[])不再执行fn
  • useEffect(effect,[n])n变化时再执行fn

注意:effect容易形成过时的闭包(clause closure),因此你需要将会更新的变量添加到useEfffect的依赖项中,这样当变量改变时,effect会重新执行。

useLayoutEffect

useLayoutEffecth和useEffect只是执行时机不同,它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

useLayoutEffect.jpg

useMemo

当父组件更新,即使更新的原因和子组件无关,子组件也会更新。那么useMemo是我们性能优化的手段,他可以添加子组件更新的依赖,只有依赖改变才会改变。

// App.js
import { memo, useMemo, useState } from "react";
import "./styles.css";

export default function App() {
  const [n, setN] = useState(0);
  const [m, setM] = useState(0);
  const onChildClick = useMemo(() => {
    return () => {};
  }, [n]);
  const onClick = () => {
    setN(n + 1);
  };
  return (
    <div className="App">
      <button onClick={onClick}>update n {n} </button>
      <Child2 data={m} />
    </div>
  );
}
function Child(props) {
  const m = props.data;
  console.log("child执行了");
  return <div onClick={props.onChildClick}>{m}</div>;
}
const Child2 = memo(Child);

forwardRef

如果你的子组件想要接受ref属性就必须要使用forwardRef

自定义Hook

我们利用上面这些Hook和模块化的思想,就可以自定义出所有满足需求的Hook啦。