本文介紹的是react新特性react hooks,本文面向的是有必定react開發經驗的小夥伴,若是你對react還不是很熟悉的話我建議你先學習react並多多聯繫。node
首先咱們都知道react有3種組件;分別是Function函數式無狀態組件、class有狀態組件、高階組件。這裏不對這3種組件作一一介紹。react
本文重點是react hookses6
首先讓咱們看一下一個簡單的有狀態組件:ajax
1 class Example extends React.Component { 2 constructor(props) { 3 super(props); 4 this.state = { 5 count: 0 6 }; 7 } 8 9 render() { 10 return ( 11 <div> 12 <p>You clicked {this.state.count} times</p> 13 <button onClick={() => this.setState({ count: this.state.count + 1 })}> 14 Click me 15 </button> 16 </div> 17 ); 18 } 19 }
咱們再來看一下使用hooks後的版本:redux
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> ); }
是否是簡單多了!能夠看到,Example變成了一個函數,但這個函數卻有本身的狀態(count),同時它還能夠更新本身的狀態(setCount)。這個函數之因此這麼了不起,就是由於它注入了一個hook—useState,就是這個hook讓咱們的函數變成了一個有狀態的函數。數組
除了useState這個hook外,還有不少別的hook,好比useEffect提供了相似於componentDidMount等生命週期鉤子的功能,useContext提供了上下文(context)的功能等等。瀏覽器
Hooks本質上就是一類特殊的函數,它們能夠爲你的函數型組件(function component)注入一些特殊的功能。咦?這聽起來有點像被詬病的Mixins啊?難道是Mixins要在react中死灰復燃了嗎?固然不會了,等會咱們再來談二者的區別。總而言之,這些hooks的目標就是讓你再也不寫class,讓function一統江湖。session
想要複用一個有狀態的組件太麻煩了!閉包
咱們都知道react都核心思想就是,將一個頁面拆成一堆獨立的,可複用的組件,而且用自上而下的單向數據流的形式將這些組件串聯起來。但假如你在大型的工做項目中用react,你會發現你的項目中實際上不少react組件冗長且難以複用。尤爲是那些寫成class的組件,它們自己包含了狀態(state),因此複用這類組件就變得很麻煩。app
那以前,官方推薦怎麼解決這個問題呢?答案是:渲染屬性(Render Props)和高階組件(Higher-Order Components)。咱們能夠稍微跑下題簡單看一下這兩種模式。
渲染屬性指的是使用一個值爲函數的prop來傳遞須要動態渲染的nodes或組件。以下面的代碼能夠看到咱們的DataProvider組件包含了全部跟狀態相關的代碼,而Cat組件則能夠是一個單純的展現型組件,這樣一來DataProvider就能夠單獨複用了。
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>
高階組件這個概念就更好理解了,說白了就是一個函數接受一個組件做爲參數,通過一系列加工後,最後返回一個新的組件。看下面的代碼示例,withUser函數就是一個高階組件,它返回了一個新的組件,這個組件具備了它提供的獲取用戶信息的功能。
const 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);
以上這兩種模式看上去都挺不錯的,不少庫也運用了這種模式,好比咱們經常使用的React Router。但咱們仔細看這兩種模式,會發現它們會增長咱們代碼的層級關係。最直觀的體現,打開devtool看看你的組件層級嵌套是否是很誇張吧。這時候再回過頭看咱們上一節給出的hooks例子,是否是簡潔多了,沒有多餘的層級嵌套。把各類想要的功能寫成一個一個可複用的自定義hook,當你的組件想用什麼功能時,直接在組件裏調用這個hook便可
咱們一般但願一個函數只作一件事情,但咱們的生命週期鉤子函數裏一般同時作了不少事情。好比咱們須要在componentDidMount中發起ajax請求獲取數據,綁定一些事件監聽等等。同時,有時候咱們還須要在componentDidUpdate作一遍一樣的事情。當項目變複雜後,這一塊的代碼也變得不那麼直觀。
咱們用class來建立react組件時,還有一件很麻煩的事情,就是this的指向問題。爲了保證this的指向正確,咱們要常常寫這樣的代碼:this.handleClick = this.handleClick.bind(this),或者是這樣的代碼:<button onClick={() => this.handleClick(e)}>
。一旦咱們不當心忘了綁定this,各類bug就隨之而來,很麻煩。
還有一件讓我很苦惱的事情。我在以前的react系列文章當中曾經說過,儘量把你的組件寫成無狀態組件的形式,由於它們更方便複用,可獨立測試。然而不少時候,咱們用function寫了一個簡潔完美的無狀態組件,後來由於需求變更這個組件必須得有本身的state,咱們又得很麻煩的把function改爲class。
在這樣的背景下,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); }
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),這裏不詳細展開,咱們就按照官方推薦使用數組解構就好。
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
無論咱們反覆調用add函數多少次,結果都是1。由於每一次咱們調用add時,result變量都是從初始值0開始的。那爲何上面的Example函數每次執行的時候,都是拿的上一次執行完的狀態值做爲初始值?答案是:是react幫咱們記住的。至於react是用什麼機制記住的,咱們能夠再思考一下。
首先,useState是能夠屢次調用的,因此咱們徹底能夠這樣寫:
function ExampleWithManyStates() { const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
其次,useState接收的初始值沒有規定必定要是string/number/boolean這種簡單數據類型,它徹底能夠接收對象或者數組做爲參數。惟一須要注意的點是,以前咱們的this.setState作的是合併狀態後返回一個新狀態,而useState是直接替換老狀態後返回新狀態。最後,react也給咱們提供了一個useReducer的hook,若是你更喜歡redux式的狀態管理方案的話。
從ExampleWithManyStates函數咱們能夠看到,useState不管調用多少次,相互之間是獨立的。這一點相當重要。爲何這麼說呢?
其實咱們看hook的「形態」,有點相似以前被官方否認掉的Mixins這種方案,都是提供一種「插拔式的功能注入」的能力。而mixins之因此被否認,是由於Mixins機制是讓多個Mixins共享一個對象的數據空間,這樣就很難確保不一樣Mixins依賴的狀態不發生衝突。
而如今咱們的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的執行順序一致。
咱們在上一節的例子中增長一個新功能:
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> ); } }
咱們寫的有狀態組件,一般會產生不少的反作用(side effect),好比發起ajax請求獲取數據,添加一些監聽的註冊和取消註冊,手動修改dom等等。咱們以前都把這些反作用的函數寫在生命週期函數鉤子裏,好比componentDidMount,componentDidUpdate和componentWillUnmount。而如今的useEffect就至關與這些聲明周期函數鉤子的集合體。它以一抵三。
同時,因爲前文所說hooks能夠反覆屢次使用,相互獨立。因此咱們合理的作法是,給每個反作用一個單獨的useEffect鉤子。這樣一來,這些反作用再也不一股腦堆在生命週期鉤子裏,代碼變得更加清晰。
咱們再梳理一遍下面代碼的邏輯:
function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; });
首先,咱們聲明瞭一個狀態變量count,將它的初始值設爲0。而後咱們告訴react,咱們的這個組件有一個反作用。咱們給useEffecthook傳了一個匿名函數,這個匿名函數就是咱們的反作用。在這個例子裏,咱們的反作用是調用browser API來修改文檔標題。當react要渲染咱們的組件時,它會先記住咱們用到的反作用。等react更新了DOM以後,它再依次執行咱們定義的反作用函數。
這裏要注意幾點:
第一,react首次渲染和以後的每次渲染都會調用一遍傳給useEffect的函數。而以前咱們要用兩個聲明周期函數來分別表示首次渲染(componentDidMount),和以後的更新致使的從新渲染(componentDidUpdate)。
第二,useEffect中定義的反作用函數的執行不會阻礙瀏覽器更新視圖,也就是說這些函數是異步執行的,而以前的componentDidMount或componentDidUpdate中的代碼則是同步執行的。這種安排對大多數反作用說都是合理的,但有的狀況除外,好比咱們有時候須要先根據DOM計算出某個元素的尺寸再從新渲染,這時候咱們但願此次從新渲染是同步發生的,也就是說它會在瀏覽器真的去繪製這個頁面前發生。
這種場景很常見,當咱們在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'; }
這裏有一個點須要重視!這種解綁的模式跟componentWillUnmount不同。componentWillUnmount只會在組件被銷燬前執行一次而已,而useEffect裏的函數,每次組件渲染後都會執行一遍,包括反作用函數返回的這個清理函數也會從新執行一遍。因此咱們一塊兒來看一下下面這個問題。
咱們先看之前的模式:
componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); }
很清楚,咱們在componentDidMount註冊,再在componentWillUnmount清除註冊。但假如這時候props.friend.id變了怎麼辦?咱們不得再也不添加一個componentDidUpdate來處理這種狀況:
... 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傳第二個參數便可。用第二個參數來告訴react只有當這個參數的值發生改變時,才執行咱們傳的反作用函數(第一個參數)。
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // 只有當count的值發生變化時,纔會從新執行`document.title`這一句
當咱們第二個參數傳一個空數組[]時,其實就至關於只在首次渲染的時候執行。也就是componentDidMount加componentWillUnmount的模式。不過這種用法可能帶來bug,少用。
除了上文重點介紹的useState和useEffect,react還給咱們提供來不少有用的hooks:
useContext
useReducer
useCallback
useMemo
useRef
useImperativeMethods
useMutationEffect
useLayoutEffect
我再也不一一介紹,你們自行去查閱官方文檔。
爲何要本身去寫一個Effect Hooks? 這樣咱們才能把能夠複用的邏輯抽離出來,變成一個個能夠隨意插拔的「插銷」,哪一個組件要用來,我就插進哪一個組件裏,so easy!看一個完整的例子,你就明白了。
好比咱們能夠把上面寫的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> ); }