淺析React中的useState
1. 簡(jiǎn)單的 useState 實(shí)現(xiàn)
function App() { // 簡(jiǎn)單的 +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
);
- 首次渲染頁面展示內(nèi)容 0 和 按鈕,會(huì)調(diào)用App函數(shù)
- 調(diào)用 App 函數(shù)會(huì)獲得一個(gè)對(duì)象,可以認(rèn)為這個(gè)對(duì)象是一個(gè)虛擬的DOM
- 當(dāng)用戶點(diǎn)擊 按鈕時(shí)會(huì)調(diào)用 setN 函數(shù),并且再次調(diào)用 App函數(shù) 渲染App組件
- 每次調(diào)用React.useState時(shí)返回的n值應(yīng)該不一樣
嘗試實(shí)現(xià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]
}
// 這是對(duì) render 的簡(jiǎn)化
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
);
會(huì)發(fā)現(xiàn)可以實(shí)現(xiàn) n + 1 功能
但是有這樣一個(gè)問題,如果一個(gè)組件用了兩個(gè)useState怎么辦?由于所有數(shù)據(jù)都放在_state,所以會(huì)沖突
const [n,setN] = myUseState(0)
const [m,setM] = myUseState(0) // _state 會(huì)變成后面的 m
改進(jìn)思路
- 把_state做成一個(gè)對(duì)象
- 比如_state ={n: 0, m: 0}
- 不行,因?yàn)閡seState(0)并不知道變量叫n還是m
- 把_state做成數(shù)組
- 比如_state =[0, 0]
- 貌似可行,我們來試試看
let _state = [] // 存放多個(gè)數(shù)據(jù)
let index = 0 // 數(shù)據(jù)下標(biāo)
function myUseState(initialValue) {
const currentIndex = index // 保留當(dāng)前下標(biāo)
_state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex] // 只有首次才使用初始值
function setState(newValue) { // set函數(shù)
_state[currentIndex] = newValue
render()
}
index++
return [_state[currentIndex], setState]
}
const render = () => {
index = 0 // 每次運(yùn)行 App 函數(shù)之前需要重置index 否則 會(huì)增加_state 數(shù)組長(zhǎng)度
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 數(shù)組方案的缺點(diǎn)
依賴useState的調(diào)用順序
- 若第一次渲染時(shí)n是第一個(gè),m是第二個(gè),k是第三個(gè)
- 則第二次渲染時(shí)必須保證順序完全一致
- 所以React不允許出現(xiàn)如下代碼,不能在判斷里面使用useState
function App(){
const [n,setN] = React.useState(0)
let m,setM
if(n % 2 === 1){
[m,setM] = React.useState(0) // 這句會(huì)報(bào)錯(cuò)
}
}
代碼還有一個(gè)問題,App用了_state 和 index,那其他組件用什么 ?
答:給每個(gè)組件創(chuàng)建一個(gè)_state 和index
又有問題,放在全局作用域里重名了咋整 ?
答:放在組件對(duì)應(yīng)的虛擬節(jié)點(diǎn)對(duì)象上
2. useState簡(jiǎn)單原理總結(jié)
- 每個(gè)函數(shù)組件對(duì)應(yīng)一個(gè)React節(jié)點(diǎn)
- 每個(gè)節(jié)點(diǎn)保存著 state 和 index
- useState 會(huì)讀取 state[index]
- index 由 useState 出現(xiàn)的順序決定
- setState 會(huì)修改 state,并觸發(fā)更新
注意:以上代碼對(duì)React的實(shí)現(xiàn)做了簡(jiǎn)化,React 節(jié)點(diǎn)應(yīng)該是 FiberNode,_state的真實(shí)名稱為memorizedState,index的實(shí)現(xiàn)則用到了鏈表。
3. useRef 與 useContext
新手 對(duì) 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>
);
}
當(dāng)我先點(diǎn)擊加一,再 log,與先log立即點(diǎn)擊加一 打印結(jié)果不一樣,后者會(huì)打印舊的值
這是因?yàn)橄赛c(diǎn)擊加一按鈕時(shí)會(huì)觸發(fā) render 函數(shù),相當(dāng)于再次運(yùn)行 App 函數(shù),此時(shí)的 n 為加一后的值;當(dāng)我先點(diǎn)擊log再立即點(diǎn)擊加一時(shí),log函數(shù)中使用的 n 值保留的是舊的值(或者理解為上一個(gè)App函數(shù)中的舊的變量),因此不會(huì)打印新的值,當(dāng)然沒有對(duì)舊值的引用時(shí),舊n會(huì)被垃圾回收掉
那假如我希望有一個(gè) n 能夠貫穿始終,在 React中應(yīng)該怎么辦呢?
- 使用全局變量
這樣可以,但是太low了
- useRef
useRef 不僅可以用于div,還能用于任意數(shù)據(jù)
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} 這里并不能實(shí)時(shí)更新</p>
<p>
<button onClick={() => {nRef.current += 1;update(nRef.current)}>+1</button>
<button onClick={log}>log</button>
</p>
</div>
);
}
// update 為了讓App 重新渲染
- useContext 不僅能貫穿始終,還能貫穿不同組件
點(diǎn)擊換顏色例子
const themeContext = React.createContext(null); // 其實(shí)類似全局變量
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 作用域在這個(gè)標(biāo)簽內(nèi)
);
}
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>
);
}
總結(jié):
- 每次重新渲染,組件函數(shù)就會(huì)執(zhí)行
- 對(duì)應(yīng)的所有state都會(huì)出現(xiàn) 「分身」
- 如果你不希望出現(xiàn)分身
- 可以用usaRef / useContext等