<input id="ohw05"></input>
  • <table id="ohw05"><menu id="ohw05"></menu></table>
  • <var id="ohw05"></var>
  • <code id="ohw05"><cite id="ohw05"></cite></code>
    <label id="ohw05"></label>
    <var id="ohw05"></var>
  • 手寫useState與useEffect

    手寫useState與useEffect

    useStateuseEffect是驅動React hooks運行的基礎,useState用于管理狀態,useEffect用以處理副作用,通過手寫簡單的useStateuseEffect來理解其運行原理。

    useState

    一個簡單的useState的使用如下。

    // App.tsx
    import { useState } from "react";
    import "./styles.css";
    
    export default function App() {
      const [count, setCount] = useState(0);
    
      console.log("refresh");
      const addCount = () => setCount(count + 1);
    
      return (
        <>
          <div>{count}</div>
          <button onClick={addCount}>Count++</button>
        </>
      );
    }
    

    當頁面在首次渲染時會render渲染<App />函數組件,其實際上是調用App()方法,得到虛擬DOM元素,并將其渲染到瀏覽器頁面上,當用戶點擊button按鈕時會調用addCount方法,然后再進行一次render渲染<App />函數組件,其實際上還是調用了App()方法,得到一個新的虛擬DOM元素,然后React會執行DOM diff算法,將改變的部分更新到瀏覽器的頁面上。也就是說,實際上每次setCount都會重新執行這個App()函數,這個可以通過console.log("refresh")那一行看到效果,每次點擊按鈕控制臺都會打印refresh
    那么問題來了,頁面首次渲染和進行+1操作,都會調用App()函數去執行const [count, setCount] = useState(0);這行代碼,那它是怎么做到在+ +操作后,第二次渲染時執行同樣的代碼,卻不對變量n進行初始化也就是一直為0,而是拿到n的最新值。
    考慮到上邊這個問題,我們可以簡單實現一個useMyState函數,上邊在Hooks為什么稱為Hooks這個問題上提到了可以勾過來一個函數作用域的問題,那么我們也完全可以實現一個Hooks去勾過來一個作用域,簡單來說就是在useMyState里邊保存一個變量,也就是一個閉包里邊保存了這個變量,然后這個變量保存了上次的值,再次調用的時候直接取出這個之前保存的值即可,https://codesandbox.io/s/fancy-dust-kbd1i?file=/src/use-my-state-version-1.ts

    // index.tsx
    import { render } from "react-dom";
    import App from "./App";
    
    // 改造一下讓其導出 讓我們能夠強行刷新`<App />`
    export const forceRefresh = () => {
      console.log("Force fresh <App />");
      const rootElement = document.getElementById("root");
      render(<App />, rootElement);
    };
    
    forceRefresh();
    
    // use-my-state-version-1.ts
    import { forceRefresh } from "./index";
    
    let saveState: any = null;
    
    export function useMyState<T>(state: T): [T, (newState: T) => void] {
      saveState = saveState || state;
      const rtnState: T = saveState;
      const setState = (newState: T): void => {
        saveState = newState;
        forceRefresh();
      };
      return [rtnState, setState];
    }
    
    // App.tsx
    import { useMyState } from "./use-my-state-version-1";
    import "./styles.css";
    
    export default function App() {
      const [count, setCount] = useMyState(0);
    
      console.log("refresh");
      const addCount = () => setCount(count + 1);
    
      return (
        <>
          <div>{count}</div>
          <button onClick={addCount}>Count++</button>
        </>
      );
    }
    

    可以在code sandbox中看到現在已經可以實現點擊按鈕進行++操作了,而不是無論怎么點擊都是0,但是上邊的情況太過于簡單,因為只有一個state,如果使用多個變量,那就需要調用兩次useState,我們就需要對其進行一下改進了,不然會造成多個變量存在一個saveState中,這樣會產生沖突覆蓋的問題,改進思路有兩種:1把做成一個對象,比如saveState = { n:0, m:0 },這種方式不太符合需求,因為在使用useState的時候只會傳遞一個初始值參數,不會傳遞名稱; 2saveState做成一個數組,比如saveState:[0, 0]。實際上React中是通過類似單鏈表的形式來代替數組的,通過next按順序串聯所有的hook,使用數組也是一種類似的操作,因為兩者都依賴于定義Hooks的順序,https://codesandbox.io/s/fancy-dust-kbd1i?file=/src/use-my-state-version-2.ts

    // index.tsx
    import { render } from "react-dom";
    import App from "./App";
    
    // 改造一下讓其導出 讓我們能夠強行刷新`<App />`
    export const forceRefresh = () => {
      console.log("Force fresh <App />");
      const rootElement = document.getElementById("root");
      render(<App />, rootElement);
    };
    
    forceRefresh();
    
    // use-my-state-version-2.ts
    import { forceRefresh } from "./index";
    
    let saveState: any[] = [];
    let index: number = 0;
    
    export function useMyState<T>(state: T): [T, (newState: T) => void] {
      const curIndex = index;
      index++;
      saveState[curIndex] = saveState[curIndex] || state;
      const rtnState: T = saveState[curIndex];
      const setState = (newState: T): void => {
        saveState[curIndex] = newState;
        index = 0; // 必須在渲染前后將`index`值重置為`0` 不然就無法借助調用順序確定`Hooks`了
        forceRefresh();
      };
      return [rtnState, setState];
    }
    
    // App.tsx
    import { useMyState } from "./use-my-state-version-2";
    import "./styles.css";
    
    export default function App() {
      const [count1, setCount1] = useMyState(0);
      const [count2, setCount2] = useMyState(0);
    
      console.log("refresh");
      const addCount1 = () => setCount1(count1 + 1);
      const addCount2 = () => setCount2(count2 + 1);
    
      return (
        <>
          <div>{count1}</div>
          <button onClick={addCount1}>Count1++</button>
          <div>{count2}</div>
          <button onClick={addCount2}>Count2++</button>
        </>
      );
    }
    

    可以看到已經可以實現在多個State下的獨立的狀態更新了,那么問題又又來了,<App />用了saveStateindex,那其他組件用什么,也就是說多個組件如果解決每個組件獨立的作用域,解決辦法1每個組件都創建一個saveStateindex,但是幾個組件在一個文件中又會導致saveStateindex沖突。解決辦法2放在組件對應的虛擬節點對象上,React采用的也是這種方案,將saveStateindex變量放在組件對應的虛擬節點對象FiberNode上,在React中具體實現saveState叫做memoizedState,實際上React中是通過類似單鏈表的形式來代替數組的,通過next按順序串聯所有的hook
    可以看出useState是強依賴于定義的順序的,useState數組中保存的順序非常重要在執行函數組件的時候可以通過下標的自增獲取對應的state值,由于是通過順序獲取的,這將會強制要求你不允許更改useState的順序,例如使用條件判斷是否執行useState這樣會導致按順序獲取到的值與預期的值不同,這個問題也出現在了React.useState自己身上,因此React是不允許你使用條件判斷去控制函數組件中的useState的順序的,這會導致獲取到的值混亂,類似于下邊的代碼則會拋出異常。

    const App = () => {
        let state;
        if(true){
            [state, setState] = React.useState(0);
        }
        return (
            <div>{state}</div>
        )
    }
    
    <!-- React Hook "React.useState" is called conditionally. React Hooks must be called in the exact same order in every component render  react-hooks/rules-of-hooks-->
    

    這里當然只是對于useState的簡單實現,對于React真正的實現可以參考packages/react-reconciler/src/ReactFiberHooks.js,當前的React版本是16.10.2,也可以簡略看一下相關的type

    type Hooks = {
      memoizedState: any, // 指向當前渲染節點`Fiber` 上一次完整更新之后的最終狀態值
      baseState: any, // 初始化`initialState` 已經每次`dispatch`之后`newState`
      baseUpdate: Update<any> | null, // 當前需要更新的`Update` 每次更新完之后會賦值上一個`update` 方便`react`在渲染錯誤的邊緣數據回溯
      queue: UpdateQueue<any> | null, // 緩存的更新隊列 存儲多次更新行為
      next: Hook | null, // `link`到下一個`hooks` 通過`next`串聯所有`hooks`
    }
    

    useEffect

    一個簡單的useEffect的使用如下。

    import { useEffect, useState } from "react";
    import "./styles.css";
    
    export default function App() {
      const [count1, setCount1] = useState(0);
      const [count2, setCount2] = useState(0);
    
      console.log("refresh");
      const addCount1 = () => setCount1(count1 + 1);
      const addCount2 = () => setCount2(count2 + 1);
    
      useEffect(() => {
        console.log("count1 -> effect", count1);
      }, [count1]);
    
      return (
        <>
          <div>{count1}</div>
          <button onClick={addCount1}>Count1++</button>
          <div>{count2}</div>
          <button onClick={addCount2}>Count2++</button>
        </>
      );
    }
    

    同樣,每次addCount1都會重新執行這個App()函數,每次點擊按鈕控制臺都會打印refresh,在這里還通過count1變動的副作用來打印了count1 -> effect ${count1},而點擊addCount2卻不會處罰副作用的打印,原因明顯是我們只指定了count1的副作用,由此可見可以通過useEffect來實現更細粒度的副作用處理。
    在這里我們依舊延續上邊useState的實現思路,將之前的數據存儲起來,之后當函數執行的時候我們對比這其中的數據是否發生了變動,如果發生了變動,那么我們便執行該函數,當然我們還需要完成副作用清除的功能,https://codesandbox.io/s/react-usestate-8v0li9?file=/src/use-my-effect.ts

    // use-my-effect.ts
    const dependencyList: unknown[][] = [];
    const clearCallbacks: (void | (() => void))[] = [];
    let index: number = 0;
    
    export function useMyEffect(
      callback: () => void | (() => void),
      deps: unknown[]
    ): void {
      const curIndex = index;
      index++;
      const lastDeps = dependencyList[curIndex];
      const changed =
        !lastDeps || !deps || deps.some((dep, i) => dep !== lastDeps[i]);
      if (changed) {
        dependencyList[curIndex] = deps;
        const clearCallback = clearCallbacks[curIndex];
        if (clearCallback) clearCallback();
        clearCallbacks[curIndex] = callback();
      }
    }
    
    export function clearEffectIndex() {
      index = 0;
    }
    
    // App.tsx
    import { useState } from "react";
    import { useMyEffect, clearEffectIndex } from "./use-my-effect";
    import "./styles.css";
    
    export default function App() {
      const [count1, setCount1] = useState(0);
      const [count2, setCount2] = useState(0);
    
      console.log("refresh");
      const addCount1 = () => setCount1(count1 + 1);
      const addCount2 = () => setCount2(count2 + 1);
    
      useMyEffect(() => {
        console.log("count1 -> effect", count1);
        console.log("setTimeout", count1);
        return () => console.log("clear setTimeout", count1);
      }, [count1]);
    
      useMyEffect(() => {
        console.log("count2 -> effect", count2);
      }, [count2]);
    
      clearEffectIndex();
    
      return (
        <>
          <div>{count1}</div>
          <button onClick={addCount1}>Count1++</button>
          <div>{count2}</div>
          <button onClick={addCount2}>Count2++</button>
        </>
      );
    }
    

    通過上邊的實現,我們也可以通過將依賴與副作用清除函數存起來的方式,來實現useEffect,通過對比上一次傳遞的依賴值與當前傳遞的依賴值是否相同,來決定是否執行傳遞過來的函數,在這里由于我們無法得知這個React.Fc組件函數是在什么時候完成最后一個Effect,我們就需要手動來賦值這個標記的index0。當然在React之中同樣也是將useEffect掛載到了Fiber上來實現的,并且將所需要的依賴值存儲在當前的FibermemorizedState中,通過實現的鏈表以及判斷初次加載來實現了通過next按順序串聯所有的hooks,這樣也就能知道究竟哪個是最后一個Hooks了,另外useEffect同樣也是強依賴于定義的順序的,能夠讓React對齊多次執行組件函數時的依賴。

    自定義Hooks

    我在初學Hooks的時候一直有一個疑問,對于React Hooks的使用與普通的函數調用區別究竟在哪里,當時我還對知乎的某個問題強答了一番。

    以我學了幾天React的理解,自定義Hooks跟普通函數區別在于:

    • Hooks只應該在React函數組件內調用,而不應該在普通函數調用。
    • Hooks能夠調用諸如useStateuseEffectuseContext等,普通函數則不能。

    由此覺得Hooks就像mixin,是在組件之間共享有狀態和副作用的方式,所以應該是應該在函數組件中用到的與組件生命周期等相關的函數才能稱為Hooks,而不僅僅是普通的utils函數。
    對于第一個問題,如果將其聲明為Hooks但是并沒有起到作為Hooks的功能,那么私認為不能稱為Hooks,為避免混淆,還是建議在調用其他Hooks的時候再使用use標識。當然,諸如自己實現一個useState功能這種雖然并沒有調用其他的Hooks,但是他與函數組件的功能強相關,肯定是屬于Hooks的。
    對于第二個問題的話,其實必須使用use開頭并不是一個語法或者一個強制性的方案, 以use開頭其實更像是一個約定,就像是GET請求約定語義不攜帶Body一樣, 其主要目的還是為了約束語法,如果你自己實現一個類似useState簡單功能的話,就會了解到為什么不能夠出現類似于if (xxx) const [a, setA] = useState(0);這樣的代碼了,React文檔中明確說明了使用Hooks的規則,使用use開頭的目的就是讓React識別出來這是個Hooks,從而檢查這些規則約束,通常也會使用ESlint配合eslint-plugin-react-hooks檢查這些規則。

    后來對于這個問題有了新的理解,如果定義一個真正的自定義Hooks的話,那么通常都會需要使用useStateuseEffectHooks,就相當于自定義Hooks是由官方的Hooks組合而成的,而通過官方的這些Hooks來組合的話,就可以實現將數據掛載到節點上,也就是上邊的實現提到的實際memorizedState都是在Fiber中的,而自行實現的函數例如上邊的Hooks實現,是無法做到這一點的。也就是說我們通過自定義Hooks是通過來組合官方Hooks以及自己的邏輯來實現的對于節點內的一些狀態或者其他方面的邏輯封裝,而使用普通函數且采用類似于Hooks的語法的話則只能實現在全局的狀態和邏輯的封裝,簡單來說就是提供了接口來讓我們可以在節點上做邏輯的封裝。
    有一個簡單的例子,例如我們要封裝一個useUpdateEffect來避免在函數組件在第一次掛載的時候就執行effect,在這里我們就應該采用useRef或者是useState而不是僅僅定義一個變量來存儲狀態值,https://codesandbox.io/s/flamboyant-tu-21po2l?file=/src/App.tsx

    // use-update-effect-ref.ts
    import { DependencyList, EffectCallback, useEffect, useRef } from "react";
    
    export const useUpdateEffect = (
      effect: EffectCallback,
      deps?: DependencyList
    ) => {
      const isMounted = useRef(false);
    
      useEffect(() => {
        if (!isMounted.current) {
          isMounted.current = true;
        } else {
          return effect();
        }
      }, deps);
    };
    
    // use-update-effect-var.ts
    import { DependencyList, EffectCallback, useEffect } from "react";
    
    let isMounted = false;
    export const useUpdateEffect = (
      effect: EffectCallback,
      deps?: DependencyList
    ) => {
      useEffect(() => {
        if (!isMounted) {
          isMounted = true;
        } else {
          return effect();
        }
      }, deps);
    };
    
    // App.tsx
    import { useState, useEffect } from "react";
    import { useUpdateEffect } from "./use-update-effect-ref";
    // import { useUpdateEffect } from "./use-update-effect-var";
    import "./styles.css";
    
    export default function App() {
      const [count1, setCount1] = useState(0);
      const [count2, setCount2] = useState(0);
    
      const addCount1 = () => setCount1(count1 + 1);
      const addCount2 = () => setCount2(count2 + 1);
    
      useUpdateEffect(() => {
        console.log("count1 -> effect", count1);
      }, [count1]);
    
      useUpdateEffect(() => {
        console.log("count2 -> effect", count2);
      }, [count2]);
    
      return (
        <>
          <div>{count1}</div>
          <button onClick={addCount1}>Count1++</button>
          <div>{count2}</div>
          <button onClick={addCount2}>Count2++</button>
        </>
      );
    }
    

    當我們切換use-update-effect-refuse-update-effect-varuseUpdateEffect時,我們會發現當刷新頁面時使用use-update-effect-ref將不會有值打印,而use-update-effect-var則會打印count2 -> effect 0,而在點擊Count1++或者Count2++的效果都是正常的,說明use-update-effect-ref是能夠我們想要的useUpdateEffect功能,而use-update-effect-var卻因為變量值共享的問題而無法正確實現功能,當然我們也可以通過類似于數組的方式來解決這個問題,但是再具體到各個組件之間的共享上面,我們就無法在在類似于Hooks語法的基礎上來實現了,必須手動注冊一個閉包來完成類似的功能,而且類似于useStateset時刷新本組件以及子組件的方式,就必須借助useState來實現了。

    每日一題

    https://github.com/WindrunnerMax/EveryDay
    

    參考

    https://zhuanlan.zhihu.com/p/265662126
    https://juejin.cn/post/6927698033798807560
    https://segmentfault.com/a/1190000037608813
    https://github.com/brickspert/blog/issues/26
    https://codesandbox.io/s/flamboyant-tu-21po2l
    https://codesandbox.io/s/react-usestate-kbd1i
    https://codesandbox.io/s/react-usestate-8v0li9
    https://stackoverflow.com/questions/60133412/react-custom-hooks-vs-normal-functions-what-is-the-difference
    
    posted @ 2022-04-30 20:50  WindrunnerMax  閱讀(213)  評論(0編輯  收藏  舉報
    国产美女a做受大片观看