最近一直在學(xué)React相關(guān)的東西,React基于組件的編碼方式,讓寫(xiě)界面省了不少事兒。難怪現(xiàn)在Flutter,Compose都開(kāi)始擁抱這種開(kāi)發(fā)方式。順便也重拾起了荒廢已久的js,js經(jīng)過(guò)這幾年的更新已經(jīng)變得像一門(mén)新語(yǔ)言了,還支持了class這個(gè)語(yǔ)法,讓我們熟悉面向?qū)ο箝_(kāi)發(fā)的人更容易上手。但是惱人多變的this一直都在,一開(kāi)始用類(lèi)寫(xiě)組件的時(shí)候經(jīng)常會(huì)莫名其妙地遇到對(duì)象找不到的問(wèn)題,最后發(fā)現(xiàn)要bind(this)。
而且還有個(gè)問(wèn)題是好多復(fù)雜的場(chǎng)景為了傳遞數(shù)據(jù)只能用高階組件或者渲染屬性來(lái)實(shí)現(xiàn),像我這種剛接觸前端的人肯定一臉懵逼。比如業(yè)務(wù)復(fù)雜之后我們有好多個(gè)Context相關(guān)的高階組件,一層套一層,重重嵌套讓我想起了在寫(xiě)Flutter時(shí)的恐懼。
像這樣:
<AuthenticationContext.Consumer>
{user => (
<LanguageContext.Consumer>
{language => (
<StatusContext.Consumer>
{status => (
...
)}
</StatusContext.Consumer>
)}
</LanguageContext.Consumer>
)}
</AuthenticationContext.Consumer>
所以在React提供的幾種編寫(xiě)組件的方式中,我最喜歡函數(shù)組件,代碼更加簡(jiǎn)潔,沒(méi)有什么花里胡哨的新概念,而且可以讓我避免跟this打交道。當(dāng)然了,因此它的能力也十分有限,函數(shù)組件沒(méi)有狀態(tài),大部分業(yè)務(wù)邏輯需要跟生命周期打交道,我還是需要通過(guò)類(lèi)來(lái)寫(xiě)組件,管理生命周期跟狀態(tài),哪怕它只是個(gè)很小的組件。
轉(zhuǎn)機(jī)
然后某天我發(fā)現(xiàn)了Hook,打開(kāi)了新大門(mén)!React內(nèi)置了幾個(gè)Hook,100%向后兼容, 對(duì)所有的React我們熟知的概念提供了直接支持: props, state, context, refs, 以及生命周期。而且, Hook提供了更好的方式去組合這些概念,封裝你的邏輯,避免了嵌套地獄或者類(lèi)似的問(wèn)題。我們可以在函數(shù)組件中使用狀態(tài),也可以在渲染后執(zhí)行一些網(wǎng)絡(luò)請(qǐng)求。
Hook其實(shí)就是普通的函數(shù),是對(duì)類(lèi)組件中一些能力在函數(shù)組件的補(bǔ)充,所以我們可以在函數(shù)組件中直接使用它,在類(lèi)組件中,我們是不需要它的。
React提供的Hook不算多,我們最常用的Hook要數(shù)useState,useEffect跟useContext了,其他的都是適用更加通用的或者更加邊界的場(chǎng)景的Hook。
useState可以讓我們?cè)诤瘮?shù)組件中管理狀態(tài)。
import { useState } from 'react'
const [ state, setState ] = useState(initialState)
之后我們就可以通過(guò)state直接訪問(wèn)狀態(tài),通過(guò)setState來(lái)設(shè)置狀態(tài),組件會(huì)自動(dòng)重新渲染。
useEffect類(lèi)似于向componentDidMount跟componentDidUpdate添加代碼,我們常在這兩個(gè)方法中設(shè)置網(wǎng)絡(luò)請(qǐng)求或者Timer,現(xiàn)在統(tǒng)一寫(xiě)到一個(gè)地方就好了,同時(shí)我們也可以返回一個(gè)清理函數(shù),它將會(huì)在在類(lèi)似componentWillUnmount的時(shí)機(jī)被調(diào)用,執(zhí)行一些清理操作。使用useEffect就可以替代這三個(gè)方法。
import { useEffect } from 'react'
useEffect(didUpdate)
而useContext接受一個(gè)Context對(duì)象,返回一個(gè)Context的值。
import { useContext } from 'react'
const value = useContext(MyContext)
可以用來(lái)取代之前的Context Consumer。具體的使用方式我們以后再說(shuō),之前的嵌套地獄可以使用useContext來(lái)化解:
const user = useContext(AuthenticationContext)
const language = useContext(LanguageContext)
const status = useContext(StatusContext)
看到這兒,大家應(yīng)該對(duì)Hook開(kāi)始感興趣了。與其寫(xiě)那么多Provider,Consumer,去熟悉一大堆花里胡哨的概念,大家都更喜歡這種直接的方式吧。我將展示給大家看,分別用類(lèi)的方式跟Hook的方式來(lái)實(shí)現(xiàn)一個(gè)組件,進(jìn)一步展示Hook帶來(lái)的便利。
- 類(lèi)的方式
采用類(lèi)去實(shí)現(xiàn)組件,我們要在構(gòu)造器中去定義狀態(tài),而且需要修改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
- 我們現(xiàn)在來(lái)看看函數(shù)組件的方式:
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,減少了很多模版代碼,也不用處理構(gòu)造器跟修改this了,想要修改狀態(tài)直接調(diào)用setName就好了。整個(gè)代碼看起來(lái)更加簡(jiǎn)潔易于理解,我們不再關(guān)心要怎么維護(hù)保存狀態(tài),安安心心通過(guò)useState函數(shù)使用狀態(tài)就行了。而且函數(shù)的形式讓編譯器更容易去分析優(yōu)化代碼,移除無(wú)用的代碼塊,使生成的文件更小。
香不香?
我們可以發(fā)現(xiàn),Hook更偏向于我們向React聲明我們想要什么,這一點(diǎn)類(lèi)似于我們的界面描述方式,我們只說(shuō)我們要什么,而不是告訴框架該怎么做,代碼也更加簡(jiǎn)潔,方便其他人理解跟后期維護(hù),通過(guò)函數(shù)的方式我們也可以在組件間共享邏輯。
深入
那么Hook是怎么做到這么神奇的事情的呢,為了深入理解這背后的原理,我們從頭開(kāi)始實(shí)現(xiàn)一個(gè)我們自己的useState函數(shù)來(lái)理解這個(gè)過(guò)程。這個(gè)實(shí)現(xiàn)不會(huì)跟React的實(shí)現(xiàn)完全相同,我會(huì)盡量簡(jiǎn)化,將核心原理展示出來(lái)。
首先定義一個(gè)我們自己的useState函數(shù),方法簽名大家都知道了,要傳遞一個(gè)參數(shù)作為初始值。
function useState (initialState) {
然后我們定義一個(gè)值來(lái)保存我們的狀態(tài),一開(kāi)始,它的值會(huì)是我們傳給函數(shù)的initialState。
let value = initialState
然后我們要定義一個(gè)setState函數(shù),當(dāng)我們改變狀態(tài)值時(shí),重新渲染組件。
function setState (nextValue) {
value = nextValue
ReactDOM.render(<MyName />, document.getElementById('root'))
}
這邊的ReactDOM是用來(lái)重新渲染用的。
最終我們要把這個(gè)狀態(tài)值跟設(shè)置方法以數(shù)組的形式返回出去:
return [ value, setState ]
}
一個(gè)簡(jiǎn)單的Hook就實(shí)現(xiàn)了,Hook其實(shí)就是簡(jiǎn)單的js函數(shù),用來(lái)執(zhí)行一些有副作用的操作,比如用來(lái)設(shè)置一個(gè)有狀態(tài)的值。我們的Hook使用了一個(gè)閉包來(lái)保存狀態(tài)值,因?yàn)?code>setState跟value在同一個(gè)閉包下,所以我們的setState可以訪問(wèn)它,同理不把它傳遞出去的話在這個(gè)閉包外我們是沒(méi)辦法直接訪問(wèn)的。
來(lái)問(wèn)題了
如果我們現(xiàn)在運(yùn)行我們的代碼,我們會(huì)發(fā)現(xiàn)組件重新渲染的時(shí)候狀態(tài)重置了,然后我們就不能輸入任何文字。這是因?yàn)槊看沃匦落秩径颊{(diào)用了useState,然后導(dǎo)致value初始化了那我們得想辦法把狀態(tài)保存在別的地方避免因?yàn)橹匦落秩径艿接绊懥恕?/p>
我們先嘗試在函數(shù)外使用一個(gè)全局變量來(lái)保存我們的狀態(tài),那這樣的話我們的狀態(tài)就不會(huì)因?yàn)橹匦落秩径跏蓟恕?/p>
let value
function useState (initialState) {
在useState上定義了一個(gè)全局變量后,我們的初始化代碼也要改一改:
if (typeof value === 'undefined') value = initialState
這樣就沒(méi)問(wèn)題了。
但是緊接著,我們又發(fā)現(xiàn),當(dāng)我們想多調(diào)用幾次useState來(lái)管理多個(gè)狀態(tài)時(shí),它總在往同一個(gè)全局變量上寫(xiě)值,所有的useState方法都在操作同一個(gè)value!這肯定不是我們想要的結(jié)果。
那為了支持多個(gè)useState調(diào)用,我們要想辦法改進(jìn)一下,把變量替換成一個(gè)數(shù)組試試?
let values = []
let currentHook = 0
然后賦初始值的地方也要修改:
if (typeof values[currentHook] === 'undefined')
values[currentHook] = initialState
最重要的是我們的setState方法要修改好,這樣我們只會(huì)更新該更新的狀態(tài)值。我們需要把當(dāng)前Hook對(duì)應(yīng)的currentHook保存起來(lái),因?yàn)?code>currentHook是一直會(huì)變的。
let hookIndex = currentHook
function setState (nextValue) {
values[hookIndex] = nextValue
ReactDOM.render(<MyName />, document.getElementById('root'))
}
最終返回:
return [ values[currentHook++], setState ]
然后我們還要在開(kāi)始渲染的時(shí)候初始化一下currentHook:
function Name () {
currentHook = 0
現(xiàn)在我們的Hook可以說(shuō)是正常工作了

使用一個(gè)全局?jǐn)?shù)組保存Hook的value可以滿足多次調(diào)用useState的需求,React內(nèi)部實(shí)現(xiàn)也是類(lèi)似,不過(guò)它的實(shí)現(xiàn)更加復(fù)雜跟優(yōu)化,它自己處理好了計(jì)數(shù)器跟全局變量,而且也不需要我們手動(dòng)去重置計(jì)數(shù)器,不過(guò)大體原理咱算是把它摸清楚了。
那復(fù)雜場(chǎng)景來(lái)了
其實(shí)也不是什么復(fù)雜的場(chǎng)景啦,想象這樣一個(gè)情況,我們需要把輸入的姓名展示出來(lái),姓跟名分開(kāi)用狀態(tài)保存,同時(shí)我們想把姓做成選填那該怎么辦?
我們可以先用一個(gè)狀態(tài)記錄姓是不是必需的:
const [ enableFirstName, setEnableFirstName ] = useState(false)
然后我們定義一個(gè)處理函數(shù):
function handleEnableChange (evt) {
setEnableFirstName(!enableFirstName)
}
如果checkbox沒(méi)有勾選上我們就不打算渲染姓了,
<h1>My name is: {enableFirstName ? name : ''} {lastName}</h1>
我們能不能把Hook定義放進(jìn)一個(gè)if條件或者三目運(yùn)算符中去呢?像這樣:
const [ name, setName ] = enableFirstName
? useState('') : [ '', () => {} ]
現(xiàn)在yarn start來(lái)運(yùn)行我們的代碼,我們可以發(fā)現(xiàn)復(fù)選框沒(méi)有勾選時(shí),名還是可以修改的,姓隨你怎么改都沒(méi)用,這是我們想要的結(jié)果。

當(dāng)我們?cè)俅芜x中復(fù)選框時(shí),我們能修改姓了。但是奇怪的事發(fā)生了,名的值跑到姓那兒去了。

這是因?yàn)?code>Hook的順序很重要,我們都記得我們實(shí)現(xiàn)useState的時(shí)候,通過(guò)currentHook來(lái)確定當(dāng)前調(diào)用的狀態(tài)所在位置的,現(xiàn)在我們憑空插入了一個(gè)Hook調(diào)用,導(dǎo)致順序被打亂了,Hook在重新渲染時(shí)會(huì)重新確定索引,但是我們的全局?jǐn)?shù)組并不會(huì)變,導(dǎo)致姓去取了名的狀態(tài)。
勾選復(fù)選框之前的狀態(tài):
- [false, '客']
- 依次是:enableFirstName, lastName
勾選之后: - [true, '客', ' ']
- 依次是:enableFirstName, name, lastName
所以調(diào)用Hook的順序很重要! 這個(gè)限制在React官方提供的Hook中也存在,而且React也決定堅(jiān)持現(xiàn)在的設(shè)計(jì)。我們要避免這種寫(xiě)法,真有這種情況選擇的情況,不管用不用,都直接把可能要用的Hook聲明好,或者拆分出獨(dú)立的組件,在組件里使用Hook,把問(wèn)題轉(zhuǎn)換成要不要渲染某個(gè)組件,這也是React團(tuán)隊(duì)推薦的做法。
雖然有時(shí)候我們會(huì)覺(jué)得能在條件語(yǔ)句或者循環(huán)中這樣使用Hook更好,但是React團(tuán)隊(duì)為什么這么設(shè)計(jì)呢?有木有更好的方案呢?
有人提出了 NamedHook:
// 注意: 不是真實(shí)的React Hook API
const [ name, setName ] = useState('nameHook', '')
這樣做可以避免上面那種數(shù)據(jù)混亂的情況,每個(gè)Hook調(diào)用我們都設(shè)了一個(gè)獨(dú)特的名字,但是這樣做我們就得花時(shí)間想出獨(dú)一無(wú)二的名字,解決命名沖突,而且當(dāng)一個(gè)條件變成false的時(shí)候我們?cè)撛趺醋??如果一個(gè)元素從循環(huán)中刪除了我們?cè)撛趺醋觯课覀冊(cè)撉謇頎顟B(tài)嗎?如果不清理狀態(tài),內(nèi)存泄漏怎么辦?
我們可以看到,這樣并沒(méi)有讓事情變得簡(jiǎn)單,也引入了很多復(fù)雜的問(wèn)題,所以React團(tuán)隊(duì)最后堅(jiān)持了現(xiàn)在的設(shè)計(jì),讓API盡可能保持簡(jiǎn)單簡(jiǎn)單,而我們,在使用時(shí)要注意順序。
看到這兒的同學(xué)可能已經(jīng)躍躍欲試了,可能有同學(xué)會(huì)問(wèn)道,既然Hook能大大地簡(jiǎn)化代碼結(jié)構(gòu),讓代碼更加可維護(hù),我們是不是該把所有的組件都用Hook來(lái)重寫(xiě)呢? 當(dāng)然不—Hook是可選的。你可以在你的部分組件里面嘗試Hook,React團(tuán)隊(duì)現(xiàn)在還沒(méi)有打算移除類(lèi)組件。現(xiàn)在不急著把所有東西都重構(gòu)成基于Hook。而且Hook并不是銀彈,我們可以在覺(jué)得用Hook最恰當(dāng)?shù)牡胤接?code>Hook來(lái)實(shí)現(xiàn),比如, 你有許多組件處理相似的邏輯, 你可以把邏輯抽象成一個(gè)Hook,或者一個(gè)小組件用Hook實(shí)現(xiàn)會(huì)比較簡(jiǎn)單,有些地方狀態(tài)管理比較復(fù)雜那還是用類(lèi)組件會(huì)比較好。所以大部分情況下我們還是會(huì)函數(shù)組件跟類(lèi)組件一起混用。
結(jié)語(yǔ)
最后,相信大家對(duì)于Hook的作用跟實(shí)現(xiàn)原理想必有了個(gè)大體的了解,Hook就是一些簡(jiǎn)單的js函數(shù),大家看一眼文檔就知道怎么用啦,現(xiàn)在我們了解了Hook的優(yōu)點(diǎn)跟限制,可以在日常開(kāi)發(fā)中更好地做出選擇,本文的代碼看這里:示例代碼。