浅析React中的useState
1. 简单的 useState 实现
function App() { // 简单的 +1 案例
const [n, setN] = React.useState(0)
return (
<div className='App'>
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>n+1</button>
</p>
</div>
)
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App/>,
rootElement
);
- 首次渲染页面展示内容 0 和 按钮,会调用App函数
- 调用 App 函数会获得一个对象,可以认为这个对象是一个虚拟的DOM
- 当用户点击 按钮时会调用 setN 函数,并且再次调用 App函数 渲染App组件
- 每次调用React.useState时返回的n值应该不一样
尝试实现 React.useState
let _state = undefined // 模拟 state
function myUseState(initialValue) { // 模拟useState
_state = _state === undefined ? initialValue : _state // 只在第一次使用初始值
function setState(newValue) {
_state = newValue
render()
}
return [_state, setState]
}
// 这是对 render 的简化
const render = () => ReactDOM.render(<App/>, document.getElementById("root"))
function App() {
const [n, setN] = myUseState(0)
return (
<div className='App'>
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>n+1</button>
</p>
</div>
)
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App/>,
rootElement
);
会发现可以实现 n + 1 功能
但是有这样一个问题,如果一个组件用了两个useState怎么办?由于所有数据都放在_state,所以会冲突
const [n,setN] = myUseState(0)
const [m,setM] = myUseState(0) // _state 会变成后面的 m
改进思路
- 把_state做成一个对象
- 比如_state ={n: 0, m: 0}
- 不行,因为useState(0)并不知道变量叫n还是m
- 把_state做成数组
- 比如_state =[0, 0]
- 貌似可行,我们来试试看
let _state = [] // 存放多个数据
let index = 0 // 数据下标
function myUseState(initialValue) {
const currentIndex = index // 保留当前下标
_state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex] // 只有首次才使用初始值
function setState(newValue) { // set函数
_state[currentIndex] = newValue
render()
}
index++
return [_state[currentIndex], setState]
}
const render = () => {
index = 0 // 每次运行 App 函数之前需要重置index 否则 会增加_state 数组长度
ReactDOM.render(<App/>, document.getElementById("root"))
}
function App() {
const [n, setN] = myUseState(0)
const [m, setM] = myUseState(0)
return (
<div className='App'>
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>n+1</button>
</p>
<p>{m}</p>
<p>
<button onClick={() => setM(m + 1)}>n+1</button>
</p>
</div>
)
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App/>,
rootElement
);
_state 数组方案的缺点
依赖useState的调用顺序
- 若第一次渲染时n是第一个,m是第二个,k是第三个
- 则第二次渲染时必须保证顺序完全一致
- 所以React不允许出现如下代码,不能在判断里面使用useState
function App(){
const [n,setN] = React.useState(0)
let m,setM
if(n % 2 === 1){
[m,setM] = React.useState(0) // 这句会报错
}
}
代码还有一个问题,App用了_state 和 index,那其他组件用什么 ?
答:给每个组件创建一个_state 和index
又有问题,放在全局作用域里重名了咋整 ?
答:放在组件对应的虚拟节点对象上
2. useState简单原理总结
- 每个函数组件对应一个React节点
- 每个节点保存着 state 和 index
- useState 会读取 state[index]
- index 由 useState 出现的顺序决定
- setState 会修改 state,并触发更新
注意:以上代码对React的实现做了简化,React 节点应该是 FiberNode,_state的真实名称为memorizedState,index的实现则用到了链表。
3. useRef 与 useContext
新手 对 n 值的分身问题疑惑问题
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>
);
}
当我先点击加一,再 log,与先log立即点击加一 打印结果不一样,后者会打印旧的值
这是因为先点击加一按钮时会触发 render 函数,相当于再次运行 App 函数,此时的 n 为加一后的值;当我先点击log再立即点击加一时,log函数中使用的 n 值保留的是旧的值(或者理解为上一个App函数中的旧的变量),因此不会打印新的值,当然没有对旧值的引用时,旧n会被垃圾回收掉
那假如我希望有一个 n 能够贯穿始终,在 React中应该怎么办呢?
- 使用全局变量
这样可以,但是太low了
- useRef
useRef 不仅可以用于div,还能用于任意数据
function App() {
const nRef = React.useRef(0);
const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
const uadate = React.useState(null)[1]
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>
);
}
// update 为了让App 重新渲染
- useContext 不仅能贯穿始终,还能贯穿不同组件
点击换颜色例子
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> // themeContext 作用域在这个标签内
);
}
function ChildA() {
const { setTheme } = React.useContext(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>
);
}
总结:
- 每次重新渲染,组件函数就会执行
- 对应的所有state都会出现 「分身」
- 如果你不希望出现分身
- 可以用usaRef / useContext等