理解 React Hooks 心智模型:必須按順序、不能在條件語句中呼叫的規則

breezefeng發表於2021-07-29

前言

自從 React 推出 hooks 的 API 後,相信大家對新 API 都很喜歡,但是它對你如何使用它會有一些奇怪的限制。比如,React 官網介紹了 Hooks 的這樣一個限制:

不要在迴圈,條件或巢狀函式中呼叫 Hook, 確保總是在你的 React 函式的最頂層以及任何 return 之前呼叫他們。遵守這條規則,你就能確保 Hook 在每一次渲染中都按照同樣的順序被呼叫。這讓 React 能夠在多次的 useState 和 useEffect 呼叫之間保持 hook 狀態的正確。

useState Hook 的工作原理

這個限制並不是 React 團隊憑空造出來的,的確是由於 React Hooks 的實現設計而不得已為之。

為了讓大家有一個更清晰的思維模型,我將用陣列來模擬useState的簡單實現。

首先讓我們通過一個例子來看看 hook 是如何工作的。

我們首先從一個元件開始:

function RenderFunctionComponent() {
    const [firstName, setFirstName] = useState("Rudi");
    const [lastName, setLastName] = useState("Yardley");

    return (
        <Button onClick={() => setFirstName("Fred")}>Fred</Button>
    );
}

  

useState hook 背後的思想是,你可以使用 hook 函式返回的陣列的第二個陣列項作為 setter 函式,並且該 setter 將控制由 hook 管理的狀態。

具體實現

1)初始化

建立兩個空陣列:setters 和 state

將 cursor 設定為 0

2)第一次渲染

首次執行元件函式。

每次useState()呼叫,在第一次執行時,都會將一個 setter 函式推送到 setters 陣列上,然後將一些狀態推送到 state 陣列上。

3)後續渲染

每次後續渲染都會重置 cursor,並且僅從每個陣列中讀取這些值。

4)事件處理

每個 setter 都有對其 cursor 的引用,因此通過觸發對 setter 的呼叫,setter 它將更改狀態陣列中該位置的狀態值。

簡單程式碼實現

下面通過一段簡單的程式碼示例來演示該實現。

注意:這並不是 React 的底層實現,但對於我們理解 react hook 的心智模型非常有幫助。

const state = [];
const setters = [];
let cursor = 0;

function createSetter(cursor) {
    return function setterWithCursor(newVal) {
        state[cursor] = newVal;
    };
}

export function useState(initVal) {
    if (state[cursor] === undefined && setters[cursor] === undefined) {
        state.push(initVal);
        setters.push(createSetter(cursor));
    }

    const setter = setters[cursor];
    const value = state[cursor];

    cursor++;
    return [value, setter];
}

function RenderFunctionComponent() {
    const [firstName, setFirstName] = useState('Rudi'); // cursor: 0
    const [lastName, setLastName] = useState('Yardley'); // cursor: 1

    return (
        <div>
            <button onClick={() => setFirstName('Richard')}>Richard</button>
            <button onClick={() => setLastName('Fred')}>Fred</button>
        </div>
    );
}

function MyComponent() {
    cursor = 0; // resetting the cursor
    return <RenderFunctionComponent />; // render
}

console.log(state); // Pre-render: []
MyComponent();
console.log(state); // First-render: ['Rudi', 'Yardley']
MyComponent();
console.log(state); // Subsequent-render: ['Rudi', 'Yardley']

// click the 'Richard' button

console.log(state); // After-click: ['Richard', 'Yardley']

為什麼順序很重要

現在,如果我們根據某些外部因素甚至元件狀態更改渲染週期的鉤子順序會發生什麼?

讓我們做 React 團隊說你不應該做的事情:

let firstRender = true;

function RenderFunctionComponent() {
  let initName;

  if(firstRender){
    [initName] = useState("Rudi");
    firstRender = false;
  }
  const [firstName, setFirstName] = useState(initName);
  const [lastName, setLastName] = useState("Yardley");

  return (
    <Button onClick={() => setFirstName("Fred")}>Fred</Button>
  );
}  

這裡我們有useState的一個條件呼叫。讓我們看看這對系統造成的破壞。

Bad Component 第一次渲染

我們的例項變數firstNamelastName包含正確的資料,但讓我們看看第二次渲染會發生什麼:

Bad Component 第二次渲染

現在,firstNamelastName發生了錯位,我們的狀態儲存變得不一致了。這就是為什麼保持正確順序的重要性。

總結:

通過對 useState 的簡單實現來理解 react hooks 的幕後實現邏輯。考慮將狀態作為一組陣列存在的模型,那麼我們不該違反其對應的使用規則。

相關文章