X 爲啥不是 hook?

X 爲啥不是 hook?

由讀者翻譯的版本:西班牙語html

React Hooks 第一個 alpha 版本發佈以來, 這個問題一直被激烈討論:「爲何 API 不是 hook?」前端

你要知道,只有下面這幾個算是 hooks:react

可是像 React.memo()<Context.Provider>,這些 API 它們不是 Hooks。通常來講,這些 Hook 版本的 API 被認爲是 非組件化反模塊化 的。這篇文章將幫助你理解其中的原理。android

注:這篇文章並不是教你如何高效的使用 React,而是對 hooks API 饒有興趣的開發者所準備的深刻分析。ios


如下兩個重要的屬性是咱們但願 React 的 APIs 應該擁有的:git

  1. 可組合Custom Hooks(自定義 Hooks)極大程度上決定了 Hooks API 爲什麼如此好用。咱們但願開發者們常用自定義 hooks,這樣就須要確保不一樣開發者所寫的 hooks 不會衝突。(撰寫乾淨而且不會相互衝突的組件實在太棒了)github

  2. 可調試:隨着應用的膨脹,咱們但願 bug 很容易被發現。React 最棒的特性之一就是,當你發現某些渲染錯誤的時候,你能夠順着組件樹尋找,直到找出是哪個組件的 props 或 state 的值致使的錯誤。後端

有了這兩個約束,咱們就知道哪些算是真正意義上的 Hook,而哪些不算。api


一個真正的 Hook: useState()

可組合

多個自定義 Hooks 各自調用 useState() 不會衝突:安全

function useMyCustomHook1() {
  const [value, setValue] = useState(0);
  // 不管這裏作了什麼,它都只會做用在這裏
}

function useMyCustomHook2() {
  const [value, setValue] = useState(0);
  // 不管這裏作了什麼,它都只會做用在這裏
}

function MyComponent() {
  useMyCustomHook1();
  useMyCustomHook2();
  // ...
}
複製代碼

無限制的調用一個 useState() 老是安全的。在你聲明新的狀態量時,你不用理會其餘組件用到的 Hooks,也不用擔憂狀態量的更新會相互干擾。

結論:useState() 不會使自定義 Hooks 變得脆弱。

可調試

Hooks 很是好用,由於你能夠在 Hooks 之間傳值:

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  // ...
  return width;
}

function useTheme(isMobile) {
  // ...
}

function Comment() {
  const width = useWindowWidth();
  const isMobile = width < MOBILE_VIEWPORT;
  const theme = useTheme(isMobile);
  return (
    <section className={theme.comment}>
      {/* ... */}
    </section>
  );
}
複製代碼

可是若是咱們的代碼出錯了呢?咱們又該怎麼調試?

咱們先假設,從 theme.comment 拿到的 CSS 的 class 是錯的。咱們該怎麼調試? 咱們能夠打一個斷點或者在咱們的組件體內加一些 log。

咱們可能會發現 theme 是錯的,可是 widthisMobile 是對的。這會提示咱們問題出在 useTheme() 內部。又或許咱們發現 width 自己是錯的。這能夠指引咱們去查看 useWindowWidth()

簡單看一下中間值就能指導咱們哪一個頂層的 Hooks 有 bug。 咱們不須要挨個去查看他們全部的實現。

這樣,咱們就可以洞察 bug 所在的部分,幾回三番以後,程序問題終得其解。

若是咱們的自定義 Hook 嵌套的層級加深的時候,這一點就顯得很重要了。假設一下咱們有一個 3 層嵌套的自定義 Hook,每一層級的內部又用了 3 個不一樣的自定義 Hooks。在 3 處找bug和最多 3 + 3×3 + 3×3×3 = 39 處找 bug 的區別是巨大的。幸運的是, useState() 不會魔法般的 「影響」 其餘 Hooks 或組件。與任何 useState() 所返回的變量同樣,一個可能形成 bug 的返回值也是有跡可循的。

結論:useState() 不會使你的代碼邏輯變得模糊不清,咱們能夠直接沿着麪包屑找到 bug。


它不是一個 Hook: useBailout()

做爲一個優化點,組件使用 Hooks 能夠避免重複渲染(re-rendering)。

其中一個方法是使用 React.memo() 包裹住整個組件。若是 props 和上次渲染完以後對比淺相等(shallowly equal),就能夠避免重複渲染。這和 class 模式中的PureComponent 很像。

React.memo() 接受一個組件做爲參數,並返回一個組件:

function Button(props) {
  // ...
}
export default React.memo(Button);
複製代碼

但它爲何就不是 Hook?

不論你叫它 useShouldComponentUpdate()usePure()useSkipRender() 仍是 useBailout(),它看起來都差很少長這樣:

function Button({ color }) {
  // ⚠️ 不是真正的 API
  useBailout(prevColor => prevColor !== color, color);

  return (
    <button className={'button-' + color}> OK </button>
  )
}
複製代碼

還有一些其餘的變種 (好比:一個簡單的 usePure()) 可是大致上來講,他們都有一些相同的缺陷。

可組合

咱們來試試把 useBailout() 放在 2 個自定義 Hooks 中:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  // ⚠️ 不是真正的 API
  useBailout(prevIsOnline => prevIsOnline !== isOnline, isOnline);

  useEffect(() => {
    const handleStatusChange = status => setIsOnline(status.isOnline);
    ChatAPI.subscribe(friendID, handleStatusChange);
    return () => ChatAPI.unsubscribe(friendID, handleStatusChange);
  });

  return isOnline;
}

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  
  // ⚠️ 不是真正的 API
  useBailout(prevWidth => prevWidth !== width, width);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  });

  return width;
}
複製代碼

譯註:使用了 useBailout 後,useFriendStatus 只會在 isOnline 狀態變化時才容許 re-render,useWindowWidth 只會在 width 變化時才容許 re-render。

如今若是你在同一個組件中同時用到他們會怎麼樣呢?

function ChatThread({ friendID, isTyping }) {
  const width = useWindowWidth();
  const isOnline = useFriendStatus(friendID);
  return (
    <ChatLayout width={width}>
      <FriendStatus isOnline={isOnline} />
      {isTyping && 'Typing...'}
    </ChatLayout>
  );
}
複製代碼

何時會 re-render 呢?

若是每個 useBailout() 的調用都有能力跳過此次更新,若是 useFriendStatus() 阻止了 re-render,那麼 useWindowWidth 就沒法得到更新,反之亦然。這些 Hooks 會相互阻塞。

然而,在組件內部,假若只有全部調用了 useBailout() 都贊成不 re-render 組件纔不會更新,那麼當 props 中的 isTyping 改變時,因爲內部全部 useBailout() 調用都沒有贊成更新,致使 ChatThread 也沒法更新。

基於這種假設,將致使更糟糕的局面,任何新置入組件的 Hooks 都須要去調用 useBailout(),不這樣作的話,它們就沒法投出「反對票」來讓本身得到更新。

結論: 🔴 useBailout() 破壞了可組合性。添加一個 Hook 會破壞其餘 Hooks 的狀態更新。咱們但願這些 APIs 是穩定的,可是這個特性顯然是與之相反了。

Debugging

useBailout() 對調試有什麼影響呢?

咱們用相同的例子:

function ChatThread({ friendID, isTyping }) {
  const width = useWindowWidth();
  const isOnline = useFriendStatus(friendID);
  return (
    <ChatLayout width={width}> <FriendStatus isOnline={isOnline} /> {isTyping && 'Typing...'} </ChatLayout> ); } 複製代碼

事實上即便 prop 上層的某處改變了,Typing... 這個 label 也不會像咱們指望的那樣出現。那麼咱們怎麼調試呢?

通常來講, 在 React 中你能夠經過向尋找的辦法,自信的回答這個問題。 若是 ChatThread 沒有獲得新的 isTyping 的值, 咱們能夠打開那個渲染 <ChatThread isTyping={myVar} /> 的組件,檢查 myVar,諸如此類。 在其中的某一層, 咱們會發現要麼是容易出錯的 shouldComponentUpdate() 跳過了渲染, 要麼是一個錯誤的 isTyping 的值被傳遞了下來。一般來講查看這條鏈路上的每一個組件,已經足夠定位到問題的來源了。

然而, 假如這個 useBailout() 真是個 Hook,若是你不檢查咱們在 ChatThread 中用到的每個自定義 Hook (深刻地) 和在各自鏈路上的全部組件,你永遠都不會知道跳過此次更新的緣由。更由於任何父組件可能會用到自定義 Hooks, 這個規模很恐怖。

這就像你要在抽屜裏找一把螺絲刀,而每一層抽屜裏都包含一堆小抽屜,你沒法想象愛麗絲仙境中的兔子洞有多深。

結論:🔴 useBailout() 不只破壞了可組合性,也極大的增長了調試的步驟和找 bug 過程的認知負擔 — 某些時候,是指數級的。


全文咱們探討了一個真正的 Hook,useState(),和一個不太算是 Hook 的 useBailout(),並從可組合性及可調試性兩個方面說明了爲何一個是 Hook,而一個不算是 Hook。

儘管如今沒有 「Hook 版本的 memo()shouldComponentUpdate(),但 React 確實提供了一個名叫 useMemo() 的 Hook。它有相似的做用,可是他的語義不會迷惑使用它的人。

useBailout() 這個例子,描述了控制組件是否 re-render 並不適合作成一個 hook。這裏還有一些其餘的例子 - 例如,useProvider()useCatch()useSuspense()

如今你知道爲何某些 API 不算是 Hook 了嗎?

(當你開始迷惑時,就提醒本身:可組合... 可調試)

Discuss on TwitterEdit on GitHub

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


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

相關文章
相關標籤/搜索