React的useState原理
useState的用法
function App (){
const [n,setN] = React.State(0)
return(
<div>
{n}
<button onClick={()=>setN(N+1)}>+1</button>
</div>
)
}
点击button发生什么
执行setN的时候会发生什么?n会变吗,App()
会重新执行吗
如果App()会重新执行,那么usetState(0)的时候,n每次值会有不同不同吗
function App (){
const [n,setN] = React.State(0)
return(
<div>
{n}
<button onClick={()=>setN(N+1)}>+1</button>
<button onClick={()=>{console.log(N)}}>+1</button>
</div>
)
}
分析
setN一定会修改数据X,将n+1存入x
useN一定会触发重新渲染(re-render)
useState肯定会从x读取n的最新值
x每个组件有自己的数据x,我们将其命名为state
尝试实现(React.useState)
import React from "react";
import ReactDOM from "react-dom";
function useState(initialValue) {
let state = initialValue;
function setState(newState) {
state = newState;
render();
}
return [state, setState];
}
const render = ()=>{
ReactDOM.render(
<App/>,
document.getElementById('root')
);
}
const App = (PROPS)=>{
const [n,setN] = useState(0)
return(
<div>
<p>{n}</p>
<button onClick={()=>setN(n+1)}>+1</button>
<button onClick={()=>{console.log(n)}}>logn</button>
</div>
)
}
export default App
结果
完全没有变化,,因为usetState会将State重置,需要一个不会被usetState重置的变量,声明在useState外边即可
再次尝试
import React from "react";
import ReactDOM from "react-dom";
let _state
function useState(initialValue) {
_state = _state===undefined?initialValue:_state;
function setState(newState) {
_state = newState;
render();
}
return [_state, setState];
}
const render = ()=>{
ReactDOM.render(
<App/>,
document.getElementById('root')
);
}
const App = (PROPS)=>{
const [n,setN] = useState(0)
return(
<div>
<p>{n}</p>
<button onClick={()=>setN(n+1)}>+1</button>
<button onClick={()=>{console.log(n)}}>logn</button>
</div>
)
}
export default App
冲突
如果一个组件用了**两个usetState怎么办.由于所有数据都放在_state,所以会冲突
import React from "react";
import ReactDOM from "react-dom";
let _state
function useState(initialValue) {
_state = _state===undefined?initialValue:_state;
function setState(newState) {
_state = newState;
render();
}
return [_state, setState];
}
const render = ()=>{
ReactDOM.render(
<App/>,
document.getElementById('root')
);
}
const App = (PROPS)=>{
const [n,setN] = useState(0)
const [m,setM] = useState(0)
return(
<div>
<div>
<p>{n}</p>
<button onClick={()=>setN(n+1)}>+1</button>
<button onClick={()=>setTimeout(()=>{
console.log(n)
},1000)}>logN</button>
</div>
<div>
<p>{m}</p>
<button onClick={()=>setM(n+1)}>+1</button>
<button onClick={()=>setTimeout(()=>{
console.log(m)
},1000)}>logM</button>
</div>
</div>
)
}
export default App
注意:由于组件用了两个usetState功用一个_state,出现冲突,当n改变是m也会改变
改进思路
把_state做成一个对象
比如_state={n:0,m:0},这种思路不行,因为usetState(0)不知道是n还是m变了
把_state做成一个数组
比如_state=[0,0],貌似可以,尝试一下
尝试改变usetState
import React from "react";
import ReactDOM from "react-dom";
let _state=[]
let index=0
function useState(initialValue) {
let currentIndex = index
index += 1
_state[currentIndex] = _state[currentIndex]||initialValue;
const setState=newState=> {
_state[currentIndex] = newState;
render();
}
return [_state[currentIndex], setState];
}
const render = ()=>{
index = 0
ReactDOM.render(
<App/>,
document.getElementById('root')
);
}
const App = (PROPS)=>{
const [n,setN] = useState(0)
const [m,setM] = useState(0)
return(
<div>
<div>
<p>{n}</p>
<button onClick={()=>setN(n+1)}>+1</button>
<button onClick={()=>setTimeout(()=>{
console.log(n)
},1000)}>logN</button>
</div>
<div>
<p>{m}</p>
<button onClick={()=>setM(m+1)}>+1</button>
<button onClick={()=>setTimeout(()=>{
console.log(m)
},1000)}>logM</button>
</div>
</div>
)
}
export default App
注意:index、currentIndex和第16行
_state数组方案的缺点
usetState调用顺序
若第一次渲染是n是第一个,m是第二个,k是第三个
则第二次渲染是必须保证顺序完全一致
所以React不允许出现如下代码
现在的代码还有问题
- App用了_state和index其他组件用什么
解决方法:给每个组件创建一个_state和index
- 放在全局作用域重名怎么版
解决方法:放在组件对应的虚拟节点对象上
视图
总结
- 每一个函数组件对应一个React节点
- 每个节点保存着state和index
- usetState会读取state[index]
- index有usetState出现的顺序决定的
- setState会秀给state,并触发更新
注意:上面的useState对React的实现做了简化
React节点应该是FiberNode,_state的真是名称为memorizedState,index的实现用到了链表,有兴趣可以查看
n的分身
const App = (PROPS)=>{
const [n,setN] = React.useState(0)
const log = ()=>setTimeout(()=>{console.log(n)},1000)
return(
<div>
<div>
<p>{n}</p>
<button onClick={()=>setN(n+1)}>+1</button>
<button onClick={log}>log</button>
</div>
</div>
)
}
不同结果
- 点击+1在点击log——无bug
- 点击log在点击+1——有bug
为什么log出旧的数据
因为有多个n
贯穿始终的状态
-
全局变量(用window即可,不好用)
-
使用useRef(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);
强制更新例子
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);
- useContext(useContext不仅可以贯穿始终,还能贯穿不同的组件)
示例代码
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);
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);
总结
- 每一次重新渲染,组件函数都会执行
- 对应的所有state都会出现(分身)
- 如果不希望出现分身,可以使用useRef/useContext