本文是?React?新特性系列的第二篇,第一篇請點擊這里:
React 新特性講解及實例
什么是 Hooks
Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 類組件 的情況下使用 state
以及其他的 React
特性。
類組件的不足
狀態邏輯復用難
缺少復用機制
渲染屬性和高階組件導致層級冗余
趨向復雜難以維護
生命周期函數混雜不相干邏輯
相干邏輯分散在不同生命周期
this 指向困擾
內聯函數過度創建新句柄
類成員函數不能保證?
this
Hooks 優勢
優化類組件的三大問題
函數組件無 this 問題
自定義 Hook 方便復用狀態邏輯
副作用的關注點分離
使用 State Hook
import React, {Component} from 'react'
class App extends Component {
state = {
count: 0
};
render() {
const {count} = this.state;
return (
<button type="button"
onClick={() => {
this.setState({
count: count + 1
})
}}
>Click({count})button>
)
}
}
export default App;
以上代碼很好理解,點擊按鈕讓 count
值加 1
。
接下來我們使用 useState
來實現上述功能。
import React, {useState} from 'react'
function App () {
const [count, setCount] = useState(0)
return (
<button type="button"
onClick={() => {setCount(count + 1) }}
>Click({count})button>
)
}
在這里, useState
就是一個 Hook。通過在函數組件里調用它來給組件添加一些內部 state
,React 會在重復渲染時保留這個 state
。
useState
會返回一對值:當前狀態和一個讓你更新它的函數。你可以在事件處理函數中或其他一些地方調用這個函數。它類似 class
組件的 this.setState
,但是它不會把新的 state
和舊的 state
進行合并。 useState
唯一的參數就是初始 state
。
useState
讓代碼看起來簡潔了,但是我們可能會對組件中,直接調用 useState
返回的狀態會有些懵。既然 userState
沒有傳入任何的環境參數,它怎么知道要返回的的是 count
的呢,而且還是這個組件的 count
不是其它組件的 count
。
初淺的理解: useState
確實不知道我們要返回的 count
,但其實也不需要知道,它只要返回一個變量就行了。數組解構的語法讓我們在調用 useState
時可以給 state
變量取不同的名字。
useState
怎么知道要返回當前組件的 state
?
因為 JavaScript 是單線程的。在 useState
被調用時,它只能在唯一一個組件的上下文中。
有人可能會問,如果組件內有多個 usreState
,那 useState
怎么知道哪一次調用返回哪一個 state
呢?
這個就是按照第一次運行的次序來順序來返回的。
接著上面的例子我們在聲明一個 useState
:
...
const [count, setScount] = useState(0)
const [name, setName] = useState('小智')
...
然后我們就可以斷定,以后 APP
組件每次渲染的時候, useState
第一次調用一定是返回 count
,第二次調用一定是返回 name
。不信的話來做個實驗:
let id = 0
function App () {
let name,setName;
let count,setCount;
id += 1;
if (id & 1) {
// 奇數
[count, setCount] = useState(0)
[name, setName] = useState('小智')
} else {
// 偶數
[name, setName] = useState('小智')
[count, setCount] = useState(0)
}
return (
<button type="button"
onClick={() => {setCount(count + 1) }}
>
Click({count}), name ({name})
button>
)
}
首先在外部聲明一個 id,當 id為奇數和偶數的時候分別讓 useState 調用方式相反,運行會看到有趣的現象。
當前版本如果寫的順序不一致就會報錯。
會發現 count
和 name
的取值串了。我們希望給 count 加 1
,現在卻給 name 加了 1
,說明 setCount
函數也串成了 setName
函數。
為了防止我們使用 useState 不當,React 提供了一個 ESlint 插件幫助我們檢查。
eslint-plugin-react-hooks
優化點
通過上述我們知道 useState
有個默認值,因為是默認值,所以在不同的渲染周期去傳入不同的值是沒有意義的,只有第一次傳入的才有效。如下所示:
...
const defaultCount = props.defaultCount || 0
const [count, setCount] = useState(defaultCount)
...
state
的默認值是基于 props
,在 APP 組件每次渲染的時候 constdefaultCount=props.defaultCount||0
都會運行一次,如果它復雜度比較高的話,那么浪費的資料肯定是可觀的。
useState
支持傳入函數,來延遲初始化:
const [count, setCount] = useState(() => {
return props.defaultCount || 0
})
使用 Effect Hook
Effect Hook 可以讓你在函數組件中執行副作用操作。數據獲取,設置訂閱以及手動更改 React 組件中的 DOM 都屬于副作用。不管你知不知道這些操作,或是"副作用"這個名字,應該都在組件中使用過它們。
副作用的時機
Mount 之后 對應
componentDidMount
Update 之后 對應
componentDidUpdate
Unmount 之前 對應
componentWillUnmount
現在使用 useEffect
就可以覆蓋上述的情況。
為什么一個 useEffect
就能涵蓋 Mount,Update,Unmount
等場景呢。
useEffect 標準上是在組件每次渲染之后調用,并且會根據自定義狀態來決定是否調用還是不調用。
第一次調用就相當于 componentDidMount
,后面的調用相當于 componentDidUpdate
。 useEffect
還可以返回另一個回調函數,這個函數的執行時機很重要。作用是清除上一次副作用遺留下來的狀態。
比如一個組件在第三次,第五次,第七次渲染后執行了 useEffect
邏輯,那么回調函數就會在第四次,第六次和第八次渲染之前執行。嚴格來講,是在前一次的渲染視圖清除之前。如果 useEffect
是在第一次調用的,那么它返回的回調函數就只會在組件卸載之前調用了,也就是 componentWillUnmount
。
如果你熟悉 React class 的生命周期函數,你可以把 useEffect Hook 看做
componentDidMount
,componentDidUpdate
和componentWillUnmount
這三個函數的組合。
舉粟說明一下:
class App extends Component {
state = {
count: 0,
size: {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
};
onResize = () => {
this.setState({
size: {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
})
}
componentDidMount () {
document.title = this.state.count;
window.addEventListener('resize', this.onResize, false)
}
componentWillMount () {
window.removeEventListener('resize', this.onResize, false)
}
componentDidUpdate () {
document.title = this.state.count;
}
render() {
const {count, size} = this.state;
return (
<button type="button"
onClick={() => {this.setState({count: count + 1})}}
>
Click({count})
size: {size.width}x{size.height}
button>
)
}
}
上面主要做的就是網頁 title
顯示 count
值,并監聽網頁大小的變化。這里用到了 componentDidMount
, componentDidUpdate
等副作用,因為第一次掛載我們需要把初始值給 title, 當 count
變化時,把變化后的值給它 title
,這樣 title
才能實時的更新。
注意,我們需要在兩個生命周期函數中編寫重復的代碼。
這邊我們容易出錯的地方就是在組件結束之后要記住銷毀事件的注冊,不然會導致資源的泄漏。現在我們把 App 組件的副作用用 useEffect
實現。
function App (props) {
const [count, setCount] = useState(0);
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
const onResize = () => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
)
}
useEffect(() => {
document.title = count;
})
useEffect(() => {
window.addEventListener('resize', onResize, false);
return () => {
window.removeEventListener('resize', onResize, false)
}
}, [])
return (
<button type="button"
onClick={() => {setCount(count + 1) }}
>
Click({count})
size: {size.width}x{size.height}
button>
)
}
對于上述代碼的第一個 useEffect
,相比類組件,Hooks 不在關心是 mount 還是 update。用 useEffect
統一在渲染后調用,就完整追蹤了 count
的值。
對于第二個 useEffect
,我們可以通過返回一個回調函數來注銷事件的注冊。回調函數在視圖被銷毀之前觸發,銷毀的原因有兩種:重新渲染和組件卸載。
這邊有個問題,既然 useEffect
每次渲染后都執行,那我們每次都要綁定和解綁事件嗎?當然是完全不需要,只要使用 useEffect
第二個參數,并傳入一個空數組即可。第二個參數是一個可選的數組參數,只有數組的每一項都不變的情況下, useEffect
才不會執行。第一次渲染之后,useEffect 肯定會執行。由于我們傳入的空數組,空數組與空數組是相同的,因此 useEffect
只會在第一次執行一次。
這也說明我們把 resize
相關的邏輯放在一直寫,不在像類組件那樣分散在兩個不同的生命周期內。同時我們處理 title 的邏輯與 resize 的邏輯分別在兩個 useEffect 內處理,實現關注點分離。
我們在定義一個 useEffect,來看看通過不同參數,第二個參數的不同作用。
...
useEffect(() => {
console.log('count:', count)
}, [count])
...
第二個參數我們傳入 [count]
, 表示只有 count 的變化時,我才打印 count
值, resize
變化不會打印。
運行效果如下:
第二個參數的三種形態, undefined
,空數組及非空數組,我們都經歷過了,但是咱們沒有看到過回調函數的執行。
現在有一種場景就是在組件中訪問 Dom 元素,在 Dom元素上綁定事件,在上述的代碼中添加以下代碼:
...
const onClick = () => {
console.log('click');
}
useEffect(() => {
document.querySelector('#size').addEventListener('click', onClick, false);
},[])
return (
...
<span id="size">size: {size.width}x{size.height}span>
div>
)
新增一個 DOM 元素,在新的
useEffect
中監聽 span
元素的點擊事件。
運行效果:
假如我們 span 元素可以被銷毀重建,我們看看會發生什么情況,改造一下代碼:
return (
...
button>
{
count%2
? <span id="size">我是spanspan>
: <p id='size'>我是pp>
}
div>
運行效果:
可以看出一旦 dom 元素被替換,我們綁定的事件就失效了,所以咱們始終要追蹤這個dom 元素的最新狀態。
使用
useEffect
,最合適的方式就是使用回調函數來處理了,同時要保證每次渲染后都要重新運行,所以不能給第二次參數設置 []
,改造如下:
useEffect(() => {
document.querySelector('#size').addEventListener('click', onClick, false);
return () => {
document.querySelector('#size').removeEventListener('click', onClick, false);
}
})
運行結果:
參考
[React 官方文檔][9]
《React勁爆新特性Hooks 重構去哪兒網》
交流
交流
我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的干貨,在進階的路上,共勉!
關注公眾號,后臺回復福利,即可看到福利,你懂的。