為什么順序調(diào)用對React Hook很重要

原文地址:Why Do React Hooks Rely on Call Order? - Dan Abramov

Hooks 重渲染時是依賴于固定順序調(diào)用的

Hook 規(guī)則

  • 請不要在循環(huán)、條件或者嵌套函數(shù)中調(diào)用 Hooks
  • 都有在 React 函數(shù)中才去調(diào)用 Hooks

React提供了一個 linter 插件來強制執(zhí)行這些規(guī)則

只在最頂層使用Hook

不要在循環(huán),條件或嵌套函數(shù)中調(diào)用 Hook呻引, 確彼突冢總是在你的 React 函數(shù)的最頂層以及任何 return 之前調(diào)用他們

這條規(guī)則是為了確保Hook在每次渲染中都按照同樣的順序被調(diào)用盖彭,這讓React能夠在多次的useStateuseEffect調(diào)用之間保持hook狀態(tài)正確

為什么一定要強調(diào)Hook按照順序調(diào)用

通常函數(shù)組件會有多個state,讓我們通過一個例子來理解useState可能是如何工作的

// 和useState一樣,myUseState接收一個初始值臀蛛,返回state和setState方法
const myUseState = initialValue => {
    let state = initialValue
    const setState = newValue => {
        state = newValue
        // 重新渲染
        render()
    }
    return [state, setState]
}

const render = () => {
    ReactDOM.render(<App />, document.getElementById('app'))
}

function App() {
    const [n, setN] = myUseState(0)
    ...
    return (
        <div>
            <p>{n}</p>
            <button onClick={() => setN(n + 1)}>
                +1
            </button>
        </div>
    );
}

點擊button尿扯,n沒有任何變化
原來每次state都變成了初始值0求晶,因為myUseState會將state重置

我們需要一個不會被myUseState重置的變量,那么這個變量只要聲明在myUseState外面即可

let _state;
const myUseState = initialValue => {
    // 如果state是undefined衷笋,則賦給初始值芳杏,否則就賦值為保存在外面的_state
    _state = _state === undefined ? initialValue : _state;
    const setState = newValue => {
        _state = newValue;
        render();
    };
    return [_state, setState];
};

還有問題,如果一個組件有倆state咋整辟宗?由于所有數(shù)據(jù)都放在_state爵赵,產(chǎn)生沖突:

function App() {
    const [n, setN] = myUseState(0)
    const [m, setM] = myUseState(0)
    ...
}

解決:

  • 把_state做成對象(注意后面會對此種方案進行詳細討論)
    • 不可行,沒有key泊脐,useState(0)只傳入了一個參數(shù)0空幻,并不知道是n還是m
  • 把_state做成數(shù)組
    • 可行,_state = [0, 0]
let _state = [];
// 同樣需要把index聲明在myUseState外面容客,用來記錄調(diào)用順序
let index = 0;
const myUseState = (initialValue) => {
    const currentIndex = index;
    // 對應調(diào)用順序下的state有值嗎秕铛?
    _state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
    const setState = (newValue) => {
        // 設置對應順序的state
        _state[currentIndex] = newValue;
        render();
    };
    // 下一個state的位置,使index加一位
    index += 1;
    return [_state[currentIndex], setState];
};

const render = () => {
    // 重新渲染要重置index
    // 注意觸發(fā)setter才會re-render
    index = 0;
    ReactDOM.render(<App />, document.getElementById('app'));
};

顯而易見的耘柱,因為數(shù)組根據(jù)調(diào)用順序存儲值如捅,每一個下標會對應其相應的state棍现,所以useState調(diào)用順序必須一致调煎!
re-render時會從第一行代碼開始重新執(zhí)行整個組件,所以調(diào)用順序依然是一致的

(需要注意的是己肮,這部分內(nèi)容只是API的一種可能實現(xiàn)方法士袄,真實useState使用鏈表存儲,為了大家更好地的理解它此處使用數(shù)組替代)

帶著剛剛的思考再次回顧谎僻,舉個??:

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

  return (
    <Button onClick={() => setFirstName("Fred")}>Fred</Button>
  );
}
  1. 初始化
    創(chuàng)建兩個空數(shù)組“state”與“setters”娄柳,設置指針“cursor”為 0


    初始化
  2. 首次渲染
    每當useState()被調(diào)用時,如果它是首次渲染艘绍,它會通過push將一個setter方法(綁定了指針“cursor”位置)放進“setters”數(shù)組中赤拒,同時,也會將另一個對應的狀態(tài)放進“state”數(shù)組中去
    首次渲染
  3. 后續(xù)渲染re-render
    每次的后續(xù)渲染都會重置指針“cursor”的位置(index=0)诱鞠,并會從每個數(shù)組中讀取對應的值(之前講了數(shù)據(jù)的存儲是獨立于組件之外的)


    后續(xù)渲染
  4. 處理事件
    每個setter都會有一個對應的指針位置的引用挎挖,因此當觸發(fā)任何setter調(diào)用的時候都會觸發(fā)去改變狀態(tài)數(shù)組中的對應的值
    處理事件

看到這里,想必大家對Hook的調(diào)用順序有了更深的印象了航夺,那么讓我們做一些React團隊禁止去做的事情蕉朵,比如在條件語句中使用Hook

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>
  );
}

我們在條件語句中調(diào)用了useState函數(shù),讓我們看看它對整個系統(tǒng)造成的破壞

糟糕組件的首次渲染

到此為止阳掐,我們的變量firstNamelastName依舊包含了正確的數(shù)據(jù)始衅,讓我們繼續(xù)去看一下第二次渲染會發(fā)生什么事情
糟糕的第二次渲染

現(xiàn)在firstNamelastName這兩個變量全部被設置為“Rudi”(該位置讀取到的是Rudi)冷蚂,與我們實際的存儲狀態(tài)不符

這個例子的用法顯然是不正確的,但是它讓我們知道了為什么我們必須使用React團隊規(guī)定的規(guī)則去使用Hooks

當然了汛闸,多虧了React提供了linter插件幫我們強制執(zhí)行了這條規(guī)則蝙茶,在代碼編譯過程中會報個錯 React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. React團隊yyds!orz!

所以你現(xiàn)在應該清楚為什么你不應該在條件語句或者循環(huán)語句中使用 Hooks 了嗎诸老?

因為我們維護了一個指針“cursor”指向一個數(shù)組尸闸,如果你改變了 render 函數(shù)內(nèi)部的調(diào)用順序,那么這個指針“cursor”將不會匹配到正確的數(shù)據(jù)孕锄,你的調(diào)用也將不會指向正確的數(shù)據(jù)或句柄

希望通過上面的兩個例子吮廉,為大家建立了一個關于 Hooks 的更加清晰的思維模型

另外,Dan也提到了幾個經(jīng)常有人提出的修改Hooks的方案畸肆,并對其缺陷進行了詳細闡述以佐證Hooks的設計是yyds宦芦!接著往下看

缺陷1:無法提取custom hook

有個替代方案是限制一個組件調(diào)用多次 useState(),你可以把 state 放在一個對象里轴脐,這樣還可以兼容 class 不是更好嗎调卑?

function Form() {
  const [state, setState] = useState({
    name: 'Mary',
    surname: 'Poppins',
    width: window.innerWidth,
  });
  // ...
}

Hooks 是允許這種風格寫的,你不必將 state 拆分成一堆 state 變量

但是useState的關鍵在于你可以從組件中提取出部分有狀態(tài)的邏輯(state + effect)到 custom hooks 中

function Form() {
  // 在組件內(nèi)直接定義一些 state 變量
  const [name, setName] = useState('Mary');
  const [surname, setSurname] = useState('Poppins');

  // 我們將部分 state 和 effects 移至 custom hook
  const width = useWindowWidth();
  // ...
}

function useWindowWidth() {
  // 在 custom hook 內(nèi)定義一些 state 變量
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    // ...
  });
  return width;
}

上面代碼大咱,可以將部分state(width)提取到自定義組件(useWindowWidth)中恬涧,如果你只允許每個組件調(diào)用一次useState(),你將失去用custom hook引入state能力碴巾,這就是custom hooks的關鍵

缺陷2:命名沖突

一個常見的建議是讓組件內(nèi) useState() 接收一個唯一標識 key 參數(shù)(string 等)區(qū)分 state 變量

看起來大致是這樣:

function Form() {
  // 我們傳幾種 state key 給 useState()
  const [name, setName] = useState('name');
  const [surname, setSurname] = useState('surname');
  const [width, setWidth] = useState('width');
  // ...

這試圖擺脫依賴順序調(diào)用(顯示 key)溯捆,但引入了另外一個問題 —— 命名沖突

而且,你可能無法在同一個組件調(diào)用兩次useState('name')厦瓢,比方說每當你在custom hook里添加一個新的state變量時提揍,就有可能破壞使用它的任何組件(直接或者間接),因為可能已經(jīng)有同名的變量位于組件內(nèi)

而通過一開始講到的Hooks提案煮仇,通過依賴順序調(diào)用來解決這個問題:即使兩個 Hooks都用name變量劳跃,它們也會彼此隔離,每次調(diào)用useState()都會獲得獨立的 「內(nèi)存單元」

缺陷3:同一個 Hook 無法調(diào)用兩次

給 useState 「加key」的另一種衍生提案是使用像 Symbol 這樣的東西浙垫,這樣就不沖突了對吧刨仑?

const nameKey = Symbol();
const surnameKey = Symbol();
const widthKey = Symbol();

function Form() {
  // 我們傳幾種state key給useState()
  const [name, setName] = useState(nameKey);
  const [surname, setSurname] = useState(surnameKey);
  const [width, setWidth] = useState(widthKey);
  // ...

這個提案看起來好像有利于提取state到custom hook當中

function Form() {
  // ...
  const width = useWindowWidth();
  // ...
}

/*********************
 * useWindowWidth.js *
 ********************/
const widthKey = Symbol();
 
function useWindowWidth() {
  const [width, setWidth] = useState(widthKey);
  // ...
  return width;
}

但是如果多次調(diào)用,例如:

function Form() {
  // ...
  const name = useFormInput();
  const surname = useFormInput();
  // ...
  return (
    <>
      <input {...name} />
      <input {...surname} />
      {/* ... */}
    </>    
  )
}

/*******************
 * useFormInput.js *
 ******************/
const valueKey = Symbol();
 
function useFormInput() {
  const [value, setValue] = useState(valueKey);
  return {
    value,
    onChange(e) {
      setValue(e.target.value);
    },
  };
}

我們調(diào)用 useFormInput() 兩次夹姥,但 useFormInput() 總是用同一個 key 調(diào)用 useState()杉武,就像這樣:

const [name, setName] = useState(valueKey);
const [surname, setSurname] = useState(valueKey);

又又又又發(fā)生了沖突:)
而Hooks提案沒有這種問題,因為每次 調(diào)用useState()會獲得單獨的state(狀態(tài)不與其他組件共享)佃声。且依賴于固定順序調(diào)用使我們免于擔心命名沖突

缺陷4:鉆石問題(多層繼承問題)

比如useWindowWidth()useNetworkStatus()這兩個custom hooks可能要用像 useSubscription() 這樣的 custom hook艺智,如下:

function StatusMessage() {
  const width = useWindowWidth();
  const isOnline = useNetworkStatus();
  return (
    <>
      <p>Window width is {width}</p>
      <p>You are {isOnline ? 'online' : 'offline'}</p>
    </>
  );
}

function useSubscription(subscribe, unsubscribe, getValue) {
  const [state, setState] = useState(getValue());
  useEffect(() => {
    const handleChange = () => setState(getValue());
    subscribe(handleChange);
    return () => unsubscribe(handleChange);
  });
  return state;
}

function useWindowWidth() {
  const width = useSubscription(
    handler => window.addEventListener('resize', handler),
    handler => window.removeEventListener('resize', handler),
    () => window.innerWidth
  );
  return width;
}

function useNetworkStatus() {
  const isOnline = useSubscription(
    handler => {
      window.addEventListener('online', handler);
      window.addEventListener('offline', handler);
    },
    handler => {
      window.removeEventListener('online', handler);
      window.removeEventListener('offline', handler);
    },
    () => navigator.onLine
  );
  return isOnline;
}

嵌套+嵌套+嵌套 = ??

       / useWindowWidth()   \                   / useState()  ?? Clash
Status                        useSubscription() 
       \ useNetworkStatus() /                   \ useEffect() ?? Clash

而固定順序調(diào)用的話

                                                / useState()  ? #1. State
       / useWindowWidth()   -> useSubscription()                    
      /                                          \ useEffect() ? #2. Effect
Status                         
      \                                          / useState()  ? #3. State
       \ useNetworkStatus() -> useSubscription()
                                                 \ useEffect() ? #4. Effect

缺陷5:復制粘貼的主意被打亂

或許我們可以通過引入某種命名空間來挽救給 state 加「key」提議,有幾種不同的方法可以做到這一點

一種方法是使用閉包隔離 state 的 key圾亏,這需要你在 「實例化」 custom hooks時給每個 hook 裹上一層 function:

/*******************
 * useFormInput.js *
 ******************/
function createUseFormInput() {
  // 每次實例化都唯一
  const valueKey = Symbol();  

  return function useFormInput() {
    const [value, setValue] = useState(valueKey);
    return {
      value,
      onChange(e) {
        setValue(e.target.value);
      },
    };
  }
}

可是要知道上面實例代碼只是一個input組件十拣,但是可以看到封拧,它的代碼已經(jīng)很重了,真實的React App由多個類按照層級夭问,一層層構成泽西,復雜度成倍增長,嵌套地獄缰趋。

而Hooks 的設計目標之一就是避免使用高階組件和render props的深層嵌套函數(shù)

而且不得不操作兩次才能使組件用上custom hook

// 我們不得不在使用任何custom hook時進實例化
const useNameFormInput = createUseFormInput();
const useSurnameFormInput = createUseFormInput();

function Form() {
  // ...
  // 還有一次是最終的調(diào)用
  const name = useNameFormInput();
  const surname = useNameFormInput();
  // ...
}

這意味著即使一個很小的改動捧杉,你也得在頂層聲明和render函數(shù)間來回跳轉

你還需要非常精確的命名,總是需要考慮「兩層」命名 —— 像 createUseFormInput 這樣的工廠函數(shù)和 useNameFormInput秘血、useSurnameFormInput這樣的實例 Hooks

參考資料:
Why Do React Hooks Rely on Call Order?
(譯)React hooks:它不是一種魔法味抖,只是一個數(shù)組——使用圖表揭秘提案規(guī)則
Rules of Hooks
RFC: React Hooks

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市灰粮,隨后出現(xiàn)的幾起案子仔涩,更是在濱河造成了極大的恐慌,老刑警劉巖粘舟,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件熔脂,死亡現(xiàn)場離奇詭異,居然都是意外死亡柑肴,警方通過查閱死者的電腦和手機霞揉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來晰骑,“玉大人适秩,你說我怎么就攤上這事⌒┦蹋” “怎么了隶症?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長岗宣。 經(jīng)常有香客問我,道長淋样,這世上最難降的妖魔是什么耗式? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮趁猴,結果婚禮上刊咳,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布琼开。 她就那樣靜靜地躺著桂塞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪锌历。 梳的紋絲不亂的頭發(fā)上希坚,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天殴穴,我揣著相機與錄音柴钻,去河邊找鬼淮韭。 笑死,一個胖子當著我的面吹牛贴届,可吹牛的內(nèi)容都是我干的靠粪。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼毫蚓,長吁一口氣:“原來是場噩夢啊……” “哼占键!你這毒婦竟也來了?” 一聲冷哼從身側響起元潘,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤捞慌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后柬批,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體啸澡,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年氮帐,在試婚紗的時候發(fā)現(xiàn)自己被綠了嗅虏。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡上沐,死狀恐怖皮服,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情参咙,我是刑警寧澤龄广,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站蕴侧,受9級特大地震影響择同,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜净宵,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一敲才、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧择葡,春花似錦紧武、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至已添,卻和暖如春妥箕,著一層夾襖步出監(jiān)牢的瞬間滥酥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工矾踱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留恨狈,地道東北人。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓呛讲,卻偏偏與公主長得像禾怠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子贝搁,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容