手寫幾個自定義React Hook

        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動畫性能更好,其道理大致一樣!

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章