react hooks
是 React 16.8
的新增特性。 它可讓咱們在函數組件中使用 state
、生命週期以及其餘 react
特性,而不只限於 class
組件javascript
react hooks
的出現,標示着 react
中不會在存在無狀態組件了,只有類組件和函數組件html
存在即合理,hooks
也不例外,它的出現,就表明了它要解決一些 class
組件的缺陷或者不足,那麼咱們先來看看 class
組件有什麼不足或者問題存在前端
根據網上的說法我總結出三點,固然每種問題都有其解決方案java
問題 | 解決方案 | 缺點 |
---|---|---|
生命週期臃腫、邏輯耦合 | 無 | |
邏輯難以複用 | 經過繼承解決 | 不支持多繼承 |
經過hoc解決 | 會增長額外的組件嵌套,也會有一些性能影響 | |
渲染屬性 | 同上、層級臃腫、性能影響 | |
class this 指向問題 |
匿名函數 | 每次都建立新的函數,子組件重複沒必要要渲染 |
bind |
須要寫不少跟邏輯、狀態無關的代碼 |
而 hooks
對這些問題都有較好的解決方案react
class,
天然就沒有了 this
指向問題useEffect
來解決複用問題useEffect
來細分邏輯,減少出現邏輯臃腫的場景固然,hooks
是一把雙刃劍,用的好本身可以達到效果,用的很差反而會 下降開發效率和質量,那麼咱們接下來看看如用更好的使用 hooks
吧git
hooks
的能力,就是讓咱們在函數組件中使用 state
, 就是經過 useState
來實現的,咱們來看一個簡單的例子es6
function App () {
const [ count, setCount ] = useState(0)
return (
<div> 點擊次數: { count } <button onClick={() => { setCount(count + 1)}}>點我</button> </div>
)
}
複製代碼
useState
的使用很是簡單,咱們從 React
中拿到 useState
後,只須要在使用的地方直接調用 useState
函數就能夠, useState
會返回一個數組,第一個值是咱們的 state,
第二個值是一個函數,用來修改該 state
的,那麼這裏爲何叫 count
和 setCount
?必定要叫這個嗎,這裏使用了 es6
的解構賦值,因此你能夠給它起任何名字,updateCount
, doCount
、any thing
,固然,爲了編碼規範,因此建議統一使用一種命名規範,尤爲是第二個值github
當咱們在使用 useState
時,修改值時傳入一樣的值,咱們的組件會從新渲染嗎,例如這樣web
function App () {
const [ count, setCount ] = useState(0)
console.log('component render count')
return (
<div> 點擊次數: { count } <button onClick={() => { setCount(count)}}>點我</button> </div>
)
}
複製代碼
結果是不會,放心使用redux
useState
支持咱們在調用的時候直接傳入一個值,來指定 state
的默認值,好比這樣 useState(0)
, useState({ a: 1 })
, useState([ 1, 2 ])
,還支持咱們傳入一個函數,來經過邏輯計算出默認值,好比這樣
function App (props) {
const [ count, setCount ] = useState(() => {
return props.count || 0
})
return (
<div> 點擊次數: { count } <button onClick={() => { setCount(count + 1)}}>點我</button> </div>
)
}
複製代碼
這個時候,就有小夥伴問了,那我組件每渲染一次,useState
中的函數就會執行一邊嗎,浪費性能,其實不會,useState
中的函數只會執行一次,咱們能夠作個測試
function App (props) {
const [ count, setCount ] = useState(() => {
console.log('useState default value function is call')
return props.count || 0
})
return (
<div> 點擊次數: { count } <button onClick={() => { setCount(count + 1)}}>點我</button> </div>
)
}
複製代碼
結果是
咱們在使用 useState
的第二個參數時,咱們想要獲取上一輪該 state
的值的話,只須要在 useState
返回的第二個參數,也就是咱們上面的例子中的 setCount
使用時,傳入一個參數,該函數的參數就是上一輪的 state
的值
setCount((count => count + 1)
複製代碼
useState
咱們不可能只使用一個,當咱們使用多個 useState
的時候,那 react
是如何識別那個是哪一個呢,其實很簡單,它是靠第一次執行的順序來記錄的,就至關於每一個組件存放useState
的地方是一個數組,每使用一個新的 useState
,就向數組中 push
一個 useState
,那麼固然,當咱們在運行時改變、添加、減小 useState
時,react
還能正常執行嗎
function App (props) {
let count, setCount
let sum, setSum
if (count > 2) {
[ count, setCount ] = useState(0)
[ sum, setSum ] = useState(10)
} else {
[ sum, setSum ] = useState(10)
[ count, setCount ] = useState(0)
}
return (
<div> 點擊次數: { count } 總計:{ sum } <button onClick={() => { setCount(count + 1); setSum(sum - 1)}}>點我</button> </div>
)
}
複製代碼
當咱們在運行時改變 useState
的順序,數據會混亂,增長 useState
, 程序會報錯
不要在循環,條件或嵌套函數中調用
Hook
, 確保老是在你的React
函數的最頂層調用他們。遵照這條規則,你就能確保Hook
在每一次渲染中都按照一樣的順序被調用。這讓React
可以在屢次的useState
和useEffect
調用之間保持hook
狀態的正確
同時推薦使用 eslint-plugin-react-hooks
插件來規範代碼編寫,針對這種狀況進行校驗
useState
的使用就是這麼簡單,我已經學會了, 接下來,咱們看一下 useEffect
的使用
Effect Hook
可讓你在函數組件中執行反作用操做,這裏提到反作用,什麼是反作用呢,就是除了狀態相關的邏輯,好比網絡請求,監聽事件,查找 dom
有這樣一個需求,須要咱們在組件在狀態更新的時候改變 document.title
,在之前咱們會這樣寫代碼
class App extends PureComponent {
state = {
count: 0
}
componentDidMount() {
document.title = count
}
componentDidUpdate() {
document.title = count
}
render () {
const { count } = this.state
return (
<div> 頁面名稱: { count } <button onClick={() => { this.setState({ count: count++ })}}>點我</button> </div>
)
}
}
複製代碼
使用 hooks
怎麼寫呢
function App () {
const [ count, setCount ] = useState(0)
useEffect(() => {
document.title = count
})
return (
<div> 頁面名稱: { count } <button onClick={() => { setCount(count + 1 )}}>點我</button> </div>
)
}
複製代碼
useEffect
是什麼呢,咱們先忽略,回到咱們總結的 class
組件存在的問題,useState
只是讓咱們的函數組件具備使用 state
的能力,那咱們要解決 class
組件存在的問題,先來解決第一個,生命週期臃腫的問題
若是你熟悉
React class
的生命週期函數,你能夠把useEffect Hook
看作componentDidMount
,componentDidUpdate
和componentWillUnmount
這三個函數的組合。
以往咱們在綁定事件、解綁事件、設定定時器、查找 dom
的時候,都是經過 componentDidMount
、componentDidUpdate
、componentWillUnmount
生命週期來實現的,而 useEffect
會在組件每次 render
以後調用,就至關於這三個生命週期函數,只不過能夠經過傳參來決定是否調用
其中注意的是,useEffect
會返回一個回調函數,做用於清除上一次反作用遺留下來的狀態,若是該 useEffect
只調用一次,該回調函數至關於 componentWillUnmount
生命週期
具體看下面例子
function App () {
const [ count, setCount ] = useState(0)
const [ width, setWidth ] = useState(document.body.clientWidth)
const onChange = () => {
setWidth(document.body.clientWidth)
}
useEffect(() => {
window.addEventListener('resize', onChange, false)
return () => {
window.removeEventListener('resize', onChange, false)
}
})
useEffect(() => {
document.title = count
})
return (
<div> 頁面名稱: { count } 頁面寬度: { width } <button onClick={() => { setCount(count + 1)}}>點我</button> </div>
)
}
複製代碼
接着咱們前面的簡單例子,咱們上面例子要處理兩種反作用邏輯,這裏咱們既要處理 title
,還要監聽屏幕寬度改變,按照 class
的寫法,咱們要在生命週期中處理這兩種邏輯,但在 hooks
中,咱們只須要兩個 useEffect
就能解決這些問題,咱們以前提到,useEffect
可以返回一個函數,用來清除上一次反作用留下的狀態,這個地方咱們能夠用來解綁事件監聽,這個地方存在一個問題,就是 useEffect
是每次 render
以後就會調用,好比 title
的改變,至關於 componentDidUpdate
,但咱們的事件監聽不該該每次 render
以後,進行一次綁定和解綁,就是咱們須要 useEffect
變成 componentDidMount
, 它的返回函數變成 componentWillUnmount
,這裏就須要用到 useEffect
函數的第二個參數了
useEffect
的第二個參數,有三種狀況
render
以後 useEffect
都會調用,至關於 componentDidMount
和 componentDidUpdate
componentDidMount
和 componentWillUnmount
useEffect
纔會執行具體看下面例子
function App () {
const [ count, setCount ] = useState(0)
const [ width, setWidth ] = useState(document.body.clientWidth)
const onChange = () => {
setWidth(document.body.clientWidth)
}
useEffect(() => {
// 至關於 componentDidMount
console.log('add resize event')
window.addEventListener('resize', onChange, false)
return () => {
// 至關於 componentWillUnmount
window.removeEventListener('resize', onChange, false)
}
}, [])
useEffect(() => {
// 至關於 componentDidUpdate
document.title = count
})
useEffect(() => {
console.log(`count change: count is ${count}`)
}, [ count ])
return (
<div> 頁面名稱: { count } 頁面寬度: { width } <button onClick={() => { setCount(count + 1)}}>點我</button> </div>
)
}
複製代碼
根據上面的例子的運行結果,第一個 useEffect
中的 'add resize event'
只會在第一次運行時輸出一次,不管組件怎麼 render
,都不會在輸出,第二個 useEffect
會在每次組件 render
以後都執行,title
每次點擊都會改變, 第三個 useEffect
, 只要有在第一次運行和 count
改變時,纔會執行,屏幕發生改變引發的 render
並不會影響第三個 useEffect
關於 react
中如何使用 context
,這裏就不細說,能夠看我以前寫的 React 中 Context 的使用
context
中的 Provider
和 Consumer
,在類組件和函數組件中都能使用,contextType
只能在類組件中使用,由於它是類的靜態屬性,具體如何使用 useContext
呢,看下面的例子
// 建立一個 context
const Context = createContext(0)
// 組件一, Consumer 寫法
class Item1 extends PureComponent {
render () {
return (
<Context.Consumer>
{
(count) => (<div>{count}</div>)
}
</Context.Consumer>
)
}
}
// 組件二, contextType 寫法
class Item2 extends PureComponent {
static contextType = Context
render () {
const count = this.context
return (
<div>{count}</div>
)
}
}
// 組件一, useContext 寫法
function Item3 () {
const count = useContext(Context);
return (
<div>{ count }</div>
)
}
function App () {
const [ count, setCount ] = useState(0)
return (
<div>
點擊次數: { count }
<button onClick={() => { setCount(count + 1)}}>點我</button>
<Context.Provider value={count}>
<Item1></Item1>
<Item2></Item2>
<Item3></Item3>
</Context.Provider>
</div>
)
}
複製代碼
經過運行上面的例子,咱們獲得的結果是,三種寫法都可以實現咱們的需求,可是,三種寫有各自的優缺點,下面爲對比出的結果
寫法 | 優缺點 |
---|---|
consumer |
嵌套複雜,Consumer 第一個子節點必須爲一個函數,無形增長了工做量 |
contextType |
只支持 類組件,沒法在多 context 的狀況下使用 |
useContext |
不須要嵌套,多 context 寫法簡單 |
經過上面的比較,沒理由繼續使用 consumer
和 contextType
useMemo
是什麼呢,它跟 memo
有關係嗎, memo
的具體內容能夠查看 React 中性能優化、 memo、PureComponent、shouldComponentUpdate 的使用,說白了 memo
就是函數組件的 PureComponent
,用來作性能優化的手段,useMemo
也是,useMemo
在個人印象中和 Vue
的 computed
計算屬性相似,都是根據依賴的值計算出結果,當依賴的值未發生改變的時候,不觸發狀態改變,useMemo
具體如何使用呢,看下面例子
function App () {
const [ count, setCount ] = useState(0)
const add = useMemo(() => {
return count + 1
}, [count])
return (
<div> 點擊次數: { count } <br/> 次數加一: { add } <button onClick={() => { setCount(count + 1)}}>點我</button> </div>
)
}
複製代碼
上面的例子中,useMemo
也支持傳入第二個參數,用法和 useEffect
相似
useMemo
返回的值)須要注意的是,useMemo
會在渲染的時候執行,而不是渲染以後執行,這一點和 useEffect
有區別,因此 useMemo
不建議有 反作用相關的邏輯
同時,useMemo
能夠做爲性能優化的手段,但不要把它當成語義上的保證,未來,React
可能會選擇「遺忘」之前的一些 memoized
值,並在下次渲染時從新計算它們
useCallback
是什麼呢,能夠說是 useMemo
的語法糖,能用 useCallback
實現的,均可以使用 useMemo
, 在 react 中咱們常常面臨一個子組件渲染優化的問題,細節能夠查看React 中性能優化、 memo、PureComponent、shouldComponentUpdate 的使用,尤爲是在向子組件傳遞函數props時,每次 render
都會建立新函數,致使子組件沒必要要的渲染,浪費性能,這個時候,就是 useCallback
的用武之地了,useCallback
能夠保證,不管 render
多少次,咱們的函數都是同一個函數,減少不斷建立的開銷,具體如何使用看下面例子
const onClick = `useMemo`(() => {
return () => {
console.log('button click')
}
}, [])
const onClick = useCallback(() => {
console.log('button click')
}, [])
複製代碼
一樣,useCallback
的第二個參數和useMemo
同樣,沒有區別
useRef
有什麼做用呢,其實很簡單,總共有兩種用法
render
重複申明, 相似於類組件的 this.xxx
上面提到了一點,useRef
只能獲取子組件的實例,這在類組件中也是一樣的道理,具體看下面的例子
// 使用 ref 子組件必須是類組件
class Children extends PureComponent {
render () {
const { count } = this.props
return (
<div>{ count }</div>
)
}
}
function App () {
const [ count, setCount ] = useState(0)
const childrenRef = useRef(null)
// const
const onClick = useMemo(() => {
return () => {
console.log('button click')
console.log(childrenRef.current)
setCount((count) => count + 1)
}
}, [])
return (
<div> 點擊次數: { count } <Children ref={childrenRef} count={count}></Children> <button onClick={onClick}>點我</button> </div>
)
}
複製代碼
useRef
在使用的時候,能夠傳入默認值來指定默認值,須要使用的時候,訪問 ref.current
便可訪問到組件實例
有些狀況下,咱們須要保證函數組件每次 render
以後,某些變量不會被重複申明,好比說 Dom
節點,定時器的 id
等等,在類組件中,咱們徹底能夠經過給類添加一個自定義屬性來保留,好比說 this.xxx
, 可是函數組件沒有 this
,天然沒法經過這種方法使用,有的朋友說,我可使用 useState
來保留變量的值,可是 useState
會觸發組件 render
,在這裏徹底是不須要的,咱們就須要使用 useRef
來實現了,具體看下面例子
function App () {
const [ count, setCount ] = useState(0)
const timer = useRef(null)
let timer2
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + 1)
}, 500)
timer.current = id
timer2 = id
return () => {
clearInterval(timer.current)
}
}, [])
const onClickRef = useCallback(() => {
clearInterval(timer.current)
}, [])
const onClick = useCallback(() => {
clearInterval(timer2)
}, [])
return (
<div> 點擊次數: { count } <button onClick={onClick}>普通</button> <button onClick={onClickRef}>useRef</button> </div>
)
}
複製代碼
當咱們們使用普通的按鈕去暫停定時器時發現定時器沒法清除,由於 App
組件每次 render
,都會從新申明一次 timer2
, 定時器的 id
在第二次 render
時,就丟失了,因此沒法清除定時器,針對這種狀況,就須要使用到 useRef
,來爲咱們保留定時器 id
,相似於 this.xxx
,這就是 useRef
的另一種用法
useReducer
是什麼呢,它其實就是相似 redux
中的功能,相較於 useState
,它更適合一些邏輯較複雜且包含多個子值,或者下一個 state
依賴於以前的 state
等等的特定場景, useReducer
總共有三個參數
reducer
,就是一個函數相似 (state, action) => newState
的函數,傳入 上一個 state
和本次的 action
state
,也就是默認值,是比較簡單的方法state
的邏輯提取到 reducer
外部,這也爲未來對重置 state
的 action
作處理提供了便利具體使用方法看下面的例子
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function App() {
const [state, dispatch] = useReducer(reducer, {
count: 0
});
return (
<> 點擊次數: {state.count} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } 複製代碼
useImperativeHandle
可讓你在使用 ref
時自定義暴露給父組件的實例值,說簡單點就是,子組件能夠選擇性的暴露給副組件一些方法,這樣能夠隱藏一些私有方法和屬性,官方建議,useImperativeHandle
應當與 forwardRef
一塊兒使用,具體如何使用看下面例子
function Kun (props, ref) {
const kun = useRef()
const introduce = useCallback (() => {
console.log('i can sing, jump, rap, play basketball')
}, [])
useImperativeHandle(ref, () => ({
introduce: () => {
introduce()
}
}));
return (
<div ref={kun}> { props.count }</div>
)
}
const KunKun = forwardRef(Kun)
function App () {
const [ count, setCount ] = useState(0)
const kunRef = useRef(null)
const onClick = useCallback (() => {
setCount(count => count + 1)
kunRef.current.introduce()
}, [])
return (
<div> 點擊次數: { count } <KunKun ref={kunRef} count={count}></KunKun> <button onClick={onClick}>點我</button> </div>
)
}
複製代碼
還有兩個 hook
,沒什麼好講的,用的也很少,能夠看看官方文檔
咱們以前總結出三個問題,class this
指向問題,生命週期邏輯冗餘問題,都已獲得解決,而邏輯難以複用,在前面的例子中並無解決,要解決這個問題,就要經過 自定義 hook
來解決
自定義
Hook
,能夠將組件邏輯提取到可重用的函數中,來解決邏輯難以複用問題
前面有個例子是獲取屏幕寬度變化的例子,假設咱們有諸多組件都須要這個邏輯,那麼咱們只須要將其抽取成一個自定義 hook
便可,具體實現看下面例子
function useWidth (defaultWidth) {
const [width, setWidth] = useState(document.body.clientWidth)
const onChange = useCallback (() => {
setWidth(document.body.clientWidth)
}, [])
useEffect(() => {
window.addEventListener('resize', onChange, false)
return () => {
window.removeEventListener('resize', onChange, false)
}
}, [onChange])
return width
}
function App () {
const width = useWidth(document.body.clientWidth)
return (
<div> 頁面寬度: { width } </div>
)
}
複製代碼
經過上面的例子,咱們能夠看出
自定義 hook
是一個函數,其名稱以 use
開頭,函數內部能夠調用其餘的 hook
,至於爲何要以 use
開頭,是由於若是不以 use
開頭,React
就沒法判斷某個函數是否包含對其內部 hook
的調用,React
也將沒法自動檢查你的 hook
是否違反了 hook
的規則,因此要以 use
開頭
自定義 hook
,真的很簡單,不過具體什麼樣的邏輯,須要抽離成自定義 hook
,這就須要工做中不段積累的經驗去判斷,避免爲了 hook
而 hook
在我學習和使用自定義 hook
時,我發現其實它的道理很簡單,不少前端框架、庫裏面都有相似的概念,框架和庫的設計最後都疏通同歸,因此咱們在學習一個新的框架、庫或者理念時,不該該將其是爲一個全新的東西,而更多的應該從自身掌握的內容去推導,去舉一反三,這樣咱們在學習的時候會事半功倍,在日益更新的前端領域,可以抽出更多的時間去理解更爲核心的內容
最後,若是本文對你有任何幫助的話,感謝點個贊 💗