• <noscript id="e0iig"><kbd id="e0iig"></kbd></noscript>
  • <td id="e0iig"></td>
  • <option id="e0iig"></option>
  • <noscript id="e0iig"><source id="e0iig"></source></noscript>
  • 源碼分析:react hook 最佳實踐(上篇)

    標簽: javaScript  前端  react

    源碼分析:react hook 最佳實踐(上篇)

    原文鏈接

    前言

    本文從 mini React —— Preact 源碼的角度,分析 React Hook 各個 API 的優點缺點。
    從而理解為什么要用 hook,以及如何最佳使用。

    2條規則

    為什么?

    1. 只在最頂層使用 Hook,不要在循環,條件或嵌套函數中調用 Hook;
    2. 只在 React 函數中調用 Hook,不要在普通的 JavaScript 函數中調用 Hook。

    源碼分析

    let currentIndex; // 全局索引
    let currentComponent; // 當前 hook 所在的組件
    
    function getHookState(index) {
      const hooks =
        currentComponent.__hooks ||
        (currentComponent.__hooks = {_list: [], _pendingEffects: []});
    
      if (index >= hooks._list.length) {
        hooks._list.push({});
      }
      return hooks._list[index];
    }
    
    // 獲取注冊的 hook
    const hookState = getHookState(currentIndex++);
    
    • hook 狀態都維護在數組結構中,執行 hook api 時,索引 currentIndex + 1 依次存入數組。
      當組件 render 之前,會先調用 hook render,重置索引和設置當前組件,hook 注入在 options 內。
    options._render = vnode => {
      currentComponent = vnode._component;
      currentIndex = 0;
      // ...
    };
    
    • 首先需要知道一點的是,函數組件 在每次 diff 時,整個函數都會重新執行,
      class組件 只會執行 this.render,因此 hook 在性能上會有些損耗,
      考慮到這一點 hook 為那些聲明開銷很大的數據結構和函數,提供了 useMemouseCallback 優化。

    • hook 在每次 render 時,取上一次 hook state 時,
      如果在循環,條件或嵌套函數不確定的分支里執行,就有可能取錯數據,導致混亂。

    function Todo(props) {
      const [a] = useState(1);
      if(props.flag) {
        const [b] = useState(2);
      }
      const [c] = useState(3);
      // ...
    }
    
    <Todo flag={true} />
    
    • 此時 a = 1, b = 2, c = 3;
    <Todo flag={false} />
    
    • 當條件被改變時,a = 1, c = 2c取錯了狀態!

    第二條嘛,就顯而易見了,hook 寄生于 react 組件和生命周期。

    • Preact hookoptions 對象上聲明了 _render -> diffed -> _commit -> unmount 四個鉤子,
      分別會在對象組件的生命周期前執行,這樣侵入性較小。

    在這里插入圖片描述

    useState

    使用方式

    // 聲明 hook
    const [state, setState] = useState(initialState);
    // 更新 state
    setState(newState);
    
    // 也可以函數式更新
    setState(prevState => { // 可以拿到上一次的 state 值
      // 也可以使用 Object.assign
      return {...prevState, ...updatedValues};
    });
    
    • 惰性初始 state。如果初始化 state 值開銷很大,可以傳入函數,初始化只會執行一次。
    const [state, setState] = useState(() => {
      const initialState = someExpensiveComputation(props);
      return initialState;
    });
    
    • 跳過 state 更新。設置相同的值(Object.is判斷),不會觸發組件更新。
    const [state, setState] = useState(0);
    // ...
    // 更新 state 不會觸發組件重新渲染
    setState(0);
    setState(0);
    

    為什么?

    • 坑:依賴 props.state === 1 初始化 hook,為什么 props.state === 2 時,hook state 不會變化?
    function Component(props) {
      const [state, setState] = useState(props.state);
      // ...
    }
    
    • 惰性初始的原理是什么?
    • hook state 變更是怎么驅動組件渲染的,為什么說可以當 class state 使用?

    源碼分析

    • PreactuseState 是使用 useReducer 實現的,便于行文,代碼會略加修改。
    function useState(initialState) {
      const hookState = getHookState(currentIndex++);
      if (!hookState._component) {
        hookState._component = currentComponent;
    
        hookState._value = [
          invokeOrReturn(undefined, initialState),
    
          action => {
            const nextValue = invokeOrReturn(hookState._value[0], action);
            if (hookState._value[0] !== nextValue) {
              hookState._value[0] = nextValue;
              hookState._component.setState({});
            }
          }
        ];
      }
    
      return hookState._value;
    }
    
    // 工具函數,用來支持函數式初始和更新
    function invokeOrReturn(arg, f) {
      return typeof f === 'function' ? f(arg) : f;
    }
    
    • 可以看出 useState 只會在組件首次 render 時初始化一次,以后由返回的函數來更新狀態。
    1. 坑:初始化(包括傳入的函數)只會執行一次,所有不應該依賴 props 的值來初始化 useState;
    2. 優化:可以利用傳入函數來性能優化開銷較大的初始化操作。
    • hookState._value[0] !== nextValue 比較新舊值避免不必要的渲染。
    • 可以看出,更新操作利用了組件實例的 this.setState 函數。這就是為什么 hook 可以代替 classthis.state 使用。

    useEffect

    使用方式

    • 例如,常用根據 query 參數,首次加載組件只發一次請求內容。
    function Component(props) {
      const [state, setState] = useState({});
      
      useEffect(() => {
        ajax.then(data => setState(data));
      }, []); // 依賴項
      // ...
    }
    
    • useState 有說到,props 初始 state 有坑,可以用 useEffect 實現。
    function Component(props) {
      const [state, setState] = useState(props.state);
      
      useEffect(() => {
        setState(props.state);
      }, [props.state]); // props.state 變動賦值給 state
      // ...
    }
    
    • 清除副作用,例如監聽改變瀏覽器窗口大小,之后清除副作用
    function WindowWidth(props) {
      const [width, setWidth] = useState(0);
    
      function onResize() {
        setWidth(window.innerWidth);
      }
      // 只執行一次副作用,組件 unmount 時會被清除
      useEffect(() => {
        window.addEventListener('resize', onResize);
        return () => window.removeEventListener('resize', onResize);
      }, []);
    
      return <div>Window width: {width}</div>;
    }
    
    • 注意:在 useEffect 在使用 state 時最好把它作為依賴,不然容易產生 bug
    function Component() {
      const [a, setA] = useState(0);
      useEffect(() => {
        const timer = setInterval(() => console.log(a), 100);
        return () => clearInterval(timer)
      }, []);
      return <button onClick={() => setA(a+1)}>{a}</button>
    }
    

    當你點擊按鈕 a+=1 時,此時 console.log 依舊打印 0
    這是因為 useEffect 的副作用只會在組件首次加載時入 _pendingEffects 數組,形成閉包。

    修改如下:

    function Component() {
      const [a, setA] = useState(0);
      useEffect(() => {
        const timer = setInterval(() => console.log(a), 100);
        return () => clearInterval(timer)
    -  }, []);
    +  }, [a]);
      return <button onClick={() => setA(a+1)}>{a}</button>
    }
    

    這段代碼在 React 里運行,輸出會隨點擊按鈕而變化,而在 preact 中,之前定時器未被清除,
    說明有 bug。-_-||

    為什么?

    • useEffect 解決了什么問題

    一般發送數據請求 componentDidMount 中,之后 componentWillUnmount 在相關清理。
    這就導致相互無關的邏輯夾雜在 componentDidMount,而對應的掃尾工作卻分配在 componentWillUnmount 中。

    有了 useEffect ,你可以把相互獨立的邏輯寫在不同的 useEffect 中,他人擔心維護時,也不用擔心其他代碼塊里還有清理代碼。

    • 在組件函數體內執行副作用(改變 DOM、添加訂閱、設置定時器、記錄日志等)是不被允許的?

    每次 diff 函數組件會被當做class 組件的 this.render 函數類似使用,
    整體會被執行,在主體里操作副作用是致命的。

    • useEffect 的機制?

    源碼分析

    function useEffect(callback, args) {
      const state = getHookState(currentIndex++);
      if (argsChanged(state._args, args)) {
        state._value = callback;
        state._args = args;
    
        currentComponent.__hooks._pendingEffects.push(state);
      }
    }
    
    • 工具函數,依賴項為 undefined 或依賴項數組中一個值變動,則 true
    function argsChanged(oldArgs, newArgs) {
      return !oldArgs || newArgs.some((arg, index) => arg !== oldArgs[index]);
    }
    
    • 可以看出副作用的回調函數會在 _pendingEffects 數組中維護,代碼有兩處執行
    options._render = vnode => {
      currentComponent = vnode._component;
      currentIndex = 0;
    
      if (currentComponent.__hooks) { // 這里為什么要清理了再執行!!!
        currentComponent.__hooks._pendingEffects.forEach(invokeCleanup);
        currentComponent.__hooks._pendingEffects.forEach(invokeEffect);
        currentComponent.__hooks._pendingEffects = [];
      }
    };
    
    function invokeCleanup(hook) {
      if (hook._cleanup) hook._cleanup();
    }
    
    function invokeEffect(hook) {
      const result = hook._value(); // 如果副作用函數有返回函數的,會被當成清理函數保存。
      if (typeof result === 'function') hook._cleanup = result;
    }
    
    options.diffed = vnode => {
      const c = vnode._component;
      if (!c) return;
    
      const hooks = c.__hooks;
      if (hooks) {
        if (hooks._pendingEffects.length) {
          afterPaint(afterPaintEffects.push(c));
        }
      }
    };
    
    function afterPaint(newQueueLength) {
      if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) {
        prevRaf = options.requestAnimationFrame;
        (prevRaf || afterNextFrame)(flushAfterPaintEffects);
      }
    }
    
    function flushAfterPaintEffects() {
      afterPaintEffects.some(component => {
        if (component._parentDom) {
          try {
            component.__hooks._pendingEffects.forEach(invokeCleanup);
            component.__hooks._pendingEffects.forEach(invokeEffect);
            component.__hooks._pendingEffects = [];
          } catch (e) {
            options._catchError(e, component._vnode);
            return true;
          }
        }
      });
      afterPaintEffects = [];
    }
    
    • 我很懷疑,options._render 的代碼是從 flushAfterPaintEffects 不假思索的拷過去。
      導致上面講到的一個 bug

    • afterPaint 利用 requestAnimationFramesetTimeout 來達到以下目的

    componentDidMountcomponentDidUpdate 不同的是,在瀏覽器完成布局與繪制之后,傳給 useEffect 的函數會延遲調用,不會在函數中執行阻塞瀏覽器更新屏幕的操作。

    useMemo

    使用方式

    const memoized = useMemo(
      () => expensive(a, b),
      [a, b]
    );
    

    為什么?

    • useMemo 解決了什么問題

    上面反復強調了,函數組件體會被反復執行,如果進行大的開銷的會吃性能。
    所以 react 提供了 useMemo 來緩存函數執行返回結果,useCallback 來緩存函數。

    源碼分析

    function useMemo(factory, args) {
      const state = getHookState(currentIndex++);
      if (argsChanged(state._args, args)) {
        state._args = args;
        state._factory = factory;
        return (state._value = factory());
      }
    
      return state._value;
    }
    
    • 可以看出,只是把傳入的函數根據依賴性執行了一遍把結果保存在內部的 hook state 中。

    • 記住,所有的 hook aoi 都一樣,不要在沒有傳入state 作為依賴項的情況下,在傳入的函數體中
      使用 state

    useCallback

    使用方式

    const onClick = useCallback(
      () => console.log(a, b),
      [a, b]
    );
    

    為什么?

    • useCallback 解決了什么問題

    上面提到了,用來緩存函數的

    • 例如,上面優化監聽窗口的例子。
    function WindowWidth(props) {
      const [width, setWidth] = useState(0);
    
    -  function onResize() {
    -    setWidth(window.innerWidth);
    -  }
      
    +  const onResize = useCallback(() => {
    +    setWidth(window.innerWidth);
    +  }, []);
    
      useEffect(() => {
        window.addEventListener('resize', onResize);
        return () => window.removeEventListener('resize', onResize);
      }, []);
    
      return <div>Window width: {width}</div>;
    }
    

    上面說過,沒有依賴的時,不使要用 width,但可以使用 setWidth
    函數是引用,閉包變量 setWidth 是同一個地址。

    源碼分析

    • useMemo 的封裝
    function useCallback(callback, args) {
      return useMemo(() => callback, args);
    }
    

    上篇完。

    下篇介紹

    1. useReducer
    2. useContext
    3. useRef
    4. useLayoutEffect
    5. useImperativeHandle
    6. 自定義 hook
    7. 總結函數組件 hook 與 class 組件的對比

    參考

    1. React 文檔
    2. Preact 文檔
    3. Preact 源碼
    版權聲明:本文為guduyibeizi原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
    本文鏈接:https://blog.csdn.net/guduyibeizi/article/details/104578529

    智能推薦

    Linux C系統編程-線程互斥鎖(四)

    互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...

    統計學習方法 - 樸素貝葉斯

    引入問題:一機器在良好狀態生產合格產品幾率是 90%,在故障狀態生產合格產品幾率是 30%,機器良好的概率是 75%。若一日第一件產品是合格品,那么此日機器良好的概率是多少。 貝葉斯模型 生成模型與判別模型 判別模型,即要判斷這個東西到底是哪一類,也就是要求y,那就用給定的x去預測。 生成模型,是要生成一個模型,那就是誰根據什么生成了模型,誰就是類別y,根據的內容就是x 以上述例子,判斷一個生產出...

    styled-components —— React 中的 CSS 最佳實踐

    https://zhuanlan.zhihu.com/p/29344146 Styled-components 是目前 React 樣式方案中最受關注的一種,它既具備了 css-in-js 的模塊化與參數化優點,又完全使用CSS的書寫習慣,不會引起額外的學習成本。本文是 styled-components 作者之一 Max Stoiber 所寫,首先總結了前端組件化樣式中的最佳實踐原則,然后在此基...

    基于TCP/IP的網絡聊天室用Java來實現

    基于TCP/IP的網絡聊天室實現 開發工具:eclipse 開發環境:jdk1.8 發送端 接收端 工具類 運行截圖...

    猜你喜歡

    19.vue中封裝echarts組件

    19.vue中封裝echarts組件 1.效果圖 2.echarts組件 3.使用組件 按照組件格式整理好數據格式 傳入組件 home.vue 4.接口返回數據格式...

    劍指Offer39-調整數組順序使奇數位于偶數前面

    一開始想著用冒泡排序的方法來做,但是bug還是很多,后來看了評論區答案,發現直接空間換時間是最簡單的,而且和快排的寫法是類似的。...

    【一只蒟蒻的刷題歷程】【藍橋杯】歷屆試題 九宮重排 (八數碼問題:BFS+集合set)

    資源限制 時間限制:1.0s 內存限制:256.0MB 問題描述 如下面第一個圖的九宮格中,放著 1~8 的數字卡片,還有一個格子空著。與空格子相鄰的格子中的卡片可以移動到空格中。經過若干次移動,可以形成第二個圖所示的局面。 我們把第一個圖的局面記為:12345678. 把第二個圖的局面記為:123.46758 顯然是按從上到下,從左到右的順序記錄數字,空格記為句點。 本題目的任務是已知九宮的初態...

    dataV組件容器寬高發生變化后,組件不會自適應解決方法

    項目中需要大屏幕數據展示,于是使用了dataV組件,但是使用是發現拖動瀏覽器邊框,dataV組件顯示異常,如圖: 于是查了官網,官網的解釋如下:   于是按照官網的意思編寫代碼: 于是可以自適應了...

    CSS3干貨10:如何做一個板塊標題水平線左邊帶顏色效果

    很多網站在設計欄目標題的時候,喜歡用下劃線分開欄目標題和內容部分。 而且線條左邊的部分往往還有顏色,且這個顏色跟標題的文字長短保持一致。效果如圖所示: 這種效果其實很簡單。 我這里給大家推薦兩種方式: 假定我們的標題部分 HTML 結構如下: 方式一:利用下邊框。灰色部分是 h1 的下邊框,藍色部分是 span 標簽的下邊框。 h1 的高度為 40px,span 也設置它的高度為 40px。這樣,...

    精品国产乱码久久久久久蜜桃不卡