[譯] React Hooks 揭祕:陣列解構融成魔法

Xekin發表於2018-11-14

用模型解析此提案的執行規則

我超喜歡 React 新出的這個 Hooks API。而在使用它時卻有一些奇怪的規則。為了那些糾結於為什麼要有這些規則的人,在這裡我會以模型圖的方式來向你們展示這個新的 API。

警告: Hooks 這項提案仍在測試階段

這篇文章主要講述的是關於 React hooks 這項新 API,此時這個提案仍處於 alpha 測試版本。你可以在 React 官方文件中找到穩定的 React API。


[譯] React Hooks 揭祕:陣列解構融成魔法

圖片來自網站 Pexels(rawpixel.com

拆解 Hooks 的工作方式

“就像魔法一樣無法理解”,這是我從一些人口中聽到的對於這項新提案的評價,所以我願意嘗試通過拆解這項提案的程式碼語法去了解它是如何在程式碼中工作的,至少也要了解它最表層的執行邏輯。

Hooks 的規則

React 的核心團隊規定我們在使用 hooks 時必須遵從 hooks 提案文件中列出的兩條規則。

  • 不要在迴圈、條件語句或者巢狀函式中呼叫 Hooks
  • 只能在 React 函式中呼叫 Hooks

對於後者我覺得是不言而喻的。要將自己的業務程式碼嵌入到功能元件當中,你自然需要以某種方式將你的程式碼和元件聯絡起來。

至於前者我認為也是令人感到困惑的一點,因為這與正常程式設計時呼叫 API 的方式相比起來並不尋常,這也是我今天正想探索的一點。

在 hooks 中,狀態管理都與陣列有關

為了讓我們更清晰地理解這個模型,讓我們直接實現一個簡單的 hooks API 例項看看它可能長什麼樣子。

請注意這只是推測,並且只是可能的 API 實現方法,主要是為了展示應該通過怎樣的思維去理解它。當然這並不一定就是 API 實際的內部工作方式。而且目前這也只是一個提案,在未來一切都可能發生改變

我們可以怎樣實現 useState()

讓我們通過剖析一個例子來演示一下狀態鉤子的實現可能會怎麼執行吧。

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

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

  return (
    <Button onClick={() => setFirstName("Fred")}>Fred</Button>
  );
}
複製程式碼

實現 hooks API 的基本思想是把一個 setter 方法作為鉤子函式返回的第二個陣列項,之後通過這個 setter 方法來控制鉤子管理的狀態值。(在這個例子中,setter 方法指 setFirstName,鉤子就是 useState 而它管理的值就是"Rudi")

所以讓我們來看看 React 將利用它來做些什麼?

讓我先解釋一下在 React 的內部中它可能會怎麼工作。下面圖表將為我們展示一個特定元件在渲染時其內部執行上下文的執行過程。這也意味著儲存在這裡的資料是從外面傳入的。雖然這裡的狀態沒有與其他元件共享,但是它會繼續保留在一定範圍之內以供後續渲染對應的特殊元件時使用。

1) 初始化

宣告兩個空陣列:settersstate

設定一個 cursor 引數為 0

[譯] React Hooks 揭祕:陣列解構融成魔法

初始化:兩個空陣列,cursor 引數為 0


2) 初次渲染

第一次執行元件方法

當呼叫 useState() 時,第一次會將一個 setter 方法(就是以 cursor 為下標的引數)新增到 setters 陣列當中然後再把一些狀態新增到 state 陣列當中。

[譯] React Hooks 揭祕:陣列解構融成魔法

初次渲染:cursor ++,變數分別被寫入陣列當中


3) 後續渲染

後續的每一次渲染過程當中 cursor 都將被重置,而渲染的值都只是從每一次的陣列當中取出來的。

[譯] React Hooks 揭祕:陣列解構融成魔法

後續渲染:從陣列(以 cursor 為下標)中讀取了每一項變數的值


4) 事件代理

因為每一個 setter 方法都和其 cursor 繫結以至於通過觸發來呼叫任何 setter 時它都將改變對應索引位置的狀態陣列的狀態值。

[譯] React Hooks 揭祕:陣列解構融成魔法

Setters “記住”了他們的索引並根據它來寫入記憶體。


以及底層的實現

這裡用一段程式碼示例來展示:

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

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

// 這是我仿寫的 useState 輔助類
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];
}

// 我們在元件程式碼中使用上面的鉤子
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>
  );
}

// 這裡模擬了 React 的渲染週期
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 命令放在渲染週期裡執行會怎樣呢?

讓我們嘗試寫一些 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。一起來看這樣會對整個系統造成多大的影響。

被“破壞”的元件第一次渲染

[譯] React Hooks 揭祕:陣列解構融成魔法

渲染一個外部的“壞”鉤,之後它將會在下次渲染時消失不見。

在此時我們在例項中宣告的 firstNamelastName 暫時還帶著正確的資料,但接下來讓我們看看在第二次渲染時會發生什麼:

被“破壞”的元件第二次渲染

[譯] React Hooks 揭祕:陣列解構融成魔法

在渲染時移除了鉤子之後,我們發現了一個錯誤。

由於在此時我們的狀態儲存了錯位的資料使 firstNamelastName 被同時賦值為 “Rudi”。這很明顯是不正確的而且它也沒有任何作用,但是這也給我們說明了為什麼 hooks 具有這樣的規則限制。

React 團隊正在制定使用規範因為不遵守它們將會使資料錯位。

思考一下如何在不違反規則的情況下使用 hooks 操作一組陣列

所以現在我們應該非常清楚為什麼我們不能在迴圈或者條件語句中呼叫 use 鉤子。因為事實上我們的程式碼處理是基於陣列解構賦值的,如果你更改了渲染時的呼叫順序,那麼就會使我們的資料或者事件處理器在解構後沒有匹配正確。

所以技巧是考慮讓 hooks 用一致的 cursor 來管理陣列業務,如果你這麼做就可以讓它正常的工作。

結論

希望我是建立了一個比較清晰的模型向你們展示了這個新 hooks API 在底層是如何進行工作的。記住這裡真正的價值是如何把我們所好奇的點一步步解析出來,所以你只要多注意 hooks API 的規則,這樣我們就能更好的利用它。

在 React 元件中 Hooks 是一個高效率的 API。這也是人們對它趨之若鶩的原因,如果你還沒想到答案,那麼只要把陣列儲存為狀態的形式,就會發現你並沒有破壞他們的規則。

我希望將來可以持續再瞭解一下 useEffects 方法並且嘗試對比一下它和 React 的那些生命週期方法有什麼區別。


這篇文章尚有不足之處,如果你有更好的建議或者發現了文章中的錯誤紕漏請及時跟我聯絡。

你可以在 Twitter 上關注 Rudi Yardley @rudiyardley 或者關注 github _@_ryardley

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章