下面讨论的有:
- useState
- useContext
- useRef
- useReducer
- userEffect
- userLayoutEffect
- useforwardRef
- 自定义Hook
useState
笔者在使用useState时候发现很麻烦,而且难以理解。可是如果自己动手模拟useState,就能很容易理解useState的两个特性。
- 使用useState的顺序必须固定,不允许有条件的调用useState
- setN不是直接修改变量的值,而是创建一个新的变量替换旧的变量,背后的由于过时的闭包(Stale Closure)导致的
以+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
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。
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会让状态管理变得容易。下面的例子可以把数据操作汇总到一起。
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>
useEffect
首先useEffect会在页面渲染第一次执行,然后呢:
useEffect(effect)只要重新渲染就执行useEffect(effect,[])不再执行fnuseEffect(effect,[n])n变化时再执行fn
注意:effect容易形成过时的闭包(clause closure),因此你需要将会更新的变量添加到useEfffect的依赖项中,这样当变量改变时,effect会重新执行。
useLayoutEffect
useLayoutEffecth和useEffect只是执行时机不同,它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
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啦。