一週的磚又快搬完了,又到了開心快樂的總結時間~這兩週一直在 hook 函數的「坑」裏,久久不能自拔。應該也不能叫作「坑」吧,仍是本身太菜了,看文檔不仔細,不少覺得不重要,但在實際應用中卻很關鍵的點老是被本身忽略。因此我準備多花點時間,把官網的一些 hook 函數,再回過頭看一遍,整理整理(在整理的過程,我以爲更容易發現問題和總結經驗)。html
這篇文章主要整理一下 React 中的三個基礎 Hook:前端
useState
相比其餘 hooks 仍是很簡單的,主要就是用來定義變量。官方文檔描述的也很清楚,對此已經很熟練的看官大大能夠跳過哦~react
const [count, setCount] = useState(0)
複製代碼
調用 useState
方法作了什麼?git
定義一個「state變量」。github
useState
須要什麼參數?npm
useState
方法接收一個參數,做爲變量初始化的值。(示例中調用 useState
方法聲明一個 「state變量」 count
,默認值爲 0。)redux
useState
方法的返回值是什麼?數組
返回當前 state 以及更新 state 的函數。瀏覽器
React 會確保 setState 函數的標識是穩定的,而且不會在組件從新渲染時發生變化。這就是爲何能夠安全地從 useEffect 或 useCallback 的依賴列表中省略 setState。安全
首先咱們先經過 useState
方法定義三個變量(包含基本類型和引用類型的數據)分別爲:count
、studentInfo
、subjectList
,而後對它們的值進行修改。
const [count, setCount] = useState(0)
const [studentInfo, setStudentInfo] = useState({name: '小文', age: 18, gender: '女'})
const [subjectList, setSubjectList] = useState([
{ id: 0, project_name: '語文' },
{ id: 1, project_name: '數學' }
])
複製代碼
setCount(1)
複製代碼
setStudentInfo({
...studentInfo,
age: 20,
weight: 90
})
複製代碼
{ id: 2, project_name: '音樂' }
# 忽略這裏的深拷貝,優雅的方式有不少:immutable.js、immer.js、loadsh
let temp_subjectList = JSON.parse(JSON.stringify(subjectList))
temp_subjectList[1].project_name = '體育'
temp_subjectList[2] = { id: 2, project_name: '音樂' }
setSubjectList(temp_subjectList)
複製代碼
咱們在實際的開發中,會用到 React 提供的 Eslint 插件來檢查 Hook 的規則和 effect 的依賴,當檢測出某一塊的代碼缺乏依賴時,會給出警告,若是給出的警告是缺乏 setState
函數,那咱們就能夠忽略它。(後面講到 useEffect 的時候會再補充)
React 會確保
setState
函數的標識是穩定的,而且不會在組件從新渲染時發生變化。這就是爲何能夠安全地從useEffect
或useCallback
的依賴列表中省略 setState。(官網)
若是新的 state 須要經過使用先前的 state 計算得出,那麼能夠將函數傳遞給 setState。該函數將接收先前的 state,並返回一個更新後的值。(對於引用類型的數據,上面同樣,例如數組。也是不能夠直接對變量進行操做的)
使用上面定義的變量:
<button onClick={() => setCount(prevCount => ++prevCount)}>+ 累加</button>
複製代碼
setStudentInfo(prevState => {
# 也可使用 Object.assign
return {...prevState, age: 20}
})
複製代碼
若是初始 state 須要經過複雜計算得到,則能夠傳入一個函數,在函數中計算並返回初始的 state,此函數只在初始渲染時被調用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props)
return initialState
})
複製代碼
咱們在實際應用中,常常會遇到一些結構比較複雜的數據,若是每一個地方都使用 useState 去定義這些複雜結構的數據,估計會累死。
這裏就分享一下我在項目中使用的一個插件 use-immer
,一些基本類型數據和「直接替換的數據」(例若有一個數組 arr,對 arr 修改是直接賦值 setArr([...]),這樣的數據我會選擇用 useState 來聲明,對於大部分引用類型的數據我會使用 use-immer
提供的 useImmer
方法來聲明。下面咱們就來看看它是如何使用的吧~
npm install immer use-immer
複製代碼
import { useImmer } from 'use-immer'
複製代碼
subjectList
const [subjectList, setSubjectList] = useImmer([
{ id: 0, project_name: '語文' },
{ id: 1, project_name: '數學' }
])
複製代碼
{ id: 2, project_name: '音樂' }
setSubjectList(draft => {
draft[1].project_name = '體育'
draft[2] = { id: 2, project_name: '音樂' }
})
複製代碼
須要注意的是,這裏的 setSubjectList
方法接收的是一個函數,該函數接收一個參數 draft
,能夠理解爲是變量 subjectList
的副本。這種寫法是否是有種 「家」 的感受呢,感興趣的能夠深刻了解一下哦(immutable、immer、use-immer)。
關於 useEffect
函數,我我的的建議是先把官網上的介紹看一遍,再多研讀研讀《useEffect 完整指南》。看完會發現,咱們對它已經有了更加深入的認識。這裏也僅僅是我在學習的過程整理的筆記,內容就是《useEffect 完整指南》簡化。
useEffect
會在瀏覽器繪製後延遲執行,但會保證在任何新的渲染前執行。React 將在組件更新前刷新上一輪渲染的 effect。
重點:關於每一次渲染(rendering),組件都會擁有本身的:
寫一個計數器組件 Counter
:
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
複製代碼
Counter
組件第一次渲染的時候,從 useState()
拿到 count
的初始值爲 0。當咱們調用 setCount(1)
時,React 會再次渲染該組件,此時 count
的值爲 1,以此類推,每一次渲染都是獨立的。
# During first render
function Counter() {
const count = 0; # Returned by useState()
# ...
<p>You clicked {count} times</p>
# ...
}
# After a click, our function is called again
function Counter() {
const count = 1; # Returned by useState()
# ...
<p>You clicked {count} times</p>
# ...
}
# After another click, our function is called again
function Counter() {
const count = 2; # Returned by useState()
# ...
<p>You clicked {count} times</p>
# ...
}
複製代碼
Counter
組件中的 count
僅僅是一個常量,這個常量由 React 提供。當調用 setCount
的時候,React 會帶着一個不一樣的 count
值再次調用組件。而後,React會更新DOM以保持和渲染輸出一致。
最關鍵 的就是:任意一次渲染中的 count
常量都不會隨着時間改變。渲染輸出會變是由於 Counter
組件被調用,而在每一次調用引發的渲染中,它包含的 count
常量都是獨立的。也就是說組件的每次渲染,props 和 state 都是獨立的。
修改一下計數器組件 Counter
的例子。
組件內容:有兩個按鈕,一個按鈕用來修改 count
的值,另外一個按鈕在 3s 延遲後展現彈窗。
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count)
}, 3000)
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
)
}
複製代碼
count
的值爲 3。count
的值爲 5。此時,彈窗中的展現的 count
值爲 3。
分析:
首先整個過程進行了 6 次渲染。
count
值爲 3,進行 3 次渲染:render1 -> render2 -> render3;count
值爲 5,進行 2 次渲染:render3 -> render5 -> render5;# 組件狀態:render0 -> render1 -> render2 -> render3
function Counter() {
const count = 3
# ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count)
}, 3000)
}
# ...
}
# 組件狀態: render3
# 觸發事件處理函數 handleAlertClick,此時該函數捕獲 count 值爲 3,並將在 3 秒後打開彈窗。
function Counter() {
const count = 3
# ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + 3)
}, 3000)
}
# ...
}
# 組件狀態:render3 -> render5 -> render5
function Counter() {
const count = 5
# ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count)
}, 3000)
}
# ...
}
複製代碼
修改 Counter
組件,點擊 3 次按鈕:
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`)
}, 3000)
})
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
複製代碼
分析: 整個過程組件進行了四次渲染:
You clicked 0 times
;count
值爲1,render1:打印 You clicked 1 times
;count
值爲2,render2:打印 You clicked 2 times
;count
值爲3,render3:打印 You clicked 3 times
;經過整個例子咱們能夠知道,在每次渲染中,useEffect 也是獨立的。
並非 coun t的值在「不變」的 effect 中發生了改變,而是 effect 函數自己在每一次渲染中都不相同。
當咱們在 useEffect
中使用了定時器或者添加了某些訂閱,能夠經過 useEffect
返回一個函數,進行清除定時器或者取消訂閱等操做。但咱們須要知道的是,清除是 「滯後」 的。(這裏是我的的理解,可能描述的不許確)
看一下例子,在 useEffect
中打印點擊的次數:
function Example() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log(`You clicked ${count} times`)
return() => {
console.log('銷燬')
}
})
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
複製代碼
點擊按鈕 3 次,控制檯中打印的結果以下:
You clicked 0 times
You clicked 1 times
You clicked 2 times
You clicked 3 times
從打印結果咱們能夠很容易看出,上一次的 effect 是在從新渲染時被清除的。
補充:那麼組件的整個從新渲染的過程是怎麼樣的呢?
假設如今有 render0 和 render1 兩次渲染:
React 只會在瀏覽器繪製後運行 effects。這使得你的應用更流暢由於大多數effects並不會阻塞屏幕的更新。
經過下面這個例子,來印證一下這個結論吧~
function Example() {
const [count, setCount] = useState(0)
useEffect(() => {
setCount(99)
console.log(count)
return() => {
console.log('銷燬')
}
})
console.log('我確定最早執行!')
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
複製代碼
運行代碼,在控制檯咱們能夠看到如下輸出(此時組件狀態爲初始化:render0):
根據打印結果,咱們能夠分析出:
useEffect
;setCount
方法修改 count
值爲 99,組件從新渲染(render1);useEffect
,打印 count
的值爲 0。(render0 下 count
值爲0)useEffect
的清除函數;useEffect
;setCount
方法修改 count
值爲 99(因爲傳入的值沒有改變,因此組件沒有從新渲染);count
的值爲 99;其中 我確定最早執行!
,這個打印我理解的是:組件被調用了,React 判斷是否須要渲染。而後纔有了上面的一系列步驟,若是理解有誤還請幫忙指出。
實際應用中,咱們不須要在每次組件更新時,都去執行某些 effects,這個時候咱們能夠給 useEffect
設置依賴,告訴 React 何時去執行 useEffect
。
看下面這個例子,只有在 name
發生改變時,纔會執行這個 useEffect
。若是將依賴設置爲空數組,那麼這個 useEffect
只會執行一次。
useEffect(() => {
document.title = 'Hello, ' + name
}, [name])
複製代碼
引出問題:首先需求很簡單,經過定時器,每過一秒就將 count
的值累加 1。
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(id)
}, [])
複製代碼
咱們只但願設置一次 setInterval
定時器,因此將依賴設置爲了 []
,可是因爲組件每次渲染擁有獨立的 state 和 effects,因此上面代碼中的 count
值,一直是 0,當一次執行完 setCount
後,後續的setCount
操做都是無效的。
那既然這樣,咱們能夠在依賴裏面添加依賴 count
就能夠解決問題了吧?思路是正確的,可是這樣就違背了咱們 「咱們只但願 setInterval
執行一次」 的初衷,且極可能形成一些沒必要要的bug。
解決方案:使用函數式更新(前面有講到的哦)。
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount(preCount +> preCount + 1)
}, 1000)
return () => clearInterval(id)
}, [])
複製代碼
可是在實際應用中,這種方式還遠遠不能知足咱們的需求。好比在依賴多個數據的時候:
function Counter() {
const [count, setCount] = useState(0)
const [step, setStep] = useState(1)
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step)
}, 1000);
return () => clearInterval(id)
}, [step])
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => setStep(Number(e.target.value))} />
</>
)
}
複製代碼
當咱們在修改 step
變量時,會從新設置定時器。這是咱們不肯意看到的,那應該怎麼去優化呢?這個時候咱們就須要用到useReducer
了。
當咱們想更新一個狀態,而且這個狀態更新依賴於另外一個狀態的值時,咱們可能須要使用
useReducer
去替換它們。
import React, { useReducer, useEffect } from 'react'
import ReactDOM from 'react-dom'
const initialState = {
count: 0,
step: 1,
}
function reducer(state, action) {
const { count, step } = state
if (action.type === 'tick') {
return { count: count + step, step }
} else if (action.type === 'step') {
return { count, step: action.step }
} else {
throw new Error()
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState)
const { count, step } = state
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' })
}, 1000)
return () => clearInterval(id)
}, [dispatch])
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => {
dispatch({
type: 'step',
step: Number(e.target.value)
})
}} />
</>
)
}
複製代碼
React 會保證 dispatch 在組件的聲明週期內保持不變。
在 useEffect
中調用了定義在外部的函數時,咱們可能會遺漏依賴。因此咱們能夠將函數的定義放到useEffect
中。
可是當咱們有一些可複用的函數定義在外部,此時應該怎麼處理呢?
function getFetchUrl(query) {
return 'xxx?query=' + query
}
function SearchResults() {
useEffect(() => {
const url = getFetchUrl('react')
}, [])
useEffect(() => {
const url = getFetchUrl('redux')
}, [])
}
複製代碼
useCallback
包裝。function SearchResults() {
const [query, setQuery] = useState('react')
const getFetchUrl = useCallback(() => {
return 'xxx?query=' + query
}, [query])
useEffect(() => {
const url = getFetchUrl()
}, [getFetchUrl])
}
複製代碼
若是 query
保持不變,getFetchUrl
也會保持不變,咱們的 effect 也不會從新運行。可是若是 query
修改了,getFetchUrl
也會隨之改變,所以會從新請求數據。
useCallback本質上是添加了一層依賴檢查。它以另外一種方式解決了問題 - 咱們使函數自己只在須要的時候才改變,而不是去掉對函數的依賴。
關於 useReducer
和 useCallback
的更多內容,我會在後面的筆記中整理出來。
useContext的使用場景。
在一個典型的 React 應用中,數據是經過 props 屬性自上而下(由父及子)進行傳遞的,但這種作法對於某些類型的屬性而言是極其繁瑣的(例如:地區偏好,UI 主題),這些屬性是應用程序中許多組件都須要的。Context 提供了一種在組件之間共享此類值的方式,而沒必要顯式地經過組件樹的逐層傳遞 props。(引用自官網)
直接看一個例子吧~
Container
。import React, { useState, createContext } from 'react'
import Child1 from './Child1'
import Child2 from './Child2'
// 建立一個 Context 對象
export const ContainerContext = createContext({})
function Container() {
const [state, setState] = useState({child1Color: 'pink', child2Color: 'skyblue'})
const changeChild1Color = () => {
setState({
...state,
child1Color: 'lightgreen'
})
}
return (
<>
<ContainerContext.Provider value={state}>
<Child1></Child1>
<Child2></Child2>
</ContainerContext.Provider>
<button onClick={changeChild1Color}>修改child1顏色</button>
</>
)
}
export default Container
複製代碼
Child1
。import React, {useContext} from 'react'
import { ContainerContext } from './Container'
function Child1() {
const value = useContext(ContainerContext)
return <h1 style={{color: value.child1Color}}>我是Child1組件</h1>
}
export default Child1
複製代碼
Child2
。import React, {useContext} from 'react'
import { ContainerContext } from './Container'
function Child2() {
const value = useContext(ContainerContext)
return <h1 style={{color: value.child2Color}}>我是Child2組件</h1>
}
export default Child2
複製代碼
咱們能夠經過這個簡單的 demo 來了解一下 useContext
相關的基礎知識。
基本使用方法分析:
經過 React 提供的 createContext
方法建立一個 Context
對象。
Container
組件中,經過export const ContainerContext = createContext({})
建立了一個 Context
對象,並設置了默認值爲 {}
。
注意:默認值只有在組件所處的樹中沒有匹配到 Provider 時,默認值纔會生效。
稍微修改一下 Container
中返回的組件,這個時候 Child1
和 Child2
讀取的 context 就是建立 Context
對象時的默認值了。
<>
<Child1></Child1>
<Child2></Child2>
<button onClick={changeChild1Color}>修改child1顏色</button>
</>
複製代碼
每一個 Context
對象都會返回一個 Provider React 組件,它容許消費組件訂閱 context 的變化。
Container
組件中的 Context
對象返回 ContainerContext.Provider
組件,它接收一個value 屬性,傳遞給消費組件。同時包裹在其內部的消費組件(Child1
和Child2
)能夠訂閱 context 的變化。
一個 Provider React 組件能夠和多個消費組件有對應關係。多個 Provider React 組件 也能夠嵌套使用,裏層的會覆蓋外層的數據。
在消費組件中,使用 useContext
訂閱 context。
注意:useContext
的參數必須是 context 對象自己。
基本使用方式就是這樣了,因爲useContext
我用的還比較少,這裏就先不作過多的介紹了。
值得注意的是:
學習的過程當中,我常常會有種錯覺:我會了。其實這種「會」,也只是對某個知識點的「眼熟」。真正須要動手去完成的時候,就會發現一頭霧水。就像剛開始接觸前端的時候,看到別人的代碼,總會恍然大悟,但本身卻寫不出來同樣。再加上不少知識點學過的那兩天能夠記得,可是一段時間不用就會遺忘,又要從新學習。
因此我想要改變這種困境,經過整理本身的學習過程,加深印象的同時也方便之後查閱。
但願這篇文章對你一樣也有所幫助,若是有建議歡迎留言~
囉嗦了這麼多,小夥伴們給我留個贊吧,謝謝~
參考文章: