認清 React 的useState邏輯

feshin發表於2021-03-25

useState執行過程解析


function App() {
  const [n, setN] = useState(0); //使用 myUseState()
  return (
    <div>
      <p>{n}</p>
      <p>
        <button
          onClick={() => {
            setN(n + 1);
          }}
        >
          +1
        </button>
      </p>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App></App>, rootElement);
第一次渲染
  1. 呼叫 render <App /> ,<App / >元件呼叫 App()函式

  2. App()函式呼叫 const [n, setN] = useState(0)給 n 賦值,並且得到 一個 setN函式

  3. App()函式會根據 n 的值 和 setN返回一個虛擬的 DOM節點

  4. React 會根據 虛擬DOM建立一個真實DOM節點,並且將真實DOM渲染到瀏覽器上,所以我們可以通過除錯工具看到一個渲染出來的 DIV節點

點選一次button按鈕
  1. 呼叫 onClick()函式,onClick()函式又呼叫setN()函式

  2. setN在更新了 n 的值(先不管是怎麼更新的)之後會重新呼叫 render <App />

  3. 元件重新呼叫 App()函式

  4. App函式同樣會執行const [n, setN] = useState(0),根據 新的n 返回一個 新的虛擬DOM

  5. React 根據 DOM Diff演算法 將 新的虛擬DOM舊的虛擬DOM進行對比,得到最小的變動範圍物件(patch 物件)

  6. 根據最小的變動範圍物件來更新真實DOM

    注:

    1. 每次呼叫 App()都會執行 const [n, setN] = useState(0)
    2. 同樣的一句話每次呼叫之後,每次得到的 n的值是不一樣的

點選button 後 n 是怎麼變的 ?

首先要思考幾個問題:

  • 執行 setN()的時候會發生什麼: n 會變嗎?App()會重新執行嗎?

  • 如果 App()會重新執行,那麼 useState(0)的時候,n每次的值會有不同嗎?

    以上問題通過 console.log()就能得到答案

執行 setN的時候
  1. 一定會重新渲染UI,但是這時候 n 會變嗎?n 還沒有變!!

  2. setN( n + 1)並沒有改變 n,而是改變了一箇中介資料 state(state 是什麼?繼續往下看)

  3. 因為要重新渲染UI,所以 render <App />會重新執行 App()

重新執行App()的時候
  1. 重新執行const [n, setN] = useState(0)的時候確實 n 會得到不同的值!
分析
  • setN

    • setN(n + 1) 一定會修改資料 state,將 n + 1存入state
    • setN()一定會觸發 重新渲染(re-render)
  • useState

    • useState肯定會從 state讀取 n 的最新值
  • state

    • 每個元件都有自己的資料 state,這是我們需要理解的核心

嘗試實現一個 useState



const myUseState = (initialValue) => {
	let  state =  initialValue ;// 我們設定的一個 state 接收 初始值
  const setState = (newValue) => {
      // 我們設定的一個 setState函式
    state = newValue; // 更新 state 的值
    re_render(); // useState會重新渲染頁面UI
  };
  return [state, setState]; // 返回一個 state 和 更新state的函式: setState
};

const re_render = () => {
  ReactDOM.render(<App></App>, rootElement);
};

// 上面是我們實現的一個 myUseState
function App() {
  console.log("App 執行了");
  const [n, setN] = myUseState(0);
  console.log(n)
  return (
    <div>
      <p>{n}</p>
      <p>
        <button
          onClick={() => {
            setN(n + 1);
          }}
        >
          +1
        </button>
      </p>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App></App>, rootElement);

上面的程式碼還存在一些問題

  • 每次呼叫 myUseState(0)都會將 state重置為初始值

  • 我們需要一個不會被 myUseState重置的 state變數

  • 還需要判斷 state的取值是 初始值還是新值

改進 state變數


let _state;
const myUseState = (initialValue) => {
	_state =  _state === undefined ? initialValue : _state;// 我們設定的一個 state 接收 初始值
  const setState = (newValue) => {
      // 我們設定的一個 setState函式
    _state = newValue; // 更新 state 的值
    re_render(); // useState會重新渲染頁面UI
  };
  return [_state, setState]; // 返回一個 state 和 更新state的函式: setState
};

const re_render = () => {
  ReactDOM.render(<App></App>, rootElement);
};

// 上面是我們實現的一個 myUseState
function App() {
  console.log("App 執行了");
  const [n, setN] = myUseState(0);
  console.log(n)
  return (
    <div>
      <p>{n}</p>
      <p>
        <button
          onClick={() => {
            setN(n + 1);
          }}
        >
          +1
        </button>
      </p>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App></App>, rootElement);

這樣我們就實現了一個 useState!!

useState那麼簡單嗎?

  • 上面的 useState只能實現一個元件使用一個 useState 的情況
  • 如果一個元件使用了兩個 useState,由於資料都放在 _state,所以會產生衝突
改進思路
  1. 把_state做成一個物件
    • 比如_state = { n:0, m:0 }
    • 不行, 因為useState(0)並不知道變數叫 n 還是 m
  2. 把_state 做成陣列
    • 比如_state = [0, 0]
    • 貌似可以

let _state = [];
let index = 0;

const myUseState = (initialValue) => {
    const currentIndex = index; // 需要一個 currentIndex 來儲存當前的 index 的值
    _state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
    const setState = (newValue) => {
        _state[currentIndex] = newValue;
        re_render();
    };

    index += 1; // 當前index設定完之後,後面的state要存放在下一個 index中
    return [_state[currentIndex], setState];
};

const re_render = () => {
    ReactDOM.render(<App></App>, rootElement);
};

function App() {
    const [n, setN] = myUseState(0);
    const [m, setM] = myUseState(0);
    return (
        <div>
            <p>n :{n}</p>
            <button
                onClick={() => {
                    setN(n + 1);
                }}
            >
                n+1
            </button>
            <p>m: {m}</p>
            <button
                onClick={() => {
                    setM(m + 1);
                }}
            >
                m+1
            </button>
        </div>
    );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App></App>, rootElement);
程式碼分析
  1. 上面的程式碼貌似可以解決兩個 state 同時儲存的問題,但是還是不能完成 useState的操作
  2. 我們應該要注意到當我們只是想重新更新 state的值的時候需要重新渲染App() ,index += 1同樣也會執行,這就意味著這個陣列每次調動useState之後都會變長一個單位!變長的總長度就是 App()裡面使用的 useState的次數。
  3. 所以我們需要在每次渲染 <App />之前都需要重置一下 index = 0
重置 index

let _state = [];
let index = 0;

const myUseState = (initialValue) => {
    const currentIndex = index; // 需要一個 currentIndex 來儲存當前的 index 的值
    _state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
    const setState = (newValue) => {
        _state[currentIndex] = newValue;
        re_render();
    };

    index += 1; // 當前index設定完之後,後面的state要存放在下一個 index中
    return [_state[currentIndex], setState];
};

const re_render = () => {
    index = 0; // !! 這裡很重要,重新渲染之前必須要重置 index = 0,否則陣列會變長!
    ReactDOM.render(<App></App>, rootElement);
};

function App() {
    const [n, setN] = myUseState(0);
    const [m, setM] = myUseState(0);
    return (
        <div>
            <p>n :{n}</p>
            <button
                onClick={() => {
                    setN(n + 1);
                }}
            >
                n+1
            </button>
            <p>m: {m}</p>
            <button
                onClick={() => {
                    setM(m + 1);
                }}
            >
                m+1
            </button>
        </div>
    );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App></App>, rootElement);
終於成功實現了useState的邏輯!

總結一下:

1. 我們要知道 `useState`的執行過程,n 是怎麼進行改變的
2. 當使用兩個 `state`的時候要保證不衝突,所以要把 state 定義成陣列用來儲存
3. 使用 `currentIndex` 記錄當前的 index
4. 在 App 渲染之前要重置 `index = 0`,否則 state 陣列會變長! 

useState很明顯的缺點

  1. useState 呼叫順序不能亂

    • 如果第一次渲染時 n 是第一個,m是第二個,k是第三個
    • 則第二次渲染時必須保證順序完全一致
    • 所以React不允許出現以下程式碼

    不允許出現的程式碼:

    if( n % 2 === 0){
    	[m, setM] = React.useState(0);
    }
    

    否則會報錯:

    /* 
    React Hook 'React.useState' is called conditionally. React Hooks must be called in the exact same order in every component render.
    

    這個報錯的原理其實就是我們儲存state的陣列的順序是必須要一致的,否則如果存在判斷語句的話 state的呼叫順序不一致,就會導致 state的值混亂!這會直接導致出現bug或者錯誤!!

    注:vue3 目前已經克服了這個問題,似然vue3是借鑑的 react 的思想

  2. App 用了 _state 和 index,那其他元件還能用嗎?

    • 我們可以給每個元件都建立 _state 和 index
  3. _state 和 index 放在全域性作用域重名了咋辦?

    • 每個元件都有一個虛擬DOM節點
    • 我們可以把 _state和index放到虛擬DOM上

相關文章