因爲工做的緣由我已經很長時間沒接觸過React了。前段時間圈子裏都在討論React Hooks,出於好奇也學習了一番,特此整理以加深理解。
在web應用無所不能的9012年,組成應用的Components也愈來愈複雜,冗長而難以複用的代碼給開發者們形成了不少麻煩。好比:javascript
在這種背景下,React在16.8.0引入了React Hooks。html
主要介紹state hook,effect hook及custom hookjava
最基本的應用以下:react
import React, { useState } from 'react' function counter() { const [count, setCount] = useState(0) return ( <div> <p>You have clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click </button> </div> ) }
調用useState,傳入初始值,經過數組的結構賦值獲得獨立的local state count,及setCount。count能夠理解爲class component中的state,可見這裏的state不侷限於對象,能夠爲number,string,固然也能夠是一個對象。而setCount能夠理解爲class component中的setState,不一樣的是setState會merge新老state,而hook中的set函數會直接替換,這就意味着若是state是對象時,每次set應該傳入全部屬性,而不能像class component那樣僅傳入變化的值。因此在使用useState時,儘可能將相關聯的,會共同變化的值放入一個object。web
再看看有多個「local state」的狀況:數組
import React, { useState } from 'react' function person() { const [name, setName] = useState('simon') const [age, setAge] = useState(24) return ( <div> <p>name: {name}</p> <p>age: {age}</p> </div> ) }
咱們知道當函數執行完畢,函數做用域內的變量都會銷燬,hooks中的state在component首次render後被React保留下來了。那麼在下一次render時,React如何將這些保留的state與component中的local state對應起來呢。這裏給出一個簡單版本的實現:異步
const stateArr = [] const setterArr = [] let cursor = 0 let isFirstRender = true function createStateSetter(cursor) { return state => { stateArr[cursor] = state } } function useState(initState) { if (isFirstRender) { stateArr.push(initState) setterArr.push(createStateSetter(cursor)) isFirstRender = false } const state = stateArr[cursor] const setter = setterArr[cursor] cursor++ return [state, setter] }
能夠看出React須要保證多個hooks在component每次render的時候的執行順序都保持一致,不然就會出現錯誤。這也是React hooks rule中必須在top level使用hooks的由來——條件,遍歷等語句都有可能會改變hooks執行的順序。ide
import React, { useState, useEffect } from 'react' function FriendStatus(props) { const [isOnline, setIsOnline] = useState(null) function handleStatusChange(status) { setIsOnline(status.isOnline) } // 基本寫法 useEffect(() => { document.title = 'Dom is ready' }) // 須要取消操做的寫法 useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange) return function cleanup() { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange) } }) if (isOnline === null) { return 'Loading...' } return isOnline ? 'Online' : 'Offline' }
能夠看到上面的代碼在傳入useEffect的函數(effect)中作了一些"side effect",在class component中咱們一般會在componentDidMount,componentDidUpdate中去作這些事情。另外在class component中,須要在componentDidMount中訂閱,在componentWillUnmount中取消訂閱,這樣將一件事拆成兩件事作,不只可讀性低,還容易產生bug:函數
class FriendStatus extends React.Component { constructor(props) { super(props); this.state = { isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); } render() { if (this.state.isOnline === null) { return 'Loading...'; } return this.state.isOnline ? 'Online' : 'Offline'; } }
如上代碼,若是props中的friend.id發生變化,則會致使訂閱和取消的id不一致,如需解決須要在componentDidUpdate中先取消訂閱舊的再訂閱新的,代碼很是冗餘。而useEffect hook在這一點上是渾然天成的。另外effect函數在每次render時都是新建立的,這實際上是有意而爲之,由於這樣才能取得最新的state值。工具
有同窗可能會想,每次render後都會執行effect,這樣會不會對性能形成影響。其實effect是在頁面渲染完成以後執行的,不會阻塞,而在effect中執行的操做每每不要求同步完成,除了少數如要獲取寬度或高度,這種狀況須要使用其餘的hook(useLayoutEffect),此處不作詳解。即便這樣,React也提供了控制的方法,及useEffect的第二個參數————一個數組,若是數組中的值不發生變化的話就跳過effect的執行:
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange) return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); } }, [props.friend.id])
A custom Hook is a JavaScript function whose name starts with 」use」 and that may call other Hooks.
Custom Hook的使命是解決stateful logic複用的問題,如上面例子中的FriendStatus,在一個聊天應用中可能多個組件都須要知道好友的在線狀態,將FriendStatus抽象成這樣的hook:
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; } function FriendStatus(props) { const isOnline = useFriendStatus(props.friend.id) if (isOnline === null) { return 'Loading...' } return isOnline ? 'Online' : 'Offline' } function FriendListItem(props) { const isOnline = useFriendStatus(props.friend.id) return ( <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li> ) }
FriendStatus和FriendListItem中的isOnline是獨立的,因custom hook複用的是stateful logic,而不是state自己。另外custom hook必須以use開頭來命名,這樣linter工具才能正確檢測其是否符合規範。
除了以上三種hook,React還提供了useContext, useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect, useDebugValue內置hook,它們的用途能夠參考官方文檔,這裏我想單獨講講useRef。
顧名思義,這個hook應該跟ref相關的:
function TextInputWithFocusButton() { const inputEl = useRef(null) const onButtonClick = () => { inputEl.current.focus() } return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ) }
來看看官方文檔上的說明:
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
這句話告訴咱們在組件的整個生命週期裏,inputEl.current都是存在的,這擴展了useRef自己的用途,可使用useRef維護相似於class component中實例屬性的變量:
function Timer() { const intervalRef = useRef() useEffect(() => { const id = setInterval(() => { // ... }) intervalRef.current = id return () => { clearInterval(intervalRef.current) } }) // ... }
這在class component中是理所固然的,但不要忘記Timer僅僅是一個函數,函數執行完畢後函數做用域內的變量將會銷燬,因此這裏須要使用useRef來保持這個timerId。相似的useRef還能夠用來獲取preState:
function Counter() { const [count, setCount] = useState(0) const prevCountRef = useRef() useEffect(() => { prevCountRef.current = count // 因爲useEffect中的函數是在render完成以後異步執行的,因此在每次render時prevCountRef.current的值爲上一次的count值 }) const prevCount = prevCountRef.current return <h1>Now: {count}, before: {prevCount}</h1> }