因爲工做的緣由我已經很長時間沒接觸過React了。前段時間圈子裏都在討論React Hooks,出於好奇也學習了一番,特此整理以加深理解。javascript
在web應用無所不能的9012年,組成應用的Components也愈來愈複雜,冗長而難以複用的代碼給開發者們形成了不少麻煩。好比: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)
// 基本寫法
useEffect(() => {
document.title = 'Dom is ready'
})
// 須要取消操做的寫法
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline)
}
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);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
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>
}
複製代碼