[譯] React Hooks: 沒有魔法,只是數組

原文鏈接medium.com/@ryardley/re

我是 React 新特性 Hooks 的粉絲。但是,在你使用 React Hooks的過程中,有一些看上去 很奇怪的限制 。在本文里,對於那些還在為了理解這些限制而苦苦掙扎的同志,我嘗試通過一些列圖表的方式,來解釋為什麼會存在這些限制。

理解hooks怎麼運行

我聽說很多同學都對hooks像魔法一般的效果感到困惑,因此我將嘗試通過淺顯的方式,來演示hooks是怎麼運行的。

hooks的原則

react團隊在怎麼使用hooks的 官方文檔 中,強調了兩點主要的使用原則:

  • 不要 在 循環、條件語句或者嵌套函數中調用hooks
  • 只能在 React 函數組件中調用hooks

第二點我認為是顯而易見的。為了給 函數組件 增加一些能力(比如 state,類聲明周期方法),你當然需要通過一種方式,來把這種能力賦給函數組件,這種方式就是使用hooks。

然而,第一點規則,很容易讓人感到困惑。不就是使用一個 API 么,為什麼還有這麼多限制呢。這也正是我將要在下文里解釋的。

hooks中的state管理,只是在操作數組

為了更加清晰的理解hooks,讓我們來看看怎麼簡單實現hooks API。

請注意,下面代碼只是一個demo,是為了讓我們理解hooks大概是怎麼運作的。這不是 React 中的真正內部實現。

怎麼實現 useState 呢?

讓我們通過一個例子來演示,useState內部大概是怎麼運作的。

組件代碼如下:

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

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

useState 實現的功能是,你能通過這個hook返回的 數組 中第二個元素,作為修改這個state的一個setter方法。

那麼,React可能會怎麼來實現 useState 呢?

讓我們來想想react內部會怎麼來實現 useState 呢。在下面的實現里,state 是存放在被render的組件外面,並且這個state不會和其他組件共享,同時,在這個組件後續render中,能夠通過特定的作用域方式,訪問到這個state

1) state初始化

創建兩個空數組,分別用來存放 settersstate,將 指針 指到 0 的位置:

2) 組件首次render

當首次render這個函數組件的時候。

每一個 useState 調用,當 首次 執行的時候,在 setter 數組裡加入一個 setter 函數(和對應的數組index關聯);然後,將 state 加入對應的 state 數組裡:

3) 組件後續(非首次)render

後續組件的每次render,指針都會重置為 0 ,每調用一次 useState,都會返回指針對應的兩個數組裡的 statesetter,然後將指針位置 +1

4)setter調用處理

每一個 setter 函數,都關聯了對應的指針位置。當調用某個 setter 函數式,就可以通過這個函數所關聯的指針,找到對應的 state,修改state數組裡對應位置的值:

最後來看看useState簡單的實現

let state = [];
let setters = [];
let firstRun = true;
let cursor = 0;

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

// This is the pseudocode for the useState helper
export function useState(initVal) {
if (firstRun) {
state.push(initVal);
setters.push(createSetter(cursor));
firstRun = false;
}

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

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

// Our component code that uses hooks
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={() => setFirstName("Fred")}>Fred</Button>
</div>
);
}

// This is sort of simulating Reacts rendering cycle
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 Fred button

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

為什麼hooks的調用順序不能變呢?

如果我們根據某些外部變數,或者組件自身的state,改變hooks的調用順序,會有什麼後果呢?

我們來演示下 錯誤的 做法:

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 是在一個 條件分支里。我們來看看這樣引入的bug。

1) 第一次render

第一個render之後,我們的兩個state,firstNamelastName 都對應了正確的值。接下來看看組件第二次render的時候,會發生什麼情況。

2) 第二次render

第二次render之後,我們的兩個state, firstNamelastName 都成了 Rudi。這顯然是錯誤的,必須要避免這樣使用hooks!但是這也給我們演示了,hooks的調用順序,為什麼不能改變。

react團隊明確強調了hooks的2個使用原則,如果不按照這些原則來使用hooks,將會導致我們數據的不一致性!

將hooks的操作想像成數組的操作,你可能不太會違背這些原則

OK,現在你應該清楚,為什麼我們不能在條件塊或者循環語句里調用hooks了。因為調用hooks的過程中,我們是在操作數組上的指針,如果你在多次render中,改變了hooks的調用順序,將導致數組上的指針和組件里的 useState 不匹配,從而返回錯誤的 state 以及 setter

結論

希望我基本講明白了,hooks調用順序的大概原理。hooks是對react生態的一個很好的優化。人們對hooks感到興奮,是有原因的。如果你將hooks的操作,當做數組一樣來看待,那麼你一般不會違背hooks的使用原則。

推薦閱讀:

相关文章