一. useState
useState是React用来管理变量的一个函数,他的底层实现大概是这个样子的
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
let _state;
//声明一个全局变量_state
function myUseState(initialValue) {
_state = _state || initialValue;
function setState(newState) {
_state = newState;
render();
//state改变后重新渲染
}
return [_state, setState];
}
// 教学需要,不用在意 render 的实现
const render = () => ReactDOM.render(<App />, rootElement);
function App() {
const [n, setN] = myUseState(0);
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
- 声明一个myUseState来代替useState,定义一个全局变量_state
- 当我们第一次渲染App时,调用myUseState,将0赋值给_state,之后再次调用就将全局_state赋值给它
- 在muUseState中声明一个函数setState,他接受一个新的state,并将值赋值给_state,并触发重新渲染
这样我们就实现了一个变量的useState函数,但是如果有多个变量呢,_state的值由谁决定,所以我们对他进行改进
//version 2.0
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
let _state = [];
//将_state定义成一个数组
let index = 0;
//index用于标记变量
function myUseState(initialValue) {
const currentIndex = index;
index += 1;
_state[currentIndex] = _state[currentIndex] || initialValue;
const setState = newState => {
_state[currentIndex] = newState;
//这时候返回的函数currentIndex已经是确定的了
render();
};
return [_state[currentIndex], setState];
}
const render = () => {
index = 0;
ReactDOM.render(<App />, rootElement);
};
function App() {
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(0);
console.log(_state);
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
<p>{m}</p>
<p>
<button onClick={() => setM(m + 1)}>+1</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
- 我们将_state定义成一个变量的数组,每次调用此函数就将index+1并存入下一个变量 2.当setN触发时,将 _state[currentIndex]即n所对应的项更新,并重新渲染
- 重新渲染时重置index,因为如果不重置此时currentIndex就会变成3,使得_state数组加长
由于_state是一个数组,所以每一个变量所对应的下标是不能更改的,每次渲染都要执行myUseState,因此React是不允许在useState外层添加条件判断语句的
至此我们就完成了useState的基本功能的实现,当我们setN的时候并不会直接去把n更改了,而是会先存到_state这样一个中间变量中,然后在再次渲染页面的时候从这个对象中取出n的值并显示在页面中,所以原来的n已经不存在了,这个我们可以用一个异步函数来验证
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
const [n, setN] = React.useState(0);
const log = () => setTimeout(() => console.log(`n: ${n}`), 3000);
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
<button onClick={log}>log</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
当我们点击log时,延迟打印出当前的n,如果我们先点击log,再点击+1会怎样
n:0
log会打印出0而不是1,所以我们知道n这个变量不是同一个,setN会创建一个新的n
这里再补充一点关于setN
1. setN如果改变的是一个对象的部分属性,那么你必须将原来的对象用...展开符拷贝过来再用新属性覆盖
const [user,setUser] = useState({name:'Frank', age: 18})
const onClick = ()=>{
setUser({
...user,
name: 'Jack'
})
}
2.如果我们直接对user.name进行修改是不可行的,因为user的地址没发生改变尽管它的部分属性变了,React还是不会触发更新,所以我们要直接返回一个新的对象而不是对user直接进行修改
如果我们要定义一个唯一的全局n怎么办,用useRef,他能在一个组件定义一个全局变量
二. useRef
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
const nRef = React.useRef(0);
const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
return (
<div className="App">
<p>{nRef.current} 这里并不能实时更新</p>
<p>
<button onClick={() => (nRef.current += 1)}>+1</button>
<button onClick={log}>log</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
- 使用nRef.current获取当前的n,此时log的n和+1的n指的是同一个对象n
- 但是这个nRef也不能自动重新render
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
const nRef = React.useRef(0);
const update = React.useState()[1];
const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
return (
<div className="App">
<p>{nRef.current} 这里并不能实时更新</p>
<p>
<button onClick={() => ((nRef.current += 1), update(nRef.current))}>
+1
</button>
<button onClick={log}>log</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
我们借用React.useState的自动渲染,让+1时触发update
实际上React.useRecf(0)会将变量变成一个对象{current:0}这样才能保证每次引用的对象都是同一个
二-二 forwardRef
import React, { useRef } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const buttonRef = useRef(null);
return (
<div className="App">
<Button3 ref={buttonRef}>按钮</Button3>
</div>
);
}
const Button3 = React.forwardRef((props, ref) => {
return <button className="red" ref={ref} {...props} />;
});
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
由于props本身并不能传递ref属性,所以我们需要用React.forwardRef来对其进行一个封装,这样就可以接受一个ref参数
useRef可以引用一个普通对象,也可以引用一个DOM对象
三. useContext
useContex不仅能贯穿始终还能贯穿组件之间
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const rootElement = document.getElementById("root");
const themeContext = React.createContext(null);
//初始化一个局部全局变量他的作用域就是以下标签部分
function App() {
const [theme, setTheme] = React.useState("red");
return (
<themeContext.Provider value={{ theme, setTheme }}>
<div className={`App ${theme}`}>
<p>{theme}</p>
<div>
<ChildA />
</div>
<div>
<ChildB />
</div>
</div>
</themeContext.Provider>
);
}
function ChildA() {
const { setTheme } = React.useContext(themeContext);
//将setTheme方法从全局变量themeContext中取出
return (
<div>
<button onClick={() => setTheme("red")}>red</button>
</div>
);
}
function ChildB() {
const { setTheme } = React.useContext(themeContext);
return (
<div>
<button onClick={() => setTheme("blue")}>blue</button>
</div>
);
}
ReactDOM.render(<App />, rootElement);
-
初始化一个局部的全局变量
themeContext -
themeContext.Provider value={{ theme:theme, setTheme:setTheme }}将一个对象作为他的值
3.在 <themeContext.Provider></themeContext.Provider>标签内,以及任何子代都可以访问到这个对象
4.子组件取出此对象的setTheme方法后就可以改变themeContext的theme属性, 因此这个方法很适合用于全局切换主题颜色
5.数据更新过程是自上而下的,最外层的n发生了改变依次检查父组件,子组件,孙组件是否引用了此变量
四. useReducer
useReducer其实就是高级版的useState,他是Flux和Redux思想的一个实践
import React, { useState, useReducer } from "react";
import ReactDOM from "react-dom";
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 * 2 };
} else {
throw new Error("unknown type");
}
};
function App() {
const [state, dispatch] = useReducer(reducer, initial);
const { n } = state;
const onClick = () => {
dispatch({ type: "add", number: 1 });
};
const onClick2 = () => {
dispatch({ type: "add", number: 2 });
};
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+1</button>
<button onClick={onClick2}>+2</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
他的实现分4步
- 创建一个初始值
const initial = {
n: 0
};
- 将所有setState操作都封装到reducer中
const reducer = (state, action) => {
if (action.type === "add") {
return { n: state.n + action.number };
} else if (action.type === "multi") {
return { n: state.n * 2 };
} else {
throw new Error("unknown type");
}
};
- 将初始值和操作传给useReducer
const [state, dispatch] = useReducer(reducer, initial);
//注意参数顺序,第一参数为reducer
- 调用dispatch,将action对象作为参数传给reducer
const onClick = () => {
dispatch({ type: "add", number: 1 });
};
总的来说如果你的数据的写操作过多,就可以将它封装到一起,而简单操作就可以使用useState
如何用useReducer和useContext代替Redux
五. useEffect和useLayoutEffect
useEffect见函数组件
useLayoutEffect
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const BlinkyRender = () => {
const [value, setValue] = useState(0);
useEffect(() => {
document.querySelector('#x').innerText = `value: 1000`
}, [value]);
//页面会先出现一个0然后变成1000,使用useLayoutEffect就不会闪烁
return (
<div id="x" onClick={() => setValue(0)}>value: {value}</div>
);
};
ReactDOM.render(
<BlinkyRender />,
document.querySelector("#root")
);

useLayoutEffect相当于在DOM更新到页面之前执行,这样useState的初始化DOM就会被覆盖,所以这个一般用在render后立马操作DOM,但是这个会延长浏览器渲染页面时间
六. useMemo
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child data={m}/>
{/* <Child2 data={m}/> */}
</div>
);
}
function Child(props) {
console.log("child 执行了");
console.log('假设这里有大量代码')
return <div>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
上述例子中,Child组件接受一个props依赖于m,但是当我们改变n的时候,App重新渲染,但是Child组件也渲染了,这是React的默认多余渲染,所以我们可以用React .memo来对他进行一个封装,这样只有当自己的依赖改变时才会重新渲染
但是这个函数有个一个bug,那就是当我们接受一个外部的函数,比如onclick,那么在App渲染之后,都会重新定义一个新的onclick,函数地址发生了改变,所以child组件还是重新渲染了
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
const onClickChild = () => {
console.log(m);
};
//每次重渲染会执行此句生成一个新函数地址
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child2 data={m} onClick={onClickChild} />
{/* Child2 居然又执行了 */}
</div>
);
}
function Child(props) {
console.log("child 执行了");
console.log("假设这里有大量代码");
return <div onClick={props.onClick}>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useMemo
const onClickChild = useMemo(() => {
const fn = div => {
console.log("on click child, m: " + m);
console.log(div);
};
return fn;
}, [m]); // 这里呃 [m] 改成 [n] 就会打印出旧的 m
将上例中的onClickChild用useMemo返回,并添加一个依赖m,这样在m改变时才会重新渲染
useCallback
const onClickChild = useCallback(
const fn = div => {
console.log("on click child, m: " + m);
console.log(div);
};
return fn;
, [m])
useCallback就是省略了返回函数前的箭头函数,直接返回一个函数
七. 自定义Hook
//useList Hook
import { useState, useEffect } from "react";
const useList = () => {
const [list, setList] = useState(null);
useEffect(() => {
ajax("/list").then(list => {
setList(list);
});
}, []); // [] 确保只在第一次运行
return {
list: list,
setList: setList
};
};
export default useList;
在这里进行一系列操作,然后将useState的读和写接口返回给外部调用
import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";
function App() {
const { list, setList } = useList();
//运行后自动发送ajax请求,得到读写接口
return (
<div className="App">
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
甚至我们可以在自定义Hook中返回更多的操作接口
return {
list: list,
addItem: name => {
setList([...list, { id: Math.random(), name: name }]);
},
deleteIndex: index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}
};
};
外部可以直接调用,一个字爽~~~