1. 程式人生 > >30 分鐘精通 React 新特性React Hooks

30 分鐘精通 React 新特性React Hooks

你還在為該使用無狀態元件(Function)還是有狀態元件(Class)而煩惱嗎?——擁有了hooks,你再也不需要寫Class了,你的所有元件都將是Function。

你還在為搞不清使用哪個生命週期鉤子函式而日夜難眠嗎?——擁有了Hooks,生命週期鉤子函式可以先丟一邊了。

你在還在為元件中的this指向而暈頭轉向嗎?——既然Class都丟掉了,哪裡還有this?你的人生第一次不再需要面對this。

這樣看來,說React Hooks是今年最勁爆的新特性真的毫不誇張。如果你也對react感興趣,或者正在使用react進行專案開發,答應我,請一定抽出至少30分鐘的時間來閱讀本文好嗎?所有你需要了解的React Hooks的知識點,本文都涉及到了,相信完整讀完後你一定會有所收穫。

一個最簡單的Hooks

首先讓我們看一下一個簡單的有狀態元件:

81203ebffd814e0abe27545fdbcd7613.png

我們再來看一下使用hooks後的版本:

b4f68ab8f0584ebcb87ab0b1f27f7e08.png

是不是簡單多了!可以看到, Example變成了一個函式,但這個函式卻有自己的狀態(count),同時它還可以更新自己的狀態(setCount)。這個函式之所以這麼了不得,就是因為它注入了一個hook-- useState,就是這個hook讓我們的函式變成了一個有狀態的函式。

除了 useState這個hook外,還有很多別的hook,比如 useEffect提供了類似於 componentDidMount等生命週期鉤子的功能, useContext提供了上下文(context)的功能等等。

Hooks本質上就是一類特殊的函式,它們可以為你的函式型元件(function component)注入一些特殊的功能。咦?這聽起來有點像被詬病的Mixins啊?難道是Mixins要在react中死灰復燃了嗎?當然不會了,等會我們再來談兩者的區別。總而言之,這些hooks的目標就是讓你不再寫class,讓function一統江湖。

React為什麼要搞一個Hooks?

想要複用一個有狀態的元件太麻煩了!

我們都知道react都核心思想就是,將一個頁面拆成一堆獨立的,可複用的元件,並且用自上而下的單向資料流的形式將這些元件串聯起來。但假如你在大型的工作專案中用react,你會發現你的專案中實際上很多react元件冗長且難以複用。尤其是那些寫成class的元件,它們本身包含了狀態(state),所以複用這類元件就變得很麻煩。

那之前,官方推薦怎麼解決這個問題呢?答案是:渲染屬性(Render Props)和高階元件(Higher-Order Components)。我們可以稍微跑下題簡單看一下這兩種模式。

渲染屬性指的是使用一個值為函式的prop來傳遞需要動態渲染的nodes或元件。如下面的程式碼可以看到我們的 DataProvider元件包含了所有跟狀態相關的程式碼,而 Cat元件則可以是一個單純的展示型元件,這樣一來 DataProvider就可以單獨複用了。

1c193f54aaf048b2970ca07704943a3e.png

雖然這個模式叫Render Props,但不是說非用一個叫render的props不可,習慣上大家更常寫成下面這種:

d85dc3063f6a4de2b352738b44777147.png

高階元件這個概念就更好理解了,說白了就是一個函式接受一個元件作為引數,經過一系列加工後,最後返回一個新的元件。看下面的程式碼示例, withUser函式就是一個高階元件,它返回了一個新的元件,這個元件具有了它提供的獲取使用者資訊的功能。

df2c6592b22e454a9bc9cfc49efe0117.png

以上這兩種模式看上去都挺不錯的,很多庫也運用了這種模式,比如我們常用的React Router。但我們仔細看這兩種模式,會發現它們會增加我們程式碼的層級關係。最直觀的體現,開啟devtool看看你的元件層級巢狀是不是很誇張吧。這時候再回過頭看我們上一節給出的hooks例子,是不是簡潔多了,沒有多餘的層級巢狀。把各種想要的功能寫成一個一個可複用的自定義hook,當你的元件想用什麼功能時,直接在元件裡呼叫這個hook即可。

生命週期鉤子函式裡的邏輯太亂了吧!

我們通常希望一個函式只做一件事情,但我們的生命週期鉤子函式裡通常同時做了很多事情。比如我們需要在 componentDidMount中發起ajax請求獲取資料,繫結一些事件監聽等等。同時,有時候我們還需要在 componentDidUpdate做一遍同樣的事情。當專案變複雜後,這一塊的程式碼也變得不那麼直觀。

classes真的太讓人困惑了!

我們用class來建立react元件時,還有一件很麻煩的事情,就是this的指向問題。為了保證this的指向正確,我們要經常寫這樣的程式碼: this.handleClick=this.handleClick.bind(this),或者是這樣的程式碼: <buttononClick={()=>this.handleClick(e)}>。一旦我們不小心忘了繫結this,各種bug就隨之而來,很麻煩。

還有一件讓我很苦惱的事情。我在之前的react系列文章當中曾經說過,儘可能把你的元件寫成無狀態元件的形式,因為它們更方便複用,可獨立測試。然而很多時候,我們用function寫了一個簡潔完美的無狀態元件,後來因為需求變動這個元件必須得有自己的state,我們又得很麻煩的把function改成class。

在這樣的背景下,Hooks便橫空出世了!

什麼是State Hooks?

回到一開始我們用的例子,我們分解來看到底state hooks做了什麼:

 

c8472d53562340348b2be7463665fb68.png

宣告一個狀態變數

 

6dbfc72c1004433c80beeae68b2ea856.png

useState是react自帶的一個hook函式,它的作用就是用來宣告狀態變數。 useState這個函式接收的引數是我們的狀態初始值(initial state),它返回了一個數組,這個陣列的第 [0]項是當前當前的狀態值,第 [1]項是可以改變狀態值的方法函式。

所以我們做的事情其實就是,聲明瞭一個狀態變數count,把它的初始值設為0,同時提供了一個可以更改count的函式setCount。

上面這種表達形式,是借用了es6的陣列解構(array destructuring),它可以讓我們的程式碼看起來更簡潔。不清楚這種用法的可以先去看下我的這篇文章:30分鐘掌握ES6/ES2015核心內容(上)。

如果不用陣列解構的話,可以寫成下面這樣。實際上陣列解構是一件開銷很大的事情,用下面這種寫法,或者改用物件解構,效能會有很大的提升。具體可以去這篇文章的分析:Array destructuring for multi-value returns (in light of React hooks),這裡不詳細展開,我們就按照官方推薦使用陣列解構就好。

 

126a65e3966b416b893eb97abed431b7.png

讀取狀態值

 

f3c972c4723a45349d47d0e087dcdaf0.png

是不是超簡單?因為我們的狀態count就是一個單純的變數而已,我們再也不需要寫成 {this.state.count}這樣了。

更新狀態

 

277e7afc1d404e3aa7754794b434922d.png

當用戶點選按鈕時,我們呼叫setCount函式,這個函式接收的引數是修改過的新狀態值。接下來的事情就交給react了,react將會重新渲染我們的Example元件,並且使用的是更新後的新的狀態,即count=1。這裡我們要停下來思考一下,Example本質上也是一個普通的函式,為什麼它可以記住之前的狀態?

一個至關重要的問題

這裡我們就發現了問題,通常來說我們在一個函式中宣告的變數,當函式執行完成後,這個變數也就銷燬了(這裡我們先不考慮閉包等情況),比如考慮下面的例子:

 

81217645678645e69bcc7f74f22ec4e8.png

不管我們反覆呼叫add函式多少次,結果都是1。因為每一次我們呼叫add時,result變數都是從初始值0開始的。那為什麼上面的Example函式每次執行的時候,都是拿的上一次執行完的狀態值作為初始值?答案是:是react幫我們記住的。至於react是用什麼機制記住的,我們可以再思考一下。

假如一個元件有多個狀態值怎麼辦?

首先,useState是可以多次呼叫的,所以我們完全可以這樣寫:

 

e075c454f5754f0a9655c07256c4b7b4.png

其次,useState接收的初始值沒有規定一定要是string/number/boolean這種簡單資料型別,它完全可以接收物件或者陣列作為引數。唯一需要注意的點是,之前我們的 this.setState做的是合併狀態後返回一個新狀態,而 useState是直接替換老狀態後返回新狀態。最後,react也給我們提供了一個useReducer的hook,如果你更喜歡redux式的狀態管理方案的話。

從ExampleWithManyStates函式我們可以看到,useState無論呼叫多少次,相互之間是獨立的。這一點至關重要。為什麼這麼說呢?

其實我們看hook的“形態”,有點類似之前被官方否定掉的Mixins這種方案,都是提供一種“插拔式的功能注入”的能力。而mixins之所以被否定,是因為Mixins機制是讓多個Mixins共享一個物件的資料空間,這樣就很難確保不同Mixins依賴的狀態不發生衝突。

而現在我們的hook,一方面它是直接用在function當中,而不是class;另一方面每一個hook都是相互獨立的,不同元件呼叫同一個hook也能保證各自狀態的獨立性。這就是兩者的本質區別了。

react是怎麼保證多個useState的相互獨立的?

還是看上面給出的ExampleWithManyStates例子,我們呼叫了三次useState,每次我們傳的引數只是一個值(如42,‘banana’),我們根本沒有告訴react這些值對應的key是哪個,那react是怎麼保證這三個useState找到它對應的state呢?

答案是,react是根據useState出現的順序來定的。我們具體來看一下:

 

5ffadde7918b422a9f8fa340171a2a3f.png

假如我們改一下程式碼:

e47ea3ebbf4a42e1ac3247dad62a376a.png

這樣一來,

503fe0248f4e4a9786eb52b07ee11dc5.png

鑑於此,react規定我們必須把hooks寫在函式的最外層,不能寫在ifelse等條件語句當中,來確保hooks的執行順序一致。

什麼是Effect Hooks?

我們在上一節的例子中增加一個新功能:

 

973780d510c143f8b8533006784955c0.png

我們對比著看一下,如果沒有hooks,我們會怎麼寫?

0fe2d7928ab641d197824eabe6eb0712.png

我們寫的有狀態元件,通常會產生很多的副作用(side effect),比如發起ajax請求獲取資料,新增一些監聽的註冊和取消註冊,手動修改dom等等。我們之前都把這些副作用的函式寫在生命週期函式鉤子裡,比如componentDidMount,componentDidUpdate和componentWillUnmount。而現在的useEffect就相當與這些宣告周期函式鉤子的集合體。它以一抵三。

同時,由於前文所說hooks可以反覆多次使用,相互獨立。所以我們合理的做法是,給每一個副作用一個單獨的useEffect鉤子。這樣一來,這些副作用不再一股腦堆在生命週期鉤子裡,程式碼變得更加清晰。

useEffect做了什麼?

我們再梳理一遍下面程式碼的邏輯:

 

71a2b81ea3f04c7cb1717b71d07be8d7.png

 

首先,我們聲明瞭一個狀態變數 count,將它的初始值設為0。然後我們告訴react,我們的這個元件有一個副作用。我們給 useEffecthook傳了一個匿名函式,這個匿名函式就是我們的副作用。在這個例子裡,我們的副作用是呼叫browser API來修改文件標題。當react要渲染我們的元件時,它會先記住我們用到的副作用。等react更新了DOM之後,它再依次執行我們定義的副作用函式。

這裡要注意幾點:

第一,react首次渲染和之後的每次渲染都會呼叫一遍傳給useEffect的函式。而之前我們要用兩個宣告周期函式來分別表示首次渲染(componentDidMount),和之後的更新導致的重新渲染(componentDidUpdate)。

第二,useEffect中定義的副作用函式的執行不會阻礙瀏覽器更新檢視,也就是說這些函式是非同步執行的,而之前的componentDidMount或componentDidUpdate中的程式碼則是同步執行的。這種安排對大多數副作用說都是合理的,但有的情況除外,比如我們有時候需要先根據DOM計算出某個元素的尺寸再重新渲染,這時候我們希望這次重新渲染是同步發生的,也就是說它會在瀏覽器真的去繪製這個頁面前發生。

useEffect怎麼解綁一些副作用

這種場景很常見,當我們在componentDidMount裡添加了一個註冊,我們得馬上在componentWillUnmount中,也就是元件被登出之前清除掉我們新增的註冊,否則記憶體洩漏的問題就出現了。

怎麼清除呢?讓我們傳給useEffect的副作用函式返回一個新的函式即可。這個新的函式將會在元件下一次重新渲染之後執行。這種模式在一些pubsub模式的實現中很常見。看下面的例子:

815762066faa4b1abadc040b4ea02c11.png

這裡有一個點需要重視!這種解綁的模式跟componentWillUnmount不一樣。componentWillUnmount只會在元件被銷燬前執行一次而已,而useEffect裡的函式,每次元件渲染後都會執行一遍,包括副作用函式返回的這個清理函式也會重新執行一遍。所以我們一起來看一下下面這個問題。

為什麼要讓副作用函式每次元件更新都執行一遍?

我們先看以前的模式:

 

b8ea99ea94de44f689272d3968574bda.png

很清除,我們在componentDidMount註冊,再在componentWillUnmount清除註冊。但假如這時候 props.friend.id變了怎麼辦?我們不得不再新增一個componentDidUpdate來處理這種情況:

4a33e00c2e0947fd885f1a63d8e91d12.png

看到了嗎?很繁瑣,而我們但useEffect則沒這個問題,因為它在每次元件更新後都會重新執行一遍。所以程式碼的執行順序是這樣的:

 

  • 頁面首次渲染

  • 替friend.id=1的朋友註冊

  • 突然friend.id變成了2

  • 頁面重新渲染

  • 清除friend.id=1的繫結

  • 替friend.id=2的朋友註冊

  • ...

 

怎麼跳過一些不必要的副作用函式

按照上一節的思路,每次重新渲染都要執行一遍這些副作用函式,顯然是不經濟的。怎麼跳過一些不必要的計算呢?我們只需要給useEffect傳第二個引數即可。用第二個引數來告訴react只有當這個引數的值發生改變時,才執行我們傳的副作用函式(第一個引數)。

 

d9185fb8903e4f419377448ec9ca4a94.png

當我們第二個引數傳一個空陣列[]時,其實就相當於只在首次渲染的時候執行。也就是componentDidMount加componentWillUnmount的模式。不過這種用法可能帶來bug,少用。

還有哪些自帶的Effect Hooks?

除了上文重點介紹的useState和useEffect,react還給我們提供來很多有用的hooks:

 

  • useContext

  • useReducer

  • useCallback

  • useMemo

  • useRef

  • useImperativeMethods

  • useMutationEffect

  • useLayoutEffect

 

我不再一一介紹,大家自行去查閱官方文件。

怎麼寫自定義的Effect Hooks?

為什麼要自己去寫一個Effect Hooks? 這樣我們才能把可以複用的邏輯抽離出來,變成一個個可以隨意插拔的“插銷”,哪個元件要用來,我就插進哪個元件裡,so easy!看一個完整的例子,你就明白了。

比如我們可以把上面寫的FriendStatus元件中判斷朋友是否線上的功能抽出來,新建一個useFriendStatus的hook專門用來判斷某個id是否線上。

 

6b769400d32b4a9eb1cc782d97a24de7.png

這時候FriendStatus元件就可以簡寫為:

11ee813c5b254738bb6ca67774843ecf.png

簡直Perfect!假如這個時候我們又有一個朋友列表也需要顯示是否線上的資訊:

27348fa2c0914e41aec9ce1f9642896b.png

簡直Fabulous!

 

編輯:千鋒HTML5

作者:zach5078

segmentfault.com/a/1190000016950339