hooks 是 react 在16.8版本開始引入的一個新功能,它擴展了函數組件的功能,使得函數組件也能實現狀態、生命週期等複雜邏輯。javascript
import React, { useState } from 'react'; function Example() { // Declare a new state variable, which we'll call "count" const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
上面是 react 官方提供的 hooks 示例,使用了內置hookuseState
,對應到<u>Class Component</u>應該這麼實現html
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 就是鉤子,讓你能更方便地使用react相關功能。java
看完上面一段,你可能會以爲除了代碼塊精簡了點,沒看出什麼好處。別急,繼續往下看。react
過去,咱們習慣於使用<u>Class Component</u>,可是它存在幾個問題:git
狀態邏輯複用困難github
side effect 複用和組織困難json
class FriendStatusWithCounter extends React.Component { constructor(props) { super(props); this.state = { count: 0, isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { document.title = `You clicked ${this.state.count} times`; ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); } render() { return ( <div> <p>You clicked {this.state.count} times</p> <p>Friend {this.props.friend.id} status: {this.state.isOnline}</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div> ); } }
複用的問題就不說了,跟狀態邏輯同樣,主要說下代碼組織的問題。1. 爲了在組件刷新的時候更新文檔的標題,咱們在componentDidMount
和componentDidUpdate
中各寫了一遍更新邏輯; 2. 綁定朋友狀態更新和解綁的邏輯,分散在componentDidMount
和componentWillUnmount
中,實際上這是一對有關聯的邏輯,若是能寫在一塊兒最好;3. componentDidMount
中包含了更新文檔標題和綁定事件監聽,這2個操做自己沒有關聯,若是能分開到不一樣的代碼塊中更利於維護。redux
Javascript Class 天生缺陷數組
this
的指向問題,須要記得手動 bind 事件處理函數,這樣代碼看起來很繁瑣,除非引入@babel/plugin-proposal-class-properties
(這個提案目前還不穩定)。爲了解決上述問題,hooks 應運而生。讓咱們使用 hooks 改造下上面的例子promise
import React, { useState, useEffect } from 'react'; function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); // Specify how to clean up after this effect: return function cleanup() { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); return isOnline; } function FriendStatusWithCounter(props) { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); const isOnline = useFriendStatus(props.friend.id); return ( <div> <p>You clicked {count} times</p> <p>Friend {props.friend.id} status: {isOnline}</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } function FriendStatus(props) { // 經過自定義hook複用邏輯 const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }
看,問題都解決了!
hooks 通常配合<u>Function Components</u>使用,也能夠在內置 hooks 的基礎上封裝自定義 hook。
先介紹下 react 提供的內置 hooks。
const [count, setCount] = useState(0);
useState
接收一個參數做爲初始值,返回一個數組,數組的第一個元素是表示當前狀態值的變量,第二個參數是修改狀態的函數,執行的操做相似於this.setState({ count: someValue })
,固然內部的實現並不是如此,這裏僅爲了幫助理解。
useState
能夠屢次調用,每次當你須要聲明一個state時,就調用一次。
function ExampleWithManyStates() { // Declare multiple state variables! const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
須要更新某個具體狀態時,調用對應的 setXXX 函數便可。
useEffect的做用是讓你在<u>Function Components</u>裏面能夠執行一些 side effects,好比設置監聽、操做dom、定時器、請求等。
useEffect(() => { document.title = `You clicked ${count} times`; });
useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); // Specify how to clean up after this effect: return function cleanup() { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });
須要注意,上面這種寫法,每次組件更新都會執行 effect 的回調函數和清理函數,順序以下:
// Mount with { friend: { id: 100 } } props ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // Run first effect // Update with { friend: { id: 200 } } props ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // Run next effect // Update with { friend: { id: 300 } } props ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // Run next effect // Unmount ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect
這個效果等同於在componentDidMount
、componentDidUpdate
和componentWillUnmount
實現了事件綁定和解綁。若是隻是組件的 state 變化致使從新渲染,一樣會從新調用 cleanup 和 effect,這時候就顯得沒有必要了,因此 useEffect 支持用第2個參數來聲明依賴
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]);
第2個參數是一個數組,在數組中傳入依賴的 state 或者 props,若是依賴沒有更新,就不會從新執行 cleanup 和 effect。
若是你須要的是隻在初次渲染的時候執行一次 effect,組件卸載的時候執行一次 cleanup,那麼能夠傳一個空數組[]
做爲依賴。
context
這個概念你們應該不陌生,通常用於比較簡單的共享數據的場景。useContext
就是用於實現context
功能的 hook。
來看下官方提供的示例
const themes = { light: { foreground: "#000000", background: "#eeeeee" }, dark: { foreground: "#ffffff", background: "#222222" } }; const ThemeContext = React.createContext(themes.light); function App() { return ( <ThemeContext.Provider value={themes.dark}> <Toolbar /> </ThemeContext.Provider> ); } function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } function ThemedButton() { const theme = useContext(ThemeContext); return ( <button style={{ background: theme.background, color: theme.foreground }}> I am styled by theme context! </button> ); }
代碼挺長,可是一眼就能看懂了。把 context 對象傳入useContext
,就能夠拿到最新的 context value。
須要注意的是,只要使用了useContext
的組件,在 context value 改變後,必定會觸發組件的更新,哪怕他使用了React.memo
或是shouldComponentUpdate
。
useReducer(reducer, initialArg)
返回[state, dispatch]
,跟 redux 很像。
const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }
除此以外,react 內置的 hooks 還包括useCallback
、useMemo
、useRef
、useImperativeHandle
、useLayoutEffect
和useDebugValue
,這裏就再也不贅述了,能夠直接參考官方文檔。
基於內置 hook,咱們能夠封裝自定義的 hook,上面的示例中已經出現過useFriendStatus
這樣的自定義 hook,它能幫咱們抽離公共的組件邏輯,方便複用。注意,自定義 hook 也須要以use
開頭。
咱們能夠根據須要建立各類場景的自定義 hook,如表單處理、計時器等。後面實戰場景的章節中我會具體介紹幾個例子。
hooks 的使用須要遵循幾個規則:
之因此有這些規則限制,是跟 hooks 的實現原理有關。
這裏咱們嘗試實現一個簡單的版本的useState
和useEffect
用來講明。
const memoHooks = []; let cursor = 0; function useState(initialValue) { const current = cursor; const state = memoHooks[current] || initialValue; function setState(val) { memoHooks[current] = val; // 執行re-render操做 } cursor++; return [state, setState]; } function useEffect(cb, deps) { const hasDep = !!deps; const currentDeps = memoHooks[cursor]; const hasChanged = currentDeps ? !deps.every((val, i) => val === currentDeps[i]) : true; if (!hasDep || hasChanged) { cb(); memoHooks[cursor] = deps; } cursor++; }
此時咱們須要構造一個函數組件來使用這2個 hooks
function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); const [name, setName] = useState('Joe'); useEffect(() => { console.log(`Your name is ${name}`); }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
[]
,cursor 爲0
第一次渲染
const [count, setCount] = useState(0);
,memoHooks 爲[0]
,cursor 爲0
useEffect(() => { document.title =
You clicked ${count} times; }, [count]);
,memoHooks 爲[0, [0]]
,cursor 爲1
const [name, setName] = useState('Joe');
,memoHooks 爲[0, [0], 'Joe']
,cursor 爲2
useEffect(() => { console.log(
Your name is ${name}); });
,memoHooks 爲[0, [0], 'Joe', undefined]
,cursor 爲3
點擊按鈕
setCount(count + 1)
,memoHooks 爲[1, [0], 'Joe', undefined]
,cursor 爲0
re-render
const [count, setCount] = useState(0);
,memoHooks 爲[1, [0], 'Joe', undefined]
,cursor 爲0
useEffect(() => { document.title =
You clicked ${count} times; }, [count]);
,memoHooks 爲[1, [1], 'Joe', undefined]
,cursor 爲1
。這裏因爲hooks[1]
的值變化,會致使 cb 再次執行。const [name, setName] = useState('Joe');
,memoHooks 爲[1, [1], 'Joe', undefined]
,cursor 爲2
useEffect(() => { console.log(
Your name is ${name}); });
,memoHooks 爲[1, [1], 'Joe', undefined]
,cursor 爲3
。這裏因爲依賴爲 undefined,致使 cb 再次執行。經過上述示例,應該能夠解答爲何 hooks 要有這樣的使用規則了。
memoHooks
和cursor
其實都跟當前渲染的組件實例綁定,脫離了<u>Function Components</u>,hooks 也沒法正確執行。固然,這些只是爲了方便理解作的一個簡單demo,react 內部其實是經過一個單向鏈表來實現,並不是 array,有興趣能夠自行翻閱源碼。
實現一個hook,支持自動獲取輸入框的內容。
function useInput(initial) { const [value, setValue] = useState(initial); const onChange = useCallback(function(event) { setValue(event.currentTarget.value); }, []); return { value, onChange }; } // 使用示例 function Example() { const inputProps = useInput('Joe'); return <input {...inputProps} /> }
實現一個網絡請求hook,可以支持初次渲染後自動發請求,也能夠手動請求。參數傳入一個請求函數便可。
function useRequest(reqFn) { const initialStatus = { loading: true, result: null, err: null }; const [status, setStatus] = useState(initialStatus); function run() { reqFn().then(result => { setStatus({ loading: false, result, err: null }) }).catch(err => { setStatus({ loading: false, result: null, err }); }); } // didMount後執行一次 useEffect(run, []); return { ...status, run }; } // 使用示例 function req() { // 發送請求,返回promise return fetch('http://example.com/movies.json'); } function Example() { const { loading, result, err, run } = useRequest(req); return ( <div> <p> The result is {loading ? 'loading' : JSON.stringify(result || err)} </p> <button onClick={run}>Reload</button> </div> ); }
上面2個例子只是實戰場景中很小的一部分,卻足以看出 hooks 的強大,當咱們有豐富的封裝好的 hooks 時,業務邏輯代碼會變得很簡潔。推薦一個github repo,這裏羅列了不少社區產出的 hooks lib,有須要自取。
根據官方的說法,在可見的將來 react team 並不會中止對 class component 的支持,由於如今絕大多數 react 組件都是以 class 形式存在的,要所有改造並不現實,並且 hooks 目前還不能徹底取代 class,好比getSnapshotBeforeUpdate
和componentDidCatch
這2個生命週期,hooks尚未對等的實現辦法。建議你們能夠在新開發的組件中嘗試使用 hooks。若是通過長時間的迭代後 function components + hooks 成爲主流,且 hooks 從功能上能夠徹底替代 class,那麼 react team 應該就能夠考慮把 class component 移除,畢竟沒有必要維護2套實現,這樣不只增長了維護成本,對開發者來講也多一份學習負擔。