最近一直在學React
相關的東西,React
基於組件的編碼方式,讓寫界面省了很多事兒。難怪如今Flutter
,Compose
都開始擁抱這種開發方式。順便也重拾起了荒廢已久的js,js通過這幾年的更新已經變得像一門新語言了,還支持了class
這個語法,讓咱們熟悉面向對象開發的人更容易上手。可是惱人多變的this
一直都在,一開始用類寫組件的時候常常會莫名其妙地遇到對象找不到的問題,最後發現要bind(this)
。javascript
並且還有個問題是好多複雜的場景爲了傳遞數據只能用高階組件或者渲染屬性來實現,像我這種剛接觸前端的人確定一臉懵逼。好比業務複雜以後咱們有好多個Context
相關的高階組件,一層套一層,重重嵌套讓我想起了在寫Flutter
時的恐懼。html
像這樣:前端
<AuthenticationContext.Consumer>
{user => (
<LanguageContext.Consumer>
{language => (
<StatusContext.Consumer>
{status => (
...
)}
</StatusContext.Consumer>
)}
</LanguageContext.Consumer>
)}
</AuthenticationContext.Consumer>
複製代碼
因此在React
提供的幾種編寫組件的方式中,我最喜歡函數組件,代碼更加簡潔,沒有什麼花裏胡哨的新概念,並且可讓我避免跟this
打交道。固然了,所以它的能力也十分有限,函數組件沒有狀態,大部分業務邏輯須要跟生命週期打交道,我仍是須要經過類來寫組件,管理生命週期跟狀態,哪怕它只是個很小的組件。java
###起色 而後某天我發現了Hook
,打開了新大門!React
內置了幾個Hook
,*100%*向後兼容, 對全部的React
咱們熟知的概念提供了直接支持: props
, state
, context
, refs
, 以及生命週期
。並且, Hook
提供了更好的方式去組合這些概念,封裝你的邏輯,避免了嵌套地獄或者相似的問題。咱們能夠在函數組件中使用狀態,也能夠在渲染後執行一些網絡請求。react
Hook
其實就是普通的函數,是對類組件中一些能力在函數組件的補充,因此咱們能夠在函數組件中直接使用它,在類組件中,咱們是不須要它的。git
React
提供的Hook
不算多,咱們最經常使用的Hook
要數useState
,useEffect
跟useContext
了,其餘的都是適用更加通用的或者更加邊界的場景的Hook
。 useState
可讓咱們在函數組件中管理狀態。github
import { useState } from 'react'
const [ state, setState ] = useState(initialState)
複製代碼
以後咱們就能夠經過state
直接訪問狀態,經過setState
來設置狀態,組件會自動從新渲染。數組
useEffect
相似於向componentDidMount
跟componentDidUpdate
添加代碼,咱們常在這兩個方法中設置網絡請求或者Timer
,如今統一寫到一個地方就行了,同時咱們也能夠返回一個清理函數,它將會在在相似componentWillUnmount
的時機被調用,執行一些清理操做。使用useEffect
就能夠替代這三個方法。markdown
import { useEffect } from 'react'
useEffect(didUpdate)
複製代碼
而useContext
接受一個Context
對象,返回一個Context
的值。網絡
import { useContext } from 'react'
const value = useContext(MyContext)
複製代碼
能夠用來取代以前的Context Consumer
。具體的使用方式咱們之後再說,以前的嵌套地獄可使用useContext
來化解:
const user = useContext(AuthenticationContext)
const language = useContext(LanguageContext)
const status = useContext(StatusContext)
複製代碼
看到這兒,你們應該對Hook開始感興趣了。與其寫那麼多Provider
,Consumer
,去熟悉一大堆花裏胡哨的概念,你們都更喜歡這種直接的方式吧。我將展現給你們看,分別用類的方式跟Hook
的方式來實現一個組件,進一步展現Hook
帶來的便利。
採用類去實現組件,咱們要在構造器中去定義狀態,並且須要修改this
去作事件處理,代碼以下:
import React from 'react'
class MyName extends React.Component {
constructor(props) {
super(props)
this.state = { name: '' }
this.handleChange = this.handleChange.bind(this)
}
handleChange(evt) {
this.setState({ name: evt.target.value })
}
render() {
const { name } = this.state
return (
<div> <h1>My name is: {name}</h1> <input type="text" value={name} onChange={this.handleChange} /> </div>
)
}
}
export default MyName
複製代碼
import React, { useState } from 'react'
function MyName() {
const [name, setName] = useState('')
function handleChange(evt) {
setName(evt.target.value)
}
return (
<div> <h1>My name is: {name}</h1> <input type="text" value={name} onChange={handleChange} /> </div>
)
}
export default MyName
複製代碼
代碼量變少了,咱們使用了useState
,減小了不少模版代碼,也不用處理構造器跟修改this
了,想要修改狀態直接調用setName
就行了。整個代碼看起來更加簡潔易於理解,咱們再也不關心要怎麼維護保存狀態,安安心心經過useState
函數使用狀態就好了。並且函數的形式讓編譯器更容易去分析優化代碼,移除無用的代碼塊,使生成的文件更小。
###香不香? 咱們能夠發現,Hook
更偏向於咱們向React
聲明咱們想要什麼,這一點相似於咱們的界面描述方式,咱們只說咱們要什麼,而不是告訴框架該怎麼作,代碼也更加簡潔,方便其餘人理解跟後期維護,經過函數的方式咱們也能夠在組件間共享邏輯。
###深刻 那麼Hook
是怎麼作到這麼神奇的事情的呢,爲了深刻理解這背後的原理,咱們從頭開始實現一個咱們本身的useState
函數來理解這個過程。這個實現不會跟React的實現徹底相同,我會盡可能簡化,將核心原理展現出來。
首先定義一個咱們本身的useState
函數,方法簽名你們都知道了,要傳遞一個參數做爲初始值。
function useState (initialState) {
複製代碼
而後咱們定義一個值來保存咱們的狀態,一開始,它的值會是咱們傳給函數的initialState
。
let value = initialState
複製代碼
而後咱們要定義一個setState
函數,當咱們改變狀態值時,從新渲染組件。
function setState (nextValue) {
value = nextValue
ReactDOM.render(<MyName />, document.getElementById('root'))
}
複製代碼
這邊的ReactDOM
是用來從新渲染用的。 最終咱們要把這個狀態值跟設置方法以數組的形式返回出去:
return [ value, setState ]
}
複製代碼
一個簡單的Hook
就實現了,Hook
其實就是簡單的js函數,用來執行一些有反作用的操做,好比用來設置一個有狀態的值。咱們的Hook
使用了一個閉包來保存狀態值,由於setState
跟value
在同一個閉包下,因此咱們的setState
能夠訪問它,同理不把它傳遞出去的話在這個閉包外咱們是沒辦法直接訪問的。
###來問題了 若是咱們如今運行咱們的代碼,咱們會發現組件從新渲染的時候狀態重置了,而後咱們就不能輸入任何文字。這是由於每次從新渲染都調用了useState
,而後致使value
初始化了那咱們得想辦法把狀態保存在別的地方避免由於從新渲染而受到影響了。
咱們先嚐試在函數外使用一個全局變量來保存咱們的狀態,那這樣的話咱們的狀態就不會由於從新渲染而初始化了。
let value
function useState (initialState) {
複製代碼
在useState上定義了一個全局變量後,咱們的初始化代碼也要改一改:
if (typeof value === 'undefined') value = initialState
複製代碼
這樣就沒問題了。 可是緊接着,咱們又發現,當咱們想多調用幾回useState
來管理多個狀態時,它總在往同一個全局變量上寫值,全部的useState
方法都在操做同一個value
!這確定不是咱們想要的結果。
那爲了支持多個useState
調用,咱們要想辦法改進一下,把變量替換成一個數組試試?
let values = []
let currentHook = 0
複製代碼
而後賦初始值的地方也要修改:
if (typeof values[currentHook] === 'undefined')
values[currentHook] = initialState
複製代碼
最重要的是咱們的setState
方法要修改好,這樣咱們只會更新該更新的狀態值。咱們須要把當前Hook
對應的currentHook
保存起來,由於currentHook
是一直會變的。
let hookIndex = currentHook
function setState (nextValue) {
values[hookIndex] = nextValue
ReactDOM.render(<MyName />, document.getElementById('root'))
}
複製代碼
最終返回:
return [ values[currentHook++], setState ]
複製代碼
而後咱們還要在開始渲染的時候初始化一下currentHook
:
function Name () {
currentHook = 0
複製代碼
如今咱們的Hook
能夠說是正常工做了
使用一個全局數組保存Hook
的value
能夠知足屢次調用useState
的需求,React
內部實現也是相似,不過它的實現更加複雜跟優化,它本身處理好了計數器跟全局變量,並且也不須要咱們手動去重置計數器,不過大致原理咱算是把它摸清楚了。
###那複雜場景來了 其實也不是什麼複雜的場景啦,想象這樣一個狀況,咱們須要把輸入的姓名展現出來,姓跟名分開用狀態保存,同時咱們想把姓作成選填那該怎麼辦? 咱們能夠先用一個狀態記錄姓是否是必需的:
const [ enableFirstName, setEnableFirstName ] = useState(false)
複製代碼
而後咱們定義一個處理函數:
function handleEnableChange (evt) {
setEnableFirstName(!enableFirstName)
}
複製代碼
若是checkbox
沒有勾選上咱們就不打算渲染姓了,
<h1>My name is: {enableFirstName ? name : ''} {lastName}</h1>
複製代碼
咱們能不能把Hook
定義放進一個if條件或者三目運算符中去呢?像這樣:
const [ name, setName ] = enableFirstName
? useState('') : [ '', () => {} ]
複製代碼
如今yarn start
來運行咱們的代碼,咱們能夠發現複選框沒有勾選時,名仍是能夠修改的,姓隨你怎麼改都沒用,這是咱們想要的結果。
當咱們再次選中複選框時,咱們能修改姓了。可是奇怪的事發生了,名的值跑到姓那兒去了。
這是由於Hook
的順序很重要,咱們都記得咱們實現useState
的時候,經過currentHook
來肯定當前調用的狀態所在位置的,如今咱們憑空插入了一個Hook
調用,致使順序被打亂了,Hook
在從新渲染時會從新肯定索引,可是咱們的全局數組並不會變,致使姓去取了名的狀態。
勾選複選框以前的狀態:
勾選以後:
因此調用Hook的順序很重要! 這個限制在React
官方提供的Hook
中也存在,並且React
也決定堅持如今的設計。咱們要避免這種寫法,真有這種狀況選擇的狀況,無論用不用,都直接把可能要用的Hook
聲明好,或者拆分出獨立的組件,在組件裏使用Hook
,把問題轉換成要不要渲染某個組件,這也是React
團隊推薦的作法。
雖然有時候咱們會以爲能在條件語句或者循環中這樣使用Hook
更好,可是React
團隊爲何這麼設計呢?有木有更好的方案呢?
有人提出了 NamedHook
:
// 注意: 不是真實的React Hook API
const [ name, setName ] = useState('nameHook', '')
複製代碼
這樣作能夠避免上面那種數據混亂的狀況,每一個Hook
調用咱們都設了一個獨特的名字,可是這樣作咱們就得花時間想出獨一無二的名字,解決命名衝突,並且當一個條件變成false
的時候咱們該怎麼作?若是一個元素從循環中刪除了咱們該怎麼作?咱們該清理狀態嗎?若是不清理狀態,內存泄漏怎麼辦?
咱們能夠看到,這樣並無讓事情變得簡單,也引入了不少複雜的問題,因此React
團隊最後堅持瞭如今的設計,讓API儘量保持簡單簡單,而咱們,在使用時要注意順序。
看到這兒的同窗可能已經躍躍欲試了,可能有同窗會問道,既然Hook
能大大地簡化代碼結構,讓代碼更加可維護,咱們是否是該把全部的組件都用Hook
來重寫呢? 固然不—Hook
是可選的。你能夠在你的部分組件裏面嘗試Hook
,React
團隊如今尚未打算移除類組件。如今不急着把全部東西都重構成基於Hook
。並且Hook
並非銀彈,咱們能夠在以爲用Hook
最恰當的地方用Hook
來實現,好比, 你有許多組件處理類似的邏輯, 你能夠把邏輯抽象成一個Hook
,或者一個小組件用Hook
實現會比較簡單,有些地方狀態管理比較複雜那仍是用類組件會比較好。因此大部分狀況下咱們仍是會函數組件跟類組件一塊兒混用。
###結語
最後,相信你們對於Hook的做用跟實現原理想必有了個大致的瞭解,Hook就是一些簡單的js函數,你們看一眼文檔就知道怎麼用啦,如今咱們瞭解了Hook的優勢跟限制,能夠在平常開發中更好地作出選擇,本文的代碼看這裏:示例代碼。