——擁有了hooks,你再也不需要寫Class了,你的所有組件都將是Function。
你還在為搞不清使用哪個生命周期鈎子函數而日夜難眠嗎?
——擁有了Hooks,生命周期鈎子函數可以先丟一邊了。
你在還在為組件中的this指向而暈頭轉向嗎?
——既然Class都丟掉了,哪里還有this?你的人生第一次不再需要面對this。
一個最簡單的Hooks
首先讓我們看一下一個簡單的有狀態組件:
class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } render() { return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div> ); } }
我們再來看下使用了使用hooks后的版本:
import { useState } from 'react'; function Example() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
除了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)。我們可以稍微跑下題簡單看一下這兩種模式。
import Cat from 'components/cat' class DataProvider extends React.Component { constructor(props) { super(props); this.state = { target: 'Zac' }; } render() { return ( <div> {this.props.render(this.state)} </div> ) } } <DataProvider render={data => ( <Cat target={data.target} /> )}/>
雖然這個模式叫Render Props,但不是說非用一個叫render的props不可,習慣上大家更常寫成下面這種:
<DataProvider> {data => ( <Cat target={data.target} /> )} </DataProvider>
onst withUser = WrappedComponent => { const user = sessionStorage.getItem("user"); return props => <WrappedComponent user={user} {...props} />; }; const UserPage = props => ( <div class="user-container"> <p>My name is {props.user}!</p> </div> ); export default withUser(UserPage);

我們通常希望一個函數只做一件事情,但我們的生命周期鈎子函數里通常同時做了很多事情。比如我們需要在componentDidMount中發起ajax請求獲取數據,綁定一些事件監聽等等。同時,有時候我們還需要在componentDidUpdate做一遍同樣的事情。當項目變復雜后,這一塊的代碼也變得不那么直觀。
classes真的太讓人困惑了!
還有一件讓我很苦惱的事情。我在之前的react系列文章當中曾經說過,盡可能把你的組件寫成無狀態組件的形式,因為它們更方便復用,可獨立測試。然而很多時候,我們用function寫了一個簡潔完美的無狀態組件,后來因為需求變動這個組件必須得有自己的state,我們又得很麻煩的把function改成class。
在這樣的背景下,Hooks便橫空出世了!
什么是State Hooks?
回到一開始我們用的例子,我們分解來看到底state hooks做了什么:import { useState } from 'react'; function Example() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
聲明一個狀態變量
import { useState } from 'react'; function Example() { const [count, setCount] = useState(0);
所以我們做的事情其實就是,聲明了一個狀態變量count,把它的初始值設為0,同時提供了一個可以更改count的函數setCount。
let _useState = useState(0); let count = _useState[0]; let setCount = _useState[1];
<p>You clicked {count} times</p>
是不是超簡單?因為我們的狀態count就是一個單純的變量而已,我們再也不需要寫成{this.state.count}這樣了。
更新狀態
<button onClick={() => setCount(count + 1)}>
Click me
</button>
當用戶點擊按鈕時,我們調用setCount函數,這個函數接收的參數是修改過的新狀態值。接下來的事情就交給react了,react將會重新渲染我們的Example組件,並且使用的是更新后的新的狀態,即count=1。這里我們要停下來思考一下,Example本質上也是一個普通的函數,為什么它可以記住之前的狀態?
這里我們就發現了問題,通常來說我們在一個函數中聲明的變量,當函數運行完成后,這個變量也就銷毀了(這里我們先不考慮閉包等情況),比如考慮下面的例子:
function add(n) { const result = 0; return result + 1; } add(1); //1 add(1); //1
假如一個組件有多個狀態值怎么辦?
首先,useState是可以多次調用的,所以我們完全可以這樣寫:
function ExampleWithManyStates() { const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}
從ExampleWithManyStates函數我們可以看到,useState無論調用多少次,相互之間是獨立的。這一點至關重要。為什么這么說呢?
而現在我們的hook,一方面它是直接用在function當中,而不是class;另一方面每一個hook都是相互獨立的,不同組件調用同一個hook也能保證各自狀態的獨立性。這就是兩者的本質區別了。
還是看上面給出的ExampleWithManyStates例子,我們調用了三次useState,每次我們傳的參數只是一個值(如42,‘banana’),我們根本沒有告訴react這些值對應的key是哪個,那react是怎么保證這三個useState找到它對應的state呢?
答案是,react是根據useState出現的順序來定的。我們具體來看一下
//第一次渲染 useState(42); //將age初始化為42 useState('banana'); //將fruit初始化為banana useState([{ text: 'Learn Hooks' }]); //... //第二次渲染 useState(42); //讀取狀態變量age的值(這時候傳的參數42直接被忽略) useState('banana'); //讀取狀態變量fruit的值(這時候傳的參數banana直接被忽略) useState([{ text: 'Learn Hooks' }]); //...
假如我們改一下代碼:
let showFruit = true; function ExampleWithManyStates() { const [age, setAge] = useState(42); if(showFruit) { const [fruit, setFruit] = useState('banana'); showFruit = false; } const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
這樣一來,
//第一次渲染 useState(42); //將age初始化為42 useState('banana'); //將fruit初始化為banana useState([{ text: 'Learn Hooks' }]); //... //第二次渲染 useState(42); //讀取狀態變量age的值(這時候傳的參數42直接被忽略) // useState('banana'); useState([{ text: 'Learn Hooks' }]); //讀取到的卻是狀態變量fruit的值,導致報錯
鑒於此,react規定我們必須把hooks寫在函數的最外層,不能寫在ifelse等條件語句當中,來確保hooks的執行順序一致。
什么是Effect Hooks?
我們在上一節的例子中增加一個新功能:
import { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); // 類似於componentDidMount 和 componentDidUpdate: useEffect(() => { // 更新文檔的標題 document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
我們對比着看一下,如果沒有hooks,我們會怎么寫?
class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() { document.title = `You clicked ${this.state.count} times`; } componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; } render() { return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div> ); } }
同時,由於前文所說hooks可以反復多次使用,相互獨立。所以我們合理的做法是,給每一個副作用一個單獨的useEffect鈎子。這樣一來,這些副作用不再一股腦堆在生命周期鈎子里,代碼變得更加清晰。
useEffect做了什么?
我們再梳理一遍下面代碼的邏輯:
function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; });
這里要注意幾點:
第一,react首次渲染和之后的每次渲染都會調用一遍傳給useEffect的函數。而之前我們要用兩個聲明周期函數來分別表示首次渲染(componentDidMount),和之后的更新導致的重新渲染(componentDidUpdate)。
第二,useEffect中定義的副作用函數的執行不會阻礙瀏覽器更新視圖,也就是說這些函數是異步執行的,而之前的componentDidMount或componentDidUpdate中的代碼則是同步執行的。這種安排對大多數副作用說都是合理的,但有的情況除外,比如我們有時候需要先根據DOM計算出某個元素的尺寸再重新渲染,這時候我們希望這次重新渲染是同步發生的,也就是說它會在瀏覽器真的去繪制這個頁面前發生。
useEffect怎么解綁一些副作用
這種場景很常見,當我們在componentDidMount里添加了一個注冊,我們得馬上在componentWillUnmount中,也就是組件被注銷之前清除掉我們添加的注冊,否則內存泄漏的問題就出現了。
怎么清除呢?讓我們傳給useEffect的副作用函數返回一個新的函數即可。這個新的函數將會在組件下一次重新渲染之后執行。這種模式在一些pubsub模式的實現中很常見。看下面的例子:
import { useState, useEffect } from 'react'; function FriendStatus(props) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); // 一定注意下這個順序:告訴react在下次重新渲染組件之后,同時是下次調用ChatAPI.subscribeToFriendStatus之前執行cleanup return function cleanup() { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }
我們先看以前的模式:
componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); }
componentDidUpdate(prevProps) { // 先把上一個friend.id解綁 ChatAPI.unsubscribeFromFriendStatus( prevProps.friend.id, this.handleStatusChange ); // 再重新注冊新但friend.id ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); }
看到了嗎?很繁瑣,而我們但useEffect則沒這個問題,因為它在每次組件更新后都會重新執行一遍。所以代碼的執行順序是這樣的:
1.頁面首次渲染 2.替friend.id=1的朋友注冊 3.突然friend.id變成了2 4.頁面重新渲染 5.清除friend.id=1的綁定 6.替friend.id=2的朋友注冊
怎么跳過一些不必要的副作用函數
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // 只有當count的值發生變化時,才會重新執行`document.title`這一句
當我們第二個參數傳一個空數組[]時,其實就相當於只在首次渲染的時候執行。也就是componentDidMount加componentWillUnmount的模式。不過這種用法可能帶來bug,少用。
還有哪些自帶的Effect Hooks?
除了上文重點介紹的useState和useEffect,react還給我們提供來很多有用的hooks:
useContext
useReducer
useCallback
useMemo
useRef
useImperativeMethods
useMutationEffect
useLayoutEffect
我不再一一介紹,大家自行去查閱官方文檔。
怎么寫自定義的Effect Hooks?
比如我們可以把上面寫的FriendStatus組件中判斷朋友是否在線的功能抽出來,新建一個useFriendStatus的hook專門用來判斷某個id是否在線。
import { useState, useEffect } from 'react'; function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; }
這時候FriendStatus組件就可以簡寫為:
function FriendStatus(props) { const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }
簡直Perfect!假如這個時候我們又有一個朋友列表也需要顯示是否在線的信息:
function FriendListItem(props) { const isOnline = useFriendStatus(props.friend.id); return ( <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li> ); }