React的Hook規則其實很簡單的,只要保證use打頭即可。但是如何理解它的掛載,閉包行爲, 異步帶來的問題,以及useRef容易濫用,手動管理依賴等等,還真的是有點費力,一般人真架不住它的水這麼深!
舉個例子,你很高興的以爲使用useCallback,或useMemo緩存了某個值或函數,然後傳遞給子組件的props中去,就以爲避免了一部分子組件的更新,但可能子組件還需要用 memo(子組件) 這麼包裹一下。於是你就memo(子組件)一下,那麼現在子組件一定會少了更新嗎? 文檔上又寫了
一句,見下圖,這不讓人爲難了嗎? 到底避免不避免子組件更新,還是依賴代碼測試的實際結果,否則你任何優化可能只是一廂情願,甚至有反作用!
繼續,當你的useCallback 中,有一個異步請求,請求5秒後完成,再setXXX, 或dispatch更新組件,那麼這5秒的時間中,組件可能已經reRender多次了,根據react的函數性,useCallback的函數操作的state, 已經不是當前頁面的state了!我剛剛代碼測試了!
我說了2遍代碼測試,是的,即使我這麼自信的老鳥,僅通過文檔無法確切Hook的具體行爲的,在用Hook時,顫顫巍巍,處處 log,如履薄冰。一方面怕被人說寫法不地道,爲什麼不優化,一方面又怕自己玩着花樣寫,自己給自己埋雷,無法控制它的行爲!
雖說如此,我還是寫了一些Hook的,簡單記錄一下這些Hook:
一、生命週期Hook
雖然useEffect萬能的可以模擬很多生命週期,但我無法忍受代碼中有太多的useEffect,我還必須找它的參數,才能知道它的行爲,這不很怪異嗎?我參考網上代碼,實現了create,mount,update,unMount的生命週期鉤子,以及相關概念的驗證,不多解釋了,直接看代碼上的註釋吧!
尤其注意的時,mount事件和第一次update事件,由於都是使用useEffect來模擬的,所以它們出現的時機依賴於你在組件內調用的順序!父子組件的生命週期的執行順序是最有意義的事情,希望每個人都要深刻理解。
import { useEffect, useRef } from "react";
// useRef, 保證created早於 mounted 因爲mounted使用useEffect,所以它是在update之後的事件
export const useCreated = (fn) => {
const init = useRef(true)
init.current && (fn() || (init.current = false))
}
export const useMount = (fn) => useEffect(fn, [])
export const useUnMount = (fn) => useEffect(() => fn, [])
export const useUpdate = (fn) => useEffect(fn)
/** 用下面兩個組件,測試父子組件的生命週期.
* 父組件<Todo>
* 子組件<Foo>
*
* 加載時:父--子--父。
* todo created
foo created
foo mounted
foo Update 由於 使用useEffect,這是加載即第一次
todo mounted
todo Update 由於 使用useEffect,這是加載即第一次
*
更新時:先子後父
* foo Update
todo Update
卸載時: 先父後子
todo UnMount
foo UnMount
*/
二、useMediaQuery
由於最近的任務是自適應頁面,看到了其它庫有這個函數,比如ahook, @chakra-ui 。ahook庫中,名字叫useResponsive,底層使用window.resize來實現的,滑天下之大稽。 公司項目是使用@chakra-ui框架,它的useMeidaQuery用的是標準window.matchMedia,但它的使用需要手寫media query表達式,所以我寫了一個更簡單的Hook, 直接傳入指定的斷點,返回相應的變量狀態即可!
// 第一個參數是斷點, 3個斷點則有4個區間
// 第二個參數是:onChange函數。 每次區間跳動時,觸發一次
// 組件內 使用方法如下:
let matches = useMediaQuery([500, 1000, 1500], () => {
console.log("change media query:", matches)
})
// JSX綁定值:
<div>500以內: {matches[0]?"是":"否"}</div>
<div>1000以內: {matches[1]?"是":"否"}</div>
<div>1500以內: {matches[2]?"是":"否"}</div>
<div>1500以上: {matches[3]?"是":"否"}</div>
效果是:
useMediaQuery 的源碼如下:
/**
* 輸入一組有序斷點,返回一組狀態值
* @example 3個斷點 生成4個區域
* let matches = useMediaQuery([500, 1000, 1500], () => {
console.log("change media query:", matches)
})
* @param {Array<Number>} breakpoints 數字數組, 由小到大。
* @param {Function} onChange onChange回調。
*/
export default function useMediaQuery(breakpoints, onChange) {
let [matches, setMatches] = useState([])
useEffect(() => {
// 生成 query 表達式
let start = 1, querys = []
breakpoints.forEach(bp => {
querys.push(`(min-width:${start}px) and (max-width:${bp-1}px)`);
start = bp;
})
querys.push(`(min-width:${start}px)`)
let mqlList = querys.map(q => window.matchMedia(q));
//添加所有監聽, 通過Idx追蹤位置
mqlList.forEach((mql, idx) => {
matches[idx] = mql.matches
mql.addEventListener("change", mql.fn = function (ev) {
matches[idx] = ev.matches
if (ev.matches) {
setTimeout(() => {
setMatches([...matches])
onChange()
}, 0);
}
})
})
setMatches([...matches]) //更新一下
return () => {
mqlList.map(mql => mql.removeEventListener("change", mql.fn))
mqlList = null
}
}, [])
return matches
}
最後,在使用useMediaQuery時,很多人是用它配合 CSS IN JS 這樣的框架,去定義不同寬度時的樣式的。 個人以爲這是不合理的用法,因爲每次窗口變化,引起狀態變化,必然帶來一次reRender,帶來運行時的消耗。如果只是控制樣式,還是要直接寫到css 文件中去,纔是性能最高的,瀏覽器一次加載css,執行css的行爲。 只有當需求是:不同寬度時,應用的邏輯不一樣了,比如大窗口彈窗,小窗口toast一下, 這時纔有必要使用useMediaQuery 這樣的 Hook。 總之不要以爲用上Hook就比用CSS牛逼這樣的想法。
在編寫動畫上也是這樣,能寫css動畫,理論上就要比js動畫性能更好,其道理大致一樣!