Hooks是 React v16.8 的新特性,可以在不使用類組件的情況下,使用 state 以及其他的React特性;
-
Hooks是完全可選的,無需重寫任何已有代碼就可以在一些組件中嘗試Hook - 于
React v16.8發(fā)布,100%向后兼容,Hooks不包含任何破壞性改動. - React也沒有計劃移除
class類組件,而且Hooks不會影響對React的理解,它為已知的React概念提供了更直接的API
Hooks解決的問題
1. 函數(shù)式組件不能使用state:函數(shù)式組件比類組件更簡潔好用,而Hooks讓它更加豐富強大;
2. 副作用問題:諸如數(shù)據(jù)獲取、訂閱、定時執(zhí)行任務、手動修改ReactDOM這些行為都可以稱為副作用;而Hooks的出現(xiàn)可以使用 useEffect 來處理這些副作用;
3. 有狀態(tài)的邏輯重用組件
4. 復雜的狀態(tài)管理:之前通常使用 redux、dva、mobx 這些第三方狀態(tài)管理器來管理復雜的狀態(tài),而Hooks可以使用 useReducer、useContext 配合實現(xiàn)復雜的狀態(tài)管理;
5. 開發(fā)效率和質量問題:函數(shù)式組件比類組件簡潔,效率高,性能也好。
useState
useState:組件狀態(tài)管理的鉤子
import { useState } from 'react'
const [state, setState] = useState(initState)
-
state:管理組件的狀態(tài); -
setState:更新state的方法,方法名不可更改! -
initState:初始的state,可以是任意的數(shù)據(jù)類型(只在初次渲染時有效,二次渲染時會被忽略),也可以是回調函數(shù),但函數(shù)必須有返回值。
- 函數(shù)式組件實現(xiàn)計數(shù)器
import { useState } from 'react' export default function App() { const [count, setCount] = useState(0) // 初始值0 return ( <div> <div>點擊了{ count }次</div> <button onClick={()=>setCount(count+1)}>點擊</button> </div> ) } -
useState讓函數(shù)式組件具備了管理狀態(tài)的能力,不再是一個無狀態(tài)組件;與class的setState類似,當向useState更新狀態(tài)的方法setCount傳遞一個函數(shù)時,此函數(shù)會接收到上一次的狀態(tài):setCount(prevCount => prevCount + 1)
但與class的setState不同的是,如果狀態(tài)值是一個對象,useState更新狀態(tài)的方法是不會合并更新對象的,可以結合展開運算符來達到合并更新對象的效果:setState(prevState => { return { ...prevState, ...updateValues }; }); -
useState當然可以多次調用,從而定義多個狀態(tài)值;但不管調用多少次,相互之間都是獨立的,不會相互污染。這就是為什么React否掉了Mixins,因為Mixins機制讓多個Mixins共享一個對象的數(shù)據(jù)空間,這樣就很難保證不同Mixins依賴的狀態(tài)不發(fā)生沖突;function App() { const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]); } - React又是如何保證多個
useState的相互獨立呢,定義時并沒有告訴React這些值的key,React如何保證這三個useState能準確找到它對應的state呢--->根據(jù)useState出現(xiàn)的順序!
加入條件控制語句第一次渲染 useState(42); //將age初始化為42 useState('banana'); //將fruit初始化為banana useState([{ text: 'Learn Hooks' }]); //... 第二次渲染 useState(42); //讀取狀態(tài)變量age的值(這時候傳的參數(shù)42直接被忽略) useState('banana'); //讀取狀態(tài)變量fruit的值(這時候傳的參數(shù)banana直接被忽略) useState([{ text: 'Learn Hooks' }]); //...
這樣一來,首次渲染的初始化過程是不變的,但第二次渲染就有所不同了let showFruit = true; function App() { const [age, setAge] = useState(42); if(showFruit) { const [fruit, setFruit] = useState('banana'); showFruit = false; } const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]); }
鑒于此,React規(guī)定:第二次渲染 useState(42); //讀取狀態(tài)變量age的值(這時候傳的參數(shù)42直接被忽略) // useState('banana'); useState([{ text: 'Learn Hooks' }]); //讀取到的卻是狀態(tài)變量fruit的值,導致報錯-
Hooks必須寫在函數(shù)的最外層,不能在循環(huán)語句、條件判斷、子函數(shù)中調用; - 只能在
React的函數(shù)式組件、自定義Hooks中調用Hooks,不能在其他JavaScript函數(shù)中調用。
-
useEffect
useEffect(callback, array):副作用處理的鉤子;它也是componentDidMount()、componentDidUpdate()、componentWillUnmount()、這幾個生命周期方法的統(tǒng)一,一個頂三個!React 會等待瀏覽器完成畫面渲染之后才會延遲調用 useEffect,而生命周期鉤子是同步執(zhí)行的
-
callback回調函數(shù),作用是處理副作用的邏輯,可以返回一個函數(shù),用作清理副作用;
為防止內存泄漏,清除函數(shù)會在組件卸載前執(zhí)行;如果組件多次渲染,則在執(zhí)行下一個 effect 之前,上一個 effect 就已被清除;import { useEffect } from 'react' useEffect(() => { ......//副作用處理 return () => { ......//清理副作用的清除函數(shù) } }, []) -
array可選數(shù)組,用于控制useEffect的執(zhí)行; 省略時,每次渲染都會執(zhí)行; 空數(shù)組時,只會在組件掛載/卸載時執(zhí)行一次,類似componentDidMount、componentWillUnmount; 非空數(shù)組時,會在數(shù)組元素發(fā)生改變后執(zhí)行,且這個變化的比較是淺比較。
提供第二個參數(shù)時,相當于告訴React 該組件不依賴于state/props的變化,只依賴第二個參數(shù)的變化。
父組件也可以通過function App() { const [count, setCount] = useState(0) // 初始值0 useEffect(()=>{ console.log('1') // 初次渲染時執(zhí)行一次,之后每次 count 變化都執(zhí)行 return () => { console.log('2') // 初次渲染時不執(zhí)行,之后每次 count 變化都最先執(zhí)行 } }, [count]) return (<div> <div>點擊了{ count }次</div> <button onClick={() => setCount(preCount => preCount+1)}>點擊</button> </div>) } // 初次渲染:1 // 點擊更新count:2 1props控制子組件是否執(zhí)行useEffectuseEffect(()=>{ // 注冊事件 const subscription = props.source.subscribe(); return ()=>{ // 解綁事件 subscription.unsubscribe(); } }, [props.source]);
如前文所述,Hooks可以反復多次使用,相互獨立。所以合理的做法是,給每一個副作用加一個單獨的useEffect鉤子。這樣一來,這些副作用不再全堆在class組件的生命周期鉤子里,代碼變得更加清晰;
useEffect中定義的副作用函數(shù)在執(zhí)行時不會阻礙瀏覽器更新視圖,即這些函數(shù)是異步執(zhí)行的,而class組件中的生命周期鉤子都是同步執(zhí)行的;異步設計對大多數(shù)副作用是合理的,但也有特例,比如有時候需要先根據(jù)DOM計算出某個元素的尺寸,然后再去渲染,此時則希望二次渲染是同步發(fā)生的,也就是在瀏覽器真的去繪制界面前發(fā)生;
為此,React 為此提供了一個額外的useLayoutEffect Hook來處理這類effect。它和useEffect的結構相同,區(qū)別只是調用時機不同-- useLayoutEffect是同步的。
useContext
useContext():同一個父組件的后臺組件之間的全局數(shù)據(jù)共享;
useContext() 接收React.createContext()的返回值作為參數(shù),即context對象,并返回最近的context。當最近的context更新時,使用該context的 Hooks 將會重新渲染;
- 創(chuàng)建一個
Context文件InfoContext.js// 設置了默認值 defaultValue const InfoContext = React.createContext({ name: 'Jerry', age: 18 }) export default InfoContext - 父組件
import InfoContext from './context/InfoContext' const Person = () => { const ctx = useContext(InfoContext) return(<InfoContext.Provider value={{ username: 'superman' }}> <div> <AgeCompt></AgeCompt> </div> </InfoContext.Provider>) } export default Person - 某一層的后代組件
import { useContext } from 'react' import InfoContext from './context/InfoContext' const AgeCompt = () => { const { username } = useContext(InfoContext) return <p>{username}</p> } export default AgeCompt
useReducer
useReducer():useState的一個增強體,用于處理復雜的狀態(tài)管理,靈感來源于Redux的reducer
useState 內部就是基于 useReducer 實現(xiàn)的,只是對于簡單的狀態(tài)管理,useState()比較好用;
const [state, setState] = useState(initState)
const [state, dispatch] = useReducer(reducer, initState, initAction)
-
reducer --一個函數(shù),根據(jù)action狀態(tài)處理并更新state -
initState --初始化state -
initAction -- useReducer()初次執(zhí)行時被處理的action,會把第二個參數(shù)initState當作參數(shù)執(zhí)行 -
state --狀態(tài)值 -
dispatch --更新state的方法,接收action作為參數(shù),當它被調用時,reducer函數(shù)也會被調用,同時根據(jù)action去更新state,action是一個描述操作的對象,如dispatch({type: 'add'})import { useReducer } from 'react'; const initState = { count: 0 }; const initAction = initState => { count: initState.count + 2 }; const reducer = (state, action) => { switch(action.type) { case 'ADD': return { count: state.count+1 } case 'DEL': return { count: state.count-1 } case 'RESET': return initState default: return state } } export default function UserCompt() { const [state, dispatch] = useReducer(reducer, initState) return (<div> <p>{state.count}</p> <div> <button onClick={() => dispatch({ type: 'ADD', /**可以加一些其他屬性*/ })}>增加</button> <button onClick={() => dispatch({ type: 'DEL' })}>減少</button> <button onClick={() => dispatch({ type: 'RESET' })}>重置</button> </div> </div>) }
useRef
useRef():創(chuàng)建ref,方便訪問操作DOM
const RefCompt = () => {
//創(chuàng)建ref
const inputRef = useRef();
const getValue = () => {
//訪問ref
const inpt = inputRef.current; // input的DOM對象
inpt.focus(); // 讓 input 框獲取焦點
console.log(inpt.value); // input輸入框的值
};
//掛載
return (<div>
<input ref={ inputRef } type="text" />
<button onClick={ getValue }>獲取值</button>
</div>);
}
當然,useRef 不只是能指向 DOM
// 可變的 ref
const App = () => {
const intervalRef = useRef<number>(0);
setEffect(() => {
intervalRef.current = setInterval(() => {}, 300);
return () => clearInterval(intervalRef.current);
});
return <div></div>
}
注意:與 useState 不同的是,useRef 會實時更新數(shù)據(jù),但不會觸發(fā)組件的更新!
useMemo
useMemo(callback, array):性能優(yōu)化,利用了閉包的特性,通過記憶值來避免在每個渲染上都執(zhí)行高開銷的計算(計算緩存);適用于復雜的計算場景,如復雜的列表渲染,對象深拷貝...
-
callback -用于處理邏輯的函數(shù) -
array -依賴項,控制useMemo()重新執(zhí)行的數(shù)組,array元素改變時才會重新執(zhí)行useMemo() - 返回值是一個記憶值,也是
callback的返回值import React, { useMemo } from 'react'; export default function UserCompt() { const obj1 = { name: 'Tom', age: 15 } const obj2 = { name: 'Jerry', age: 18, sex: '男' } //合并obj1、obj2 const memoValue = useMemo(() => Object.assign(obj1, obj2), [obj1, obj2]) return (<div> <p>{ memoValue.name }</p> <p>{ memoValue.age }</p> </div>) }
注意:不能在 useMemo() 中處理副作用邏輯,而是把副作用處理邏輯放在useEffect()
useCallback
useCallback(callback, array):也是用于性能優(yōu)化,與useMemo()不同的是,返回值是callback本身;
// 合并obj1、obj2
const backValue = useCallback(() => Object.assign(obj1, obj2), [obj1, obj2])
<div>{ backValue().name } --- { backValue().age }</div>
當依賴項變化時,返回一個新的函數(shù)體callback。
自定義Hooks
-
Hooks本質上就是封裝好的勾子函數(shù),在自定義Hooks時,最需要關心的就是性能、重復渲染這些問題; - 自定義一個
Effect Hooks,把可以復用的邏輯抽離出來,變成一個個可插拔的插銷; - 當標題變化時,則修改標題、否則不執(zhí)行的
Hooksimport { useEffect } from 'react' //封裝Hooks,以 use 開頭 const useChangeTitle = (title) => { useEffect(() => { document.title = title }, [title]) } export default (props) => { useChangeTitle("自定義修改標題Hooks") return <div>測試</div> } - 一個用來判斷某個
id是否在線的Hooks
在組件中使用此import { useState, useEffect } from 'react'; function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); useEffect(()=>{ //注冊監(jiān)聽事件 ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { //取消監(jiān)聽事件 ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; }Hooksfunction FriendStatus(props) { //調用自定義Hooks const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }
Hooks的使用規(guī)則
- 只在頂層調用
Hooks-
Hooks的調用盡量只在頂層作用域 - 不要在循環(huán)、條件或嵌套函數(shù)中調用
Hook,否則可能會無法確保每次組件渲染時都以相同的順序調用Hook
-
- 只在函數(shù)組件調用
Hooks:目前只支持函數(shù)式組件,未來版本Hooks會擴展到class類組件; -
React Hooks的應用場景:函數(shù)式組件、自定義Hooks