首先創(chuàng)建一個App組件槽畔,加入一個按鈕和點擊后顯示的值num,在按鈕上綁定click事件,每次點擊网棍,num++
function App() {
console.log('---app run again----')
const [num, setNum] = useState(0)
console.log('---render----')
console.log(`num:${num}`)
return (
<div className='App'>
<p>{num}</p>
<p>
<button
onClick={() => {
setNum(num + 1)
console.log(num)
}}
>
+1
</button>
</p>
</div>
)
}
在首次渲染的時候調(diào)用App() ---> 運行render() ---> 生成虛擬dom ---> 作用于真實dom<br />用戶點擊button ---> 調(diào)用App() --->調(diào)用setNum(n+1) ---> 運行render() ---> dom diff ---> 作用于真實dom
每次調(diào)用App的時候袜炕,useState都會執(zhí)行。<br />
state是異步的
在控制臺中讹堤,我們可以看到祭刚,打印的num并不是頁面上顯示的結(jié)果,這是因為react中state的更新是異步的丛肮。當(dāng)我們setState后赡磅,react并不會立即將值做出改變,而是將其暫時放入pedding隊列中宝与。react會合并多個state焚廊,然后只render 一次。
<a name="4fH49"></a>
useState 實現(xiàn)
const newUseState = intialValue => {
let state = intialValue
console.log('newUseState run...')
const setState = newValue => {
state = newValue
reRender()
}
return [state, setState]
}
const reRender = () => {
ReactDOM.render(<App />, document.getElementById('root'))
}
此時我們习劫,我們在App中使用newUseState
console.log('---app run again----')
const [num, setNum] = newUseState(0)
console.log('---render----')
console.log(`num:${num}`)
但是咆瘟,發(fā)現(xiàn)什么用都沒有,num 一直是0
這是由于每次App()調(diào)用后诽里,num就被初始化為0袒餐,如果不想每次調(diào)用App后被初始化,可以在newUseState外邊定義一個臨時變量來存放set之后的值.
let _state = null
const newUseState = intialValue => {
_state = _state === null ? intialValue : _state
console.log('newUseState run...')
const setState = newValue => {
_state = newValue
reRender()
}
return [_state, setState]
}
此時谤狡,點擊+1后灸眼,num就做出更新。
如果有兩個 newUseState
const [num, setNum] = newUseState(0)
const [m, setM] = newUseState(20)
此時外部變量_state 存放的num墓懂,會被后面的maxNum覆蓋幢炸,變?yōu)?20.
改變newUseState類型
1. 使_state為對象
let _state = { num:0, m:20 }
但是使用newUseState(0)的時候,我們無法知道賦值給的是num還是maxNum
2. 使_state為數(shù)組
let _state = [0, 20]
此時拒贱,我們的newUseState也要進行修改
let _state = []
let index = 0
const newUseState = intialValue => {
const currentIndex = index
_state[currentIndex] =
_state[currentIndex] === undefined ? intialValue : _state[currentIndex]
console.log('newUseState run...')
const setState = newValue => {
_state[currentIndex] = newValue
console.log('---after-set----')
console.log(_state)
reRender()
}
index++
return [_state[currentIndex], setState]
}
我們把m也放到頁面上
function App() {
console.log('---app run again----')
const [num, setNum] = newUseState(0)
const [m, setM] = newUseState(20)
console.log('---render----')
console.log(`num:${num}`)
return (
<div className='App'>
<p>{num}</p>
<p>
<button
onClick={() => {
setNum(num + 1)
console.log(`num++`)
console.log(num)
}}
>
num+1
</button>
</p>
<p>{m}</p>
<p>
<button
onClick={() => {
setM(m + 1)
console.log(`m++`)
console.log(m)
}}
>
m+1
</button>
</p>
</div>
)
}
但是宛徊,此時點擊按鈕不生效
是由于每次render運行的時候,index還保存著上次的值逻澳,導(dǎo)致數(shù)組變長闸天。應(yīng)該在render函數(shù)觸發(fā)前將index的值變?yōu)?.
const reRender = () => {
index = 0
ReactDOM.render(<App />, document.getElementById('root'))
}
此時,達到了我們想要的效果斜做。
newUseState 使用數(shù)組的'缺陷'
之前苞氮,我們使用數(shù)組和外部變量index,實現(xiàn)了多個newUseState瓤逼,使得組件中能夠使用多個state笼吟。但是實際上還有一些不是那么方便的地方。
1. 只能按順序調(diào)用
在第一次渲染的時候霸旗,第一個值是num贷帮,第二個值是m,那么當(dāng)App()再次被調(diào)用的時候诱告,下一次撵枢,還得保持這個順序,否則就會出錯。先把之前App的代碼微做修改
function App() {
console.log('---app run again----')
const [num, setNum] = newUseState(0)
let m, setM
if (num % 2 === 0) {
;[m, setM] = newUseState(20)
}
console.log('---render----')
console.log(`num:${num}`)
console.log(`m:${m}`)
return (
<div className='App'>
<p>{num}</p>
<p>
<button
onClick={() => {
setNum(num + 1)
console.log(`num++`)
console.log(num)
}}
>
num+1
</button>
</p>
<p>{m}</p>
<p>
<button
onClick={() => {
setM(m + 1)
console.log(`m++`)
console.log(m)
}}
>
m+1
</button>
</p>
</div>
)
}
初始化的時候锄禽,m就為undefined
再次點擊m+1就會報錯
我們再次將newUseState 換成 React.useState
此時編輯器就會提示useState被有條件的調(diào)用潜必,hooks必須按照完全一樣的順序渲染。
2.App使用了useState,其他組件用什么
react為每個組件創(chuàng)建了memorisedState和index沃但,并且將其放在對應(yīng)的虛擬dom上磁滚,這樣,假如App()有m宵晚,Example()也可以擁有m垂攘,不會重復(fù)。
1.創(chuàng)建Example組件坝疼,包含和App同樣的m
function Example() {
const [num, setNum] = useState(0)
return (
<>
<p>examples: {num}</p>
<p>
<button
onClick={() => {
setNum(num + 1)
console.log(`num++`)
console.log(num)
}}
>
example num+1
</button>
</p>
</>
)
}
2.修改App的return
return (
<div className='App'>
<p>{num}</p>
<p>
<button
onClick={() => {
setNum(num + 1)
console.log(`num++`)
console.log(num)
}}
>
num+1
</button>
</p>
<p>{m}</p>
<p>
<button
onClick={() => {
setM(m + 1)
console.log(`m++`)
console.log(m)
}}
>
m+1
</button>
</p>
<div>
<Example />
</div>
</div>
)
我們點擊各自的num+1搜贤,互不干擾
useState的set方法每次set的都是不同的值(相當(dāng)于set的分身)
我們創(chuàng)建一個+1 button還有一個log button
function App() {
const [num, setNum] = useState(0)
const log = () =>
setTimeout(() => {
console.log(`num:${num}`)
}, 2000)
return (
<div className='App'>
<p>{num}</p>
<p>
<button
onClick={() => {
setNum(num + 1)
}}
>
+1
</button>
<button onClick={log}>log now</button>
</p>
</div>
)
}
當(dāng)我們先點+1谆沃,然后再點log,此時num進行了+1操作钝凶,2秒后打出的num=1也是預(yù)期的結(jié)果
但是當(dāng)我們先點擊log,由于是延時2秒觸發(fā)唁影,我們點下2次+1耕陷,此時打出的num竟然是0
這是由于當(dāng)num=0時,我們觸發(fā)了log据沈,但是它兩秒后執(zhí)行l(wèi)og(num=0).當(dāng)我們先點+1哟沫,然后在點log時,我們兩秒后觸發(fā)的是log(num=1).set操作的相當(dāng)于每次都是一個副本锌介。
解決方法1
1.使用useRef貫穿整個周期
function App() {
const numRef = useRef(0)
const log = () =>
setTimeout(() => {
console.log(`num:${numRef.current}`)
}, 2000)
return (
<div className='App'>
<p>{numRef.current}</p>
<p>
<button
onClick={() => {
numRef.current++
}}
>
+1
</button>
<button onClick={log}>log now</button>
</p>
</div>
)
}
此時無論先點log還是先點+1嗜诀,都能得到我們預(yù)期的結(jié)果。但是此時孔祸,頁面上的num仍然是0隆敢,因為useRef不會觸發(fā)render函數(shù)。react更傾向于函數(shù)式崔慧,它希望每次操作的并不是同一個值拂蝎,這點有別于vue。
2.強制更新
function App() {
const numRef = useRef(0)
const log = () =>
setTimeout(() => {
console.log(`num:${numRef.current}`)
}, 2000)
const forceUpdate = useState(null)[1]
return (
<div className='App'>
<p>{numRef.current}</p>
<p>
<button
onClick={() => {
numRef.current++
forceUpdate(numRef.current)
}}
>
+1
</button>
<button onClick={log}>log now</button>
</p>
</div>
)
}
我們創(chuàng)建一個forceUpdate方法惶室,讓其一開始傳入為null温自,之后每次點擊傳入numRef.current,這時就可以強制render,達到我們的預(yù)期效果
之前皇钞,我們使用useRef創(chuàng)建了一個貫穿App組件的變量悼泌,并且通過創(chuàng)建一個無用的state,來達到強制更新組件的目的夹界。但是這樣做券躁,并不是很好。
解決方法2:使用useContext創(chuàng)建貫穿不同組件的變量
首先創(chuàng)建兩個子組件ChildA和ChildB
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>
);
}
改造下chilA和childB的父組件App
function App() {
const [theme, setTheme] = React.useState("red");
return (
<div className={`App ${theme}`}>
<p>{theme}</p>
<div>
<ChildA />
</div>
<div>
<ChildB />
</div>
</div>
);
}
增加兩個css類
.red button {
background: red;
color: white;
width: 100px;
line-height: 40px;
height: 40px;
border-radius: 4px;
}
.blue button {
background: blue;
color: white;
width: 100px;
line-height: 40px;
height: 40px;
border-radius: 4px;
}
此時變成這樣。接下來創(chuàng)建App的context也拜,來傳遞給子組件ChildA和ChildB.
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={() =>
setTimeout(() => {
setTheme('red')
}, 2000)
}
>
red
</button>
</div>
)
}
function ChildB() {
const { setTheme } = React.useContext(themeContext)
return (
<div>
<button
onClick={() =>
setTimeout(() => {
setTheme('blue')
}, 2000)
}
>
blue
</button>
</div>
)
}
此時以舒,ChildA和ChildB中操作的theme都是通過Context傳過來的,也就是它們修改的都是同一個值慢哈。
此時點擊后也就能生效了蔓钟。