React Hooks 實現(xiàn)和由來以及解決的問題

與React類組件相比车柠,React函數(shù)式組件究竟有何不同剔氏?

一般的回答都是:

  1. 類組件比函數(shù)式組件多了更多的特性,比如 state竹祷,那如果有 Hooks 之后呢谈跛?
  2. 函數(shù)組件性能比類組件好,但是在現(xiàn)代瀏覽器中塑陵,閉包和類的原始性能只有在極端場景下才會有明顯的差別感憾。
    1. 性能主要取決于代碼的作用,而不是選擇函數(shù)式還是類組件令花。盡管優(yōu)化策略有差別阻桅,但性能差異可以忽略不計。
    2. 參考官網(wǎng):(https://zh-hans.reactjs.org/docs/hooks-faq.html#are-hooks-slow-because-of-creating-functions-in-render)
    3. 參考作者github:(https://github.com/ryardley/hooks-perf-issues/pull/2)

而下面會重點講述:React的函數(shù)式組件和類組件之間根本的區(qū)別: 在心智模型上兼都。

簡單的案例

函數(shù)式組件以來嫂沉,它一直存在,但是經(jīng)常被忽略:函數(shù)式組件捕獲了渲染所用的值扮碧。(Function components capture the rendered values.)

思考這個組件:

function ProfilePage(props) {
  const showMessage = () => alert('你好 ' + props.user);

  const handleClick = () => setTimeout(showMessage, 3000);

  return <button onClick={handleClick}>Follow</button>
}

上述組件:如果 props.userDan趟章,它會在三秒后顯示 你好 Dan

如果是類組件我們怎么寫慎王?一個簡單的重構(gòu)可能就象這樣:

class ProfilePage extends React.Component {
  showMessage = () => alert('Followed ' + this.props.user);

  handleClick = () => setTimeout(this.showMessage, 3000);

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

通常我們認為蚓土,這兩個代碼片段是等效的。人們經(jīng)常在這兩種模式中自由的重構(gòu)代碼赖淤,但是很少注意到它們的含義:

我們通過 React 應(yīng)用程序中的一個常見錯誤來說明其中的不同蜀漆。

我們添加一個父組件,用一個下拉框來更改傳遞給子組件(ProfilePage)咱旱,的 props.user确丢,實例地址:(https://codesandbox.io/s/pjqnl16lm7) 绷耍。

按步驟完成以下操作:

  1. 點擊 其中某一個 Follow 按鈕。
  2. 在3秒內(nèi) 切換 選中的賬號蠕嫁。
  3. 查看 彈出的文本锨天。

這時會得到一個奇怪的結(jié)果:

  • 當使用 函數(shù)式組件 實現(xiàn)的 ProfilePage, 當前賬號是 Dan 時點擊 Follow 按鈕,然后立馬切換當前賬號到 Sophie剃毒,彈出的文本將依舊是 'Followed Dan'病袄。
  • 當使用 類組件 實現(xiàn)的 ProfilePage, 彈出的文本將是 'Followed Sophie'

在這個例子中,函數(shù)組件是正確的赘阀。 如果我關(guān)注一個人益缠,然后導(dǎo)航到另一個人的賬號,我的組件不應(yīng)該混淆我關(guān)注了誰基公。 幅慌,而類組件的實現(xiàn)很明顯是錯誤的。

案例解析

所以為什么我們的例子中類組件會有這樣的表現(xiàn)轰豆? 讓我們仔細看看類組件中的 showMessage 方法:

  showMessage = () => {
    alert('Followed ' + this.props.user);
  };

這個類方法從 this.props.user 中讀取數(shù)據(jù)胰伍。

  1. 在 React 中 Props 是 不可變(immutable)的,所以他們永遠不會改變酸休。
  2. this 是而且永遠是 可變(mutable)的骂租。**

這也是類組件 this 存在的意義:能在渲染方法以及生命周期方法中得到最新的實例。

所以如果在請求已經(jīng)發(fā)出的情況下我們的組件進行了重新渲染斑司, this.props將會改變渗饮。 showMessage方法從一個"過于新"的 props中得到了 user

從 this 中讀取數(shù)據(jù)的這種行為宿刮,調(diào)用一個回調(diào)函數(shù)讀取 this.props 的 timeout 會讓 showMessage 回調(diào)并沒有與任何一個特定的渲染"綁定"在一起互站,所以它"失去"了正確的 props。僵缺。

如何用類組件解決上述BUG胡桃?(假設(shè)函數(shù)式組件不存在)

我們想要以某種方式"修復(fù)"擁有正確 props 的渲染與讀取這些 props 的 showMessage回調(diào)之間的聯(lián)系。在某個地方 props被弄丟了磕潮。

方法一:在調(diào)用事件之前讀取 this.props标捺,然后顯式地傳遞到timeout回調(diào)函數(shù)中:
class ProfilePage extends React.Component {
  showMessage = (user) => alert('Followed ' + user);

  handleClick = () => {
    const {user} = this.props;
    setTimeout(() => this.showMessage(user), 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Followbutton>;
  }
}

然而,這種方法使得代碼明顯變得更加冗長揉抵。如果我們需要的不止是一個props 該怎么辦? 如果我們還需要訪問state 又該怎么辦嗤疯? 如果 showMessage 調(diào)用了另一個方法冤今,然后那個方法中讀取了 this.props.something 或者 this.state.something ,我們又將遇到同樣的問題茂缚。然后我們不得不將 this.propsthis.state以函數(shù)參數(shù)的形式在被 showMessage調(diào)用的每個方法中一路傳遞下去戏罢。

這樣的做法破壞了類提供的工程學(xué)屋谭。同時這也很難讓人去記住傳遞的變量或者強制執(zhí)行,這也是為什么人們總是在解決bugs龟糕。

這個問題可以在任何一個將數(shù)據(jù)放入類似 this 這樣的可變對象中的UI庫中重現(xiàn)它(不僅只存在 React 中)

方法二:如果我們能利用JavaScript閉包的話問題將迎刃而解桐磁。*

如果你在一次特定的渲染中捕獲那一次渲染所用的props或者state,你會發(fā)現(xiàn)他們總是會保持一致讲岁,就如同你的預(yù)期那樣:

class ProfilePage extends React.Component {
  render() {
    const props = this.props;

    const showMessage = () => {
      alert('Followed ' + props.user);
    };

    const handleClick = () => {
      setTimeout(showMessage, 3000);
    };

    return <button onClick={handleClick}>Follow</button>;
  }
}

你在渲染的時候就已經(jīng)"捕獲"了props:我擂。這樣,在它內(nèi)部的任何代碼(包括 showMessage)都保證可以得到這一次特定渲染所使用的props缓艳。

Hooks 的由來

但是:如果你在 render方法中定義各種函數(shù)校摩,而不是使用class的方法,那么使用類的意義在哪里阶淘?

事實上衙吩,我們可以通過刪除類的"包裹"來簡化代碼:

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

就像上面這樣, props仍舊被捕獲了 —— React將它們作為參數(shù)傳遞溪窒。 不同于 this 坤塞, props 對象本身永遠不會被React改變。

當父組件使用不同的props來渲染 ProfilePage時澈蚌,React會再次調(diào)用 ProfilePage函數(shù)摹芙。但是我們點擊的事件處理函數(shù),"屬于"具有自己的 user值的上一次渲染惜浅,并且 showMessage回調(diào)函數(shù)也能讀取到這個值瘫辩。它們都保持完好無損。

這就是為什么坛悉,在上面那個的函數(shù)式版本中伐厌,點擊關(guān)注賬號1,然后改變選擇為賬號2裸影,仍舊會彈出 'Followed 賬號1'

函數(shù)式組件捕獲了渲染所使用的值挣轨。

使用Hooks,同樣的原則也適用于state轩猩。 看這個例子:

function MessageThread() {
  const [message, setMessage] = useState('');

  const showMessage = () => {
    alert('You said: ' + message);
  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
  };

  return <>
    <input value={message} onChange={handleMessageChange} />
    <button onClick={handleSendClick}>Send</button>
  </>;
}

如果我發(fā)送一條特定的消息卷扮,組件不應(yīng)該對實際發(fā)送的是哪條消息感到困惑。這個函數(shù)組件的 message變量捕獲了"屬于"返回了被瀏覽器調(diào)用的單擊處理函數(shù)的那一次渲染均践。所以當我點擊"發(fā)送"時 message被設(shè)置為那一刻在input中輸入的內(nèi)容晤锹。

讀取最新的狀態(tài)

因此我們知道,在默認情況下React中的函數(shù)會捕獲props和state彤委。 但是如果我們想要讀取并不屬于這一次特定渲染的鞭铆,最新的props和state呢?如果我們想要["從未來讀取他們"]呢焦影?

在類中车遂,你通過讀取 this.props或者 this.state來實現(xiàn)封断,因為 this本身時可變的。React改變了它舶担。在函數(shù)式組件中坡疼,你也可以擁有一個在所有的組件渲染幀中共享的可變變量。它被成為"ref":

function MyComponent() {
  const ref = useRef(null);

}

但是衣陶,你必須自己管理它柄瑰。

一個ref與一個實例字段扮演同樣的角色。這是進入可變的命令式的世界的后門祖搓。你可能熟悉'DOM refs'狱意,但是ref在概念上更為廣泛通用。它只是一個你可以放東西進去的盒子拯欧。

甚至在視覺上详囤, this.something就像是 something.current的一個鏡像。他們代表了同樣的概念镐作。

默認情況下藏姐,React不會在函數(shù)式組件中為最新的props和state創(chuàng)造refs。在很多情況下该贾,你并不需要它們羔杨,并且分配它們將是一種浪費。但是杨蛋,如果你愿意兜材,你可以這樣手動地來追蹤這些值:

function MessageThread() {
  const [message, setMessage] = useState('');
  const latestMessage = useRef('');
  const showMessage = () => {
    alert('You said: ' + latestMessage.current);  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
    latestMessage.current = e.target.value;  };

如果我們在 showMessage中讀取 message,我們將得到在我們按下發(fā)送按鈕那一刻的信息逞力。但是當我們讀取 latestMessage.current诉稍,我們將得到最新的值 —— 即使我們在按下發(fā)送按鈕后繼續(xù)輸入徘溢。

ref是一種"選擇退出"渲染一致性的方法,在某些情況下會十分方便。

通常情況下心褐,你應(yīng)該避免在渲染期間讀取或者設(shè)置refs肆糕,因為它們是可變得藻懒。我們希望保持渲染的可預(yù)測性却邓。 然而,如果我們想要特定props或者state的最新值峦嗤,那么手動更新ref會有些煩人蕊唐。我們可以通過使用一個effect來自動化實現(xiàn)它:

function MessageThread() {
  const [message, setMessage] = useState('');

  const latestMessage = useRef('');
  useEffect(() => {
    latestMessage.current = message;
  });
  const showMessage = () => {
    alert('You said: ' + latestMessage.current);
  };

我們在一個effect 內(nèi)部執(zhí)行賦值操作以便讓ref的值只會在DOM被更新后才會改變。這確保了我們的變量突變不會破壞依賴于可中斷渲染的時間切片和 Suspense 等特性烁设。

通常來說使用這樣的ref并不是非常地必要刃泌。 捕獲props和state通常是更好的默認值。 然而,在處理類似于intervals和訂閱這樣的命令式API時耙替,ref會十分便利。你可以像這樣跟蹤 任何值 —— 一個prop曹体,一個state變量俗扇,整個props對象,或者甚至一個函數(shù)箕别。

這種模式對于優(yōu)化來說也很方便 —— 例如當 useCallback本身經(jīng)常改變時铜幽。然而,使用一個reducer 通常是一個更好的解決方式

閉包幫我們解決了很難注意到的細微問題串稀。同樣除抛,它們也使得在并發(fā)模式下能更輕松地編寫能夠正確運行的代碼。這是可行的母截,因為組件內(nèi)部的邏輯在渲染它時捕獲并包含了正確的props和state到忽。

函數(shù)捕獲了他們的props和state —— 因此它們的標識也同樣重要。這不是一個bug清寇,而是一個函數(shù)式組件的特性喘漏。例如,對于 useEffect或者 useCallback來說华烟,函數(shù)不應(yīng)該被排除在"依賴數(shù)組"之外翩迈。(正確的解決方案通常是使用上面說過的 useReducer或者 useRef

當我們用函數(shù)來編寫大部分的React代碼時,我們需要調(diào)整關(guān)于優(yōu)化代碼什么變量會隨著時間改變的認知與直覺盔夜。

到目前為止负饲,我發(fā)現(xiàn)的有關(guān)于hooks的最好的心里規(guī)則是"寫代碼時要認為任何值都可以隨時更改"。

React函數(shù)總是捕獲他們的值 —— 現(xiàn)在我們也知道這是為什么了喂链。

文章參考:React作者 Dan Abramov 的github

最后

  1. 譯者寫了一個 React + Hooks 的 UI 庫返十,方便大家學(xué)習(xí)和使用, (https://github.com/zhongmeizhi/z-ui)
  2. 歡迎關(guān)注公眾號「前端進階課」認真學(xué)前端衩藤,一起進階吧慢。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赏表,隨后出現(xiàn)的幾起案子检诗,更是在濱河造成了極大的恐慌,老刑警劉巖瓢剿,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逢慌,死亡現(xiàn)場離奇詭異,居然都是意外死亡间狂,警方通過查閱死者的電腦和手機攻泼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人忙菠,你說我怎么就攤上這事何鸡。” “怎么了牛欢?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵骡男,是天一觀的道長。 經(jīng)常有香客問我傍睹,道長隔盛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任拾稳,我火速辦了婚禮吮炕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘访得。我一直安慰自己龙亲,他們只是感情好,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布震鹉。 她就那樣靜靜地躺著俱笛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪传趾。 梳的紋絲不亂的頭發(fā)上迎膜,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音浆兰,去河邊找鬼磕仅。 笑死,一個胖子當著我的面吹牛簸呈,可吹牛的內(nèi)容都是我干的榕订。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼蜕便,長吁一口氣:“原來是場噩夢啊……” “哼劫恒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起轿腺,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤两嘴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后族壳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體憔辫,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年仿荆,在試婚紗的時候發(fā)現(xiàn)自己被綠了贰您。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坏平。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖锦亦,靈堂內(nèi)的尸體忽然破棺而出舶替,到底是詐尸還是另有隱情,我是刑警寧澤孽亲,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布坎穿,位于F島的核電站,受9級特大地震影響返劲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜栖茉,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一篮绿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吕漂,春花似錦亲配、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至苍鲜,卻和暖如春思灰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背混滔。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工洒疚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坯屿。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓油湖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親领跛。 傳聞我的和親對象是個殘疾皇子乏德,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

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