最近團隊裡 @大果 分享了 React Hooks,也嘗試討論下 Function Component 與 React Hooks,技術的發展路徑總是逐步降低其門檻,簡單從輕量級角度我們做一個排序:
1 |
createClass Component > Class Component > Function Component |
技術上 Class Component 是可以完全代替 createClass Component 方式,所以已經是廢棄不推薦使用,那是不是 Function Component 也可以完全替代 Class Component?在沒有 Hooks 之前顯然是無法做到的。
1 2 3 |
function Hey(props, context) { return <span>Good {props.boy}</span> } |
Function Component 沒有內部狀態變化機制,只能從外部進行狀態的驅動,元件的可測試性也非常高,是一個沒有爭議的 Good Design。
但這個 Design 並沒法替代 Class Component,只是一個可選。所以現實是無論一個元件內部是不是有狀態,大部分開發者一定是用思維慣性在程式設計,或者說 make it work first,都用 Class Component 沒毛病。
但當下基於 Class Component 的 React 應用有哪些小問題?
第一,曾經 createClass 不用關心 this 的問題,現在很糟心,比如下面 handleClick
的 this 並不是當前 Hey 類的例項,一不小就異常,這雖然不是 Class Component 的鍋,但確實是使用者側存在的多數問題。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Hey extends Component { state = { name: 'yiyi', emoji: '?'; }; handleClick() { // throw error this.setState({ emoji: '?' }); } render() { return <span onClick={this.handleClick}>Hey {this.state.name}, {this.state.emoji}</span> } } |
第二,當前的 React 應用很容易陷入 標籤巢狀地獄
的情形,比如下面用到 Context 的場景就非常典型,看著眼花繚亂。在資料同步場景裡,
因為需要通知更新所有引用資料的地方,所以通過 render-props 形式定義在 Context.Consumer 的 Children 中,使用到越多的 Context 就會導致巢狀越多的層級,這簡直是噩夢。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
render() { return ( <FooContext.Provider value={this.state.foo}> <BarContext.Provider value={this.state.bar}> <BarContext.Consumer> {bar => ( <FooContext.Consumer> {foo => ( console.log(bar, foo) )} </FooContext.Consumer> )} </BarContext.Consumer> </BarContext.Provider> </FooContext.Provider> ) } |
第三,一些有狀態的邏輯比較難重用。這個其實不算 React 獨有的問題,只能說當前主流前端架構體系裡都沒有很好的解決方案。
因此 React 團隊基於 Function Component 提出 Hooks 的概念,Hooks 有幾個關鍵 API: useState、useEffect、useContext。這些 API 讓 React 更 Reactive:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import { useState, useContext, useEffect, createContext } from 'react'; const FooContext = createContext('foo'); const BarContext = createContext('bar'); function Hey(props, context) { const [name, setName] = useState('yiyi'); const [emoji, setEmoji] = useState('?'); const foo = useContext(FooContext); const bar = useContext(BarContext); const handleClick = () => setEmoji('?'); useEffect(() => { console.log('componentDidMount or componentDidUpdate'); return () => { console.log('componentWillUnmount'); } }, [name]); return ( <> <span onClick={handleClick}>Hey {name}, {emoji}</span> <FooContext.Provider> <BarContext.Provider> {foo}, {bar} </BarContext.Provider> </FooContext.Provider> </> ) } |
基於 Function Component 與 Hooks 整體程式碼是比較簡潔的,也直接避免了 this 指向的問題,對比上文中 標籤巢狀地獄
的程式碼,尤其使用 useContext 看起來的確舒服太多了,在使用 Context 的地方儘量通過 Function Component 結合 useContext hook 應該是未來的最佳實踐。
Hooks 在架構上最值得稱讚是提供一種有狀態邏輯的複用機制,並且是通過組合的方式。如下使用 hooks 機制對頁面是否可見狀態的封裝:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
let { useState, useEffect } = require('react'); function useDocumentVisibility() { let [documentVisibility, setDocumentVisibility] = useState(document.visibilityState); function handleVisibilityChange() { setDocumentVisibility(document.visibilityState); } useEffect(() => { window.addEventListener('visibilitychange', handleVisibilityChange); return () => { window.removeEventListener('visibilitychange', handleVisibilityChange); }; }, []); return documentVisibility; } function Hey() { let documentVisibility = useDocumentVisibility(); return {documentVisibility === 'visible' ? <span>hi</span>: null} } |
通過 Hooks 可以方便的把狀態關注點進行分離,每一個狀態分離後可複用,對於一個高複雜邏輯的專案,往往有非常多的業務資料狀態,比如A頁面與B頁面都有一個登入狀態需要同步,在原先我們的做法需要主動去關注狀態與渲染之間的關係:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class A extends Component { state = { isLogin: getLoginState() } componenetDidMount() { LoginManager.on('status', (status) => { this.setState({isLogin: status})}) } render() { return {this.state.isLogin ? <span>A</span> : null } } } class B extends Component { state = { isLogin: getLoginState() } componenetDidMount() { LoginManager.on('status', (status) => { this.setState({isLogin: status})}) } render() { return {this.state.isLogin ? <span>B</span> : null } } } |
可以明顯的察覺到兩個頁面為了做登入狀態同步的事情,感覺 80% 的程式碼都是冗餘重複的,如果使用 Hooks 就是完全不同的情形:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function useLogin(){ const [isLogin, setLogin] = useState(getLoginState()); LoginManager.on('status', (status) => { setLogin(status)}); return isLogin; } function A() { const isLogin = useLogin(); return {isLogin ? <span>A</span> : null } } function B() { const isLogin = useLogin(); return {isLogin ? <span>B</span> : null } } |
細心的同學可能會發現 Function Component 在 re-render 時除了純粹的 render 程式碼之外 useState 也是重複被申明與執行了的,這在邏輯上似乎有些不合常理,為什麼下面程式碼重複被執行元件內上一次的 state 依舊還在?
1 2 |
const [name, setName] = useState('yiyi'); const [emoji, setEmoji] = useState('?'); |
這裡我們瞭解下 useState 的工作原理,看如下 useState 實現原理的示例程式碼,引擎通過程式碼上 useState 的執行順序在內部維護一個 stateIndex 來識別當前是哪一個 state,並且只在第一次 render 時的才接受 initState, re-render 的時候從內部維護 state 儲存器中獲取上一次的 state 值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let stateIndex = 0; let currentComponentInstance = null; let isComponentDidMount = false; function useState(initState) { const index = ++stateIndex; const privateStateStore = currentComponentInstance._state; if (!isComponentDidMount) { privateStateStore[index] = initState; } // 示例程式碼只考慮簡單的資料型別 const stateUpdater = (state) => privateStateStore[index] = state; const [privateStateStore[index], stateUpdater]; } |
從內部原理實現角度,這個方案並不優雅,解決了問題但坑也比較大,比方說:useState 的執行順序要在每次 render 時必須保持一致,否則 stateIndex 的順序就會錯亂,對於不熟悉個約定的新手來說是一個噩夢,這個問題一旦發生非常難除錯。有人提議藉助 Lint 來規避這個問題,這是典型的填補一個坑通過挖另一個坑來解決。
關於生命週期,使用 useEffect 基本解決了在 Fuction Component 無生命週期的問題,但這也是有代價的,顯然 useEffect 在語義上抽象的不確定的,最糟糕的是 useEffect 約定了 return 的函式執行時機等價與 componentWillUnmount 執行時機,表達上比較晦澀,給程式碼的可讀性上帶來了些許的不愉快。要清楚 useEffect 並沒有避免生命週期的概念,只是用一種方式隱藏了他們,這種隱藏方式我理解是基於 Fuction Component 的一種無奈。
此外 Function Component 還有一個特點是外部對元件的操作只能通過 props 進行控制,所以元件暴露方法來控制元件內部狀態的方式不存在了,理想上能統一使用 Function Component 在架構上這一個益處,外部介面暴露更一致了,但只是理想。
結尾,複雜應用盡可能使用 Function Component + Hooks 是值得推薦的,這是更美好的明天。