react-hooks使用
1. 什麼是react-hooks?
** react-hooks是react16.8以後,react新增的鉤子API,目的是增加代碼的可複用性,邏輯性,彌補無狀態組件沒有生命週期,沒有數據管理狀態state的缺陷。筆者認爲,react-hooks思想和初衷,也是把組件,顆粒化,單元化,形成獨立的渲染環境,減少渲染次數,優化性能。
useCallback✅
useContext✅
useEffect✅
useLayoutEffect ✅
useMemo ✅
useReducer✅
useRef✅
useState✅
以上就是react-hooks主要的api,接下來我會和大家分享一下這些api的用法,以及使用他們的注意事項。
2.爲什麼要使用hooks
我們爲什麼要使用react-hooks呢,首先和傳統的class聲明的有狀態有這顯著的優點就是
1 react-hooks可以讓我們的代碼的邏輯性更強,可以抽離公共的方法,公共組件。
2 react-hooks思想更趨近於函數式編程。用函數聲明方式代替class聲明方式,雖說class也是es6構造函數語法糖,但是react-hooks寫起來更有函數即組件,無疑也提高代碼的開發效率(無需像class聲明組件那樣寫聲明週期,寫生命週期render函數等)
3 react-hooks可能把龐大的class組件,化整爲零成很多小組件,useMemo等方法讓組件或者變量制定一個適合自己的獨立的渲染空間,一定程度上可以提高性能,減少渲染次數。這裏值得一提的是,如果把負責 請求是數據 ➡️ 視圖更新的渲染組件,用react-hooks編寫的話 ,配合immutable等優秀的開源庫,會有更棒的效果(這裏特別注意的是⚠️,如果亂用hooks,不但不會提升性能,反而會影響性能,帶來各種各樣的想不到的問題)。
3.如何使用hooks
接下來和大家探討一下,react-hooks主要api,具體使用
1 useState 數據存儲,派發更新
useState出現,使得react無狀態組件能夠像有狀態組件一樣,可以擁有自己state,useState的參數可以是一個具體的值,也可以是一個函數用於判斷複雜的邏輯,函數返回作爲初始值,usestate 返回一個數組,數組第一項用於讀取此時的state值 ,第二項爲派發數據更新,組件渲染的函數,函數的參數即是需要更新的值。useState和useReduce 作爲能夠觸發組件重新渲染的hooks,我們在使用useState的時候要特別注意的是,useState派發更新函數的執行,就會讓整個function組件從頭到尾執行一次,所以需要配合useMemo,usecallback等api配合使用,這就是我說的爲什麼濫用hooks會帶來負作用的原因之一了。一下代碼爲usestate基本應用
const DemoState = (props) => {
/* number爲此時state讀取值 ,setNumber爲派發更新的函數 */
let [number, setNumber] = useState(0) /* 0爲初始值 */
return (<div>
<span>{ number }</span>
<button onClick={ ()=> {
setNumber(number+1)
console.log(number) /* 這裏的number是不能夠即使改變的 */
} } ></button>
</div>)
}
上邊簡單的例子說明了useState ,但是當我們在調用更新函數之後,state的值是不能即時改變的,只有當下一次上下文執行的時候,state值才隨之改變。
const a =1
const DemoState = (props) => {
/* useState 第一個參數如果是函數 則處理複雜的邏輯 ,返回值爲初始值 */
let [number, setNumber] = useState(()=>{
// number
return a===1 ? 1 : 2
}) /* 1爲初始值 */
return (<div>
<span>{ number }</span>
<button onClick={ ()=>setNumber(number+1) } ></button>
</div>)
}
2 useEffect 組件更新副作用鉤子
如果你想在function組件中,當組件完成掛載,dom渲染完成,做一些操縱dom,請求數據,那麼useEffect是一個不二選擇,如果我們需要在組件初次渲染的時候請求數據,那麼useEffect可以充當class組件中的 componentDidMount , 但是特別注意的是,如果不給useEffect執行加入限定條件,函數組件每一次更新都會觸發effect ,那麼也就說明每一次state更新,或是props的更新都會觸發useEffect執行,此時的effect又充當了componentDidUpdate和componentwillreceiveprops,所以說合理的用於useEffect就要給effect加入限定執行的條件,也就是useEffect的第二個參數,這裏說是限定條件,也可以說是上一次useeffect更新收集的某些記錄數據變化的記憶,在新的一輪更新,useeffect會拿出之前的記憶值和當前值做對比,如果發生了變化就執行新的一輪useEffect的副作用函數,useEffect第二個參數是一個數組,用來收集多個限制條件 。
/* 模擬數據交互 */
function getUserInfo(a){
return new Promise((resolve)=>{
setTimeout(()=>{
resolve({
name:a,
age:16,
})
},500)
})
}
const Demo = ({ a }) => {
const [ userMessage , setUserMessage ] :any= useState({})
const div= useRef()
const [number, setNumber] = useState(0)
/* 模擬事件監聽處理函數 */
const handleResize =()=>{}
/* useEffect使用 ,這裏如果不加限制 ,會是函數重複執行,陷入死循環*/
useEffect(()=>{
/* 請求數據 */
getUserInfo(a).then(res=>{
setUserMessage(res)
})
/* 操作dom */
console.log(div.current) /* div */
/* 事件監聽等 */
window.addEventListener('resize', handleResize)
/* 只有當props->a和state->number改變的時候 ,useEffect副作用函數重新執行 ,如果此時數組爲空[],證明函數只有在初始化的時候執行一次相當於componentDidMount */
},[ a ,number ])
return (<div ref={div} >
<span>{ userMessage.name }</span>
<span>{ userMessage.age }</span>
<div onClick={ ()=> setNumber(1) } >{ number }</div>
</div>)
}
如果我們需要在組件銷燬的階段,做一些取消dom監聽,清除定時器等操作,那麼我們可以在useEffect函數第一個參數,結尾返回一個函數,用於清除這些副作用。相當與componentWillUnmount。
const Demo = ({ a }) => {
/* 模擬事件監聽處理函數 */
const handleResize =()=>{}
useEffect(()=>{
/* 定時器 延時器等 */
const timer = setInterval(()=>console.log(666),1000)
/* 事件監聽 */
window.addEventListener('resize', handleResize)
/* 此函數用於清除副作用 */
return function(){
clearInterval(timer)
window.removeEventListener('resize', handleResize)
}
},[ a ])
return (<div >
</div>)
}
異步 async effect ?
提醒大家的是 useEffect是不能直接用 async await 語法糖的
/* 錯誤用法 ,effect不支持直接 async await 裝飾的 */
useEffect(async ()=>{
/* 請求數據 */
const res = await getUserInfo(payload)
},[ a ,number ])
如果我們想要用 async effect 可以對effect進行一層包裝
const asyncEffect = (callback, deps)=>{
useEffect(()=>{
callback()
},deps)
}
3useLayoutEffect 渲染更新之前的 useEffect
useEffect 執行順序 組件更新掛載完成 -> 瀏覽器dom 繪製完成 -> 執行useEffect回調 。
useLayoutEffect 執行順序 組件更新掛載完成 -> 執行useLayoutEffect回調-> 瀏覽器dom 繪製完成
所以說useLayoutEffect 代碼可能會阻塞瀏覽器的繪製 如果我們在useEffect 重新請求數據,渲染視圖過程中,肯定會造成畫面閃動的效果,而如果用useLayoutEffect ,回調函數的代碼就會阻塞瀏覽器繪製,所以可定會引起畫面卡頓等效果,那麼具體要用 useLayoutEffect 還是 useEffect ,要看實際項目的情況,大部分的情況 useEffect 都可以滿足的。
const DemoUseLayoutEffect = () => {
const target = useRef()
useLayoutEffect(() => {
/*我們需要在dom繪製之前,移動dom到制定位置*/
const { x ,y } = getPositon() /* 獲取要移動的 x,y座標 */
animate(target.current,{ x,y })
}, []);
return (
<div >
<span ref={ target } className="animate"></span>
</div>
)
}
4 useRef 獲取元素 ,緩存數據。
和傳統的class組件ref一樣,react-hooks 也提供獲取元素方法 useRef,它有一個參數可以作爲緩存數據的初始值,返回值可以被dom元素ref標記,可以獲取被標記的元素節點.
const DemoUseRef = ()=>{
const dom= useRef(null)
const handerSubmit = ()=>{
/* <div >表單組件</div> dom 節點 */
console.log(dom.current)
}
return <div>
{/* ref 標記當前dom節點 */}
<div ref={dom} >表單組件</div>
<button onClick={()=>handerSubmit()} >提交</button>
</div>
}
高階用法 緩存數據
當然useRef還有一個很重要的作用就是緩存數據,我們知道usestate ,useReducer 是可以保存當前的數據源的,但是如果它們更新數據源的函數執行必定會帶來整個組件從新執行到渲染,如果在函數組件內部聲明變量,則下一次更新也會重置,如果我們想要悄悄的保存數據,而又不想觸發函數的更新,那麼useRef是一個很棒的選擇。
** const currenRef = useRef(InitialData)
獲取 currenRef.current
改變 currenRef.current = newValue
useRef可以第一個參數可以用來初始化保存數據,這些數據可以在current屬性上獲取到 ,當然我們也可以通過對current賦值新的數據源。
下面我們通過react-redux源碼來看看useRef的巧妙運用
(react-redux 在react-hooks發佈後,用react-hooks重新了其中的Provide,connectAdvanced)核心模塊,可以見得 react-hooks在限制數據更新,高階組件上有這一定的優勢,其源碼大量運用useMemo來做數據判定
/* 這裏用到的useRef沒有一個是綁定在dom元素上的,都是做數據緩存用的 */
/* react-redux 用userRef 來緩存 merge之後的 props */
const lastChildProps = useRef()
// lastWrapperProps 用 useRef 來存放組件真正的 props信息
const lastWrapperProps = useRef(wrapperProps)
//是否儲存props是否處於正在更新狀態
const renderIsScheduled = useRef(false)
這是react-redux中用useRef 對數據做的緩存,那麼怎麼做更新的呢 ,我們接下來看
//獲取包裝的props
function captureWrapperProps(
lastWrapperProps,
lastChildProps,
renderIsScheduled,
wrapperProps,
actualChildProps,
childPropsFromStoreUpdate,
notifyNestedSubs
) {
//我們要捕獲包裝props和子props,以便稍後進行比較
lastWrapperProps.current = wrapperProps //子props
lastChildProps.current = actualChildProps //經過 merge props 之後形成的 prop
renderIsScheduled.current = false
}
通過上面我們可以看到 ,react-redux 用重新賦值的方法,改變緩存的數據源,避免不必要的數據更新,
如果選用useState儲存數據,必然促使組件重新渲染 所以採用了useRef解決了這個問題,至於react-redux源碼怎麼實現的,我們這裏暫且不考慮。
5 useContext 自由獲取context
我們可以使用useContext ,來獲取父級組件傳遞過來的context值,這個當前值就是最近的父級組件 Provider 設置的value值,useContext參數一般是由 createContext 方式引入 ,也可以父級上下文context傳遞 ( 參數爲context )。useContext 可以代替 context.Consumer 來獲取Provider中保存的value值
/* 用useContext方式 */
const DemoContext = ()=> {
const value:any = useContext(Context)
/* my name is alien */
return <div> my name is { value.name }</div>
}
/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
return <Context.Consumer>
{/* my name is alien */}
{ (value)=> <div> my name is { value.name }</div> }
</Context.Consumer>
}
export default ()=>{
return <div>
<Context.Provider value={{ name:'alien' , age:18 }} >
<DemoContext />
<DemoContext1 />
</Context.Provider>
</div>
}
6 useReducer 無狀態組件中的redux
useReducer 是react-hooks提供的能夠在無狀態組件中運行的類似redux的功能api,至於它到底能不能代替redux react-redux ,我個人的看法是不能的 ,redux 能夠複雜的邏輯中展現優勢 ,而且 redux的中間件模式思想也是非常優秀了,我們可以通過中間件的方式來增強dispatch redux-thunk redux-sage redux-action redux-promise都是比較不錯的中間件,可以把同步reducer編程異步的reducer。useReducer 接受的第一個參數是一個函數,我們可以認爲它就是一個reducer ,reducer的參數就是常規reducer裏面的state和action,返回改變後的state, useReducer第二個參數爲state的初始值 返回一個數組,數組的第一項就是更新之後state的值 ,第二個參數是派發更新的dispatch函數 。dispatch 的觸發會觸發組件的更新,這裏能夠促使組件從新的渲染的一個是useState派發更新函數,另一個就 useReducer中的dispatch
const DemoUseReducer = ()=>{
/* number爲更新後的state值, dispatchNumbner 爲當前的派發函數 */
const [ number , dispatchNumbner ] = useReducer((state,action)=>{
const { payload , name } = action
/* return的值爲新的state */
switch(name){
case 'add':
return state + 1
case 'sub':
return state - 1
case 'reset':
return payload
}
return state
},0)
return <div>
當前值:{ number }
{ /* 派發更新 */ }
<button onClick={()=>dispatchNumbner({ name:'add' })} >增加</button>
<button onClick={()=>dispatchNumbner({ name:'sub' })} >減少</button>
<button onClick={()=>dispatchNumbner({ name:'reset' ,payload:666 })} >賦值</button>
{ /* 把dispatch 和 state 傳遞給子組件 */ }
<MyChildren dispatch={ dispatchNumbner } State={{ number }} />
</div>
}
當然實際業務邏輯可能更復雜的,需要我們在reducer裏面做更復雜的邏輯操作。
7 useMemo 小而香性能優化
useMemo我認爲是React設計最爲精妙的hooks之一,優點就是能形成獨立的渲染空間,能夠使組件,變量按照約定好規則更新。渲染條件依賴於第二個參數deps。 我們知道無狀態組件的更新是從頭到尾的更新,如果你想要從新渲染一部分視圖,而不是整個組件,那麼用useMemo是最佳方案,避免了不需要的更新,和不必要的上下文的執行,在介紹useMemo之前,我們先來說一說, memo, 我們知道class聲明的組件可以用componentShouldUpdate來限制更新次數,那麼memo就是無狀態組件的ShouldUpdate , 而我們今天要講的useMemo就是更爲細小的ShouldUpdate單元,
先來看看memo ,memo的作用結合了pureComponent純組件和 componentShouldUpdate功能,會對傳進來的props進行一次對比,然後根據第二個函數返回值來進一步判斷哪些props需要更新。
/* memo包裹的組件,就給該組件加了限制更新的條件,是否更新取決於memo第二個參數返回的boolean值, */
const DemoMemo = connect(state =>
({ goodList: state.goodList })
)(memo(({ goodList, dispatch, }) => {
useEffect(() => {
dispatch({
name: 'goodList',
})
}, [])
return <Select placeholder={'請選擇'} style={{ width: 200, marginRight: 10 }} onChange={(value) => setSeivceId(value)} >
{
goodList.map((item, index) => <Option key={index + 'asd' + item.itemId} value={item.itemId} > {item.itemName} </Option>)
}
</Select>
/* 判斷之前的goodList 和新的goodList 是否相等,如果相等,
則不更新此組件 這樣就可以制定屬於自己的渲染約定 ,讓組件只有滿足預定的下才重新渲染 */
}, (pre, next) => is(pre.goodList, next.goodList)))
useMemo的應用理念和memo差不多,都是判定是否滿足當前的限定條件來決定是否執行useMemo的callback函數,而useMemo的第二個參數是一個deps數組,數組裏的參數變化決定了useMemo是否更新回調函數,useMemo返回值就是經過判定更新的結果。它可以應用在元素上,應用在組件上,也可以應用在上下文當中。如果又一個循環的list元素,那麼useMemo會是一個不二選擇,接下來我們一起探尋一下useMemo的優點
/* 用 useMemo包裹的list可以限定當且僅當list改變的時候才更新此list,這樣就可以避免selectList重新循環 */
{useMemo(() => (
<div>{
selectList.map((i, v) => (
<span
className={style.listSpan}
key={v} >
{i.patentName}
</span>
))}
</div>
), [selectList])}
1 useMemo可以減少不必要的循環,減少不必要的渲染
useMemo(() => (
<Modal
width={'70%'}
visible={listshow}
footer={[
<Button key="back" >取消</Button>,
<Button
key="submit"
type="primary"
>
確定
</Button>
]}
>
{ /* 減少了PatentTable組件的渲染 */ }
<PatentTable
getList={getList}
selectList={selectList}
cacheSelectList={cacheSelectList}
setCacheSelectList={setCacheSelectList} />
</Modal>
), [listshow, cacheSelectList])
2 useMemo可以減少子組件的渲染次數
const DemoUseMemo=()=>{
/* 用useMemo 包裹之後的log函數可以避免了每次組件更新再重新聲明 ,可以限制上下文的執行 */
const newLog = useMemo(()=>{
const log =()=>{
console.log(6666)
}
return log
},[])
return <div onClick={()=>newLog()} ></div>
}
3 useMemo讓函數在某個依賴項改變的時候才運行,這可以避免很多不必要的開銷(這裏要注意⚠️⚠️⚠️的是如果被useMemo包裹起來的上下文,形成一個獨立的閉包,會緩存之前的state值,如果沒有加相關的更新條件,是獲取不到更新之後的state的值的,如下邊👇⬇️)
const DemoUseMemo=()=>{
const [ number ,setNumber ] = useState(0)
const newLog = useMemo(()=>{
const log =()=>{
/* 點擊span之後 打印出來的number 不是實時更新的number值 */
console.log(number)
}
return log
/* [] 沒有 number */
},[])
return <div>
<div onClick={()=>newLog()} >打印</div>
<span onClick={ ()=> setNumber( number + 1 ) } >增加</span>
</div>
}
useMemo很不錯,react-redux 用react-hooks重寫後運用了大量的useMemo情景,我爲大家分析兩處
useMemo 同過 store didStoreComeFromProps contextValue 屬性制定是否需要重置更新訂閱者subscription ,這裏我就不爲大家講解react-redux了,有興趣的同學可以看看react-redux源碼,看看是怎麼用useMemo的
const [subscription, notifyNestedSubs] = useMemo(() => {
if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
const subscription = new Subscription(
store,
didStoreComeFromProps ? null : contextValue.subscription // old
)
const notifyNestedSubs = subscription.notifyNestedSubs.bind(
subscription
)
return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])
react-redux通過 判斷 redux store的改變來獲取與之對應的state
const previousState = useMemo(() => store.getState(), [store])
講到這裏,如果我們應用useMemo根據依賴項合理的顆粒化我們的組件,能起到很棒的優化組件的作用。
8 useCallback useMemo版本的回調函數
useMemo和useCallback接收的參數都是一樣,都是在其依賴項發生變化後才執行,都是返回緩存的值,區別在於useMemo返回的是函數運行的結果,useCallback返回的是函數,這個回調函數是經過處理後的也就是說父組件傳遞一個函數給子組件的時候,由於是無狀態組件每一次都會重新生成新的props函數,這樣就使得每一次傳遞給子組件的函數都發生了變化,這時候就會觸發子組件的更新,這些更新是沒有必要的,此時我們就可以通過usecallback來處理此函數,然後作爲props傳遞給子組件
/* 用react.memo */
const DemoChildren = React.memo((props)=>{
/* 只有初始化的時候打印了 子組件更新 */
console.log('子組件更新')
useEffect(()=>{
props.getInfo('子組件')
},[])
return <div>子組件</div>
})
const DemoUseCallback=({ id })=>{
const [number, setNumber] = useState(1)
/* 此時usecallback的第一參數 (sonName)=>{ console.log(sonName) }
經過處理賦值給 getInfo */
const getInfo = useCallback((sonName)=>{
console.log(sonName)
},[id])
return <div>
{/* 點擊按鈕觸發父組件更新 ,但是子組件沒有更新 */}
<button onClick={ ()=>setNumber(number+1) } >增加</button>
<DemoChildren getInfo={getInfo} />
</div>
}
這裏應該提醒的是,useCallback ,必須配合 react.memo pureComponent ,否則不但不會提升性能,還有可能降低性能
4總結
react-hooks的誕生,也不是說它能夠完全代替class聲明的組件,對於業務比較複雜的組件,class組件還是首選,只不過我們可以把class組件內部拆解成funciton組件,根據業務需求,哪些負責邏輯交互,哪些需要動態渲染,然後配合usememo等api,讓性能提升起來。react-hooks使用也有一些限制條件,比如說不能放在流程控制語句中,執行上下文也有一定的要求。總體來說,react-hooks還是很不錯的,值得大家去學習和探索。