[譯] React Hooks: 沒有魔法,只是數組 原文鏈接: medium.com/@ryardley/r…
我是 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初始化
創建兩個空數組,分別用來存放 setters 和 state,將 指針 指到 0 的位置:
2) 組件首次render
當首次render這個函數組件的時候。
每一個 useState 調用,當 首次 執行的時候,在 setter 數組里加入一個 setter 函數(和對應的數組index關聯);然后,將 state 加入對應的 state 數組里:
3) 組件后續(非首次)render
后續組件的每次render,指針都會重置為 0 ,每調用一次 useState,都會返回指針對應的兩個數組里的 state 和 setter,然后將指針位置 +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: 0const [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 cursorreturn <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,firstName 和 lastName 都對應了正確的值。接下來看看組件第二次render的時候,會發生什么情況。
2) 第二次render
第二次render之后,我們的兩個state, firstName和 lastName 都成了 Rudi。這顯然是錯誤的,必須要避免這樣使用hooks!但是這也給我們演示了,hooks的調用順序,為什么不能改變。
react團隊明確強調了hooks的2個使用原則,如果不按照這些原則來使用hooks,將會導致我們數據的不一致性!
將hooks的操作想象成數組的操作,你可能不太會違背這些原則
OK,現在你應該清楚,為什么我們不能在條件塊或者循環語句里調用hooks了。因為調用hooks的過程中,我們是在操作數組上的指針,如果你在多次render中,改變了hooks的調用順序,將導致數組上的指針和組件里的 useState 不匹配,從而返回錯誤的 state 以及 setter 。
結論
希望我基本講明白了,hooks調用順序的大概原理。hooks是對react生態的一個很好的優化。人們對hooks感到興奮,是有原因的。如果你將hooks的操作,當做數組一樣來看待,那么你一般不會違背hooks的使用原則。