1. 程式人生 > >React Hooks 實現和由來以及解決的問題

React Hooks 實現和由來以及解決的問題

與React類元件相比,React函式式元件究竟有何不同?

一般的回答都是:

  1. 類元件比函式式元件多了更多的特性,比如 state,那如果有 Hooks 之後呢?
  2. 函式元件效能比類元件好,但是在現代瀏覽器中,閉包和類的原始效能只有在極端場景下才會有明顯的差別。
    1. 效能主要取決於程式碼的作用,而不是選擇函式式還是類元件。儘管優化策略有差別,但效能差異可以忽略不計。
    2. 參考官網:(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的函式式元件和類元件之間根本的區別: 在心智模型上。

簡單的案例

函式式元件以來,它一直存在,但是經常被忽略:函式式元件捕獲了渲染所用的值。(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

如果是類元件我們怎麼寫?一個簡單的重構可能就象這樣:

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

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

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

通常我們認為,這兩個程式碼片段是等效的。人們經常在這兩種模式中自由的重構程式碼,但是很少注意到它們的含義:

我們通過 React 應用程式中的一個常見錯誤來說明其中的不同。

我們新增一個父元件,用一個下拉框來更改傳遞給子元件(ProfilePage),的 props.user,例項地址:(https://codesandbox.io/s/pjqnl16lm7) 。

按步驟完成以下操作:

  1. 點選 其中某一個 Follow 按鈕。
  2. 在3秒內 切換 選中的賬號。
  3. 檢視 彈出的文字。

這時會得到一個奇怪的結果:

  • 當使用 函式式元件 實現的 ProfilePage, 當前賬號是 Dan 時點選 Follow 按鈕,然後立馬切換當前賬號到 Sophie,彈出的文字將依舊是 'Followed Dan'
  • 當使用 類元件 實現的 ProfilePage, 彈出的文字將是 'Followed Sophie'

在這個例子中,函式元件是正確的。 如果我關注一個人,然後導航到另一個人的賬號,我的元件不應該混淆我關注了誰。 ,而類元件的實現很明顯是錯誤的。

案例解析

所以為什麼我們的例子中類元件會有這樣的表現? 讓我們仔細看看類元件中的 showMessage 方法:

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

這個類方法從 this.props.user 中讀取資料。

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

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

所以如果在請求已經發出的情況下我們的元件進行了重新渲染, this.props將會改變。 showMessage方法從一個"過於新"的 props中得到了 user

從 this 中讀取資料的這種行為,呼叫一個回撥函式讀取 this.props 的 timeout 會讓 showMessage 回撥並沒有與任何一個特定的渲染"繫結"在一起,所以它"失去"了正確的 props。。

如何用類元件解決上述BUG?(假設函式式元件不存在)

我們想要以某種方式"修復"擁有正確 props 的渲染與讀取這些 props 的 showMessage回撥之間的聯絡。在某個地方 props被弄丟了。

方法一:在呼叫事件之前讀取 this.props,然後顯式地傳遞到timeout回撥函式中:
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 呼叫了另一個方法,然後那個方法中讀取了 this.props.something 或者 this.state.something ,我們又將遇到同樣的問題。然後我們不得不將 this.propsthis.state以函式引數的形式在被 showMessage呼叫的每個方法中一路傳遞下去。

這樣的做法破壞了類提供的工程學。同時這也很難讓人去記住傳遞的變數或者強制執行,這也是為什麼人們總是在解決bugs。

這個問題可以在任何一個將資料放入類似 this 這樣的可變物件中的UI庫中重現它(不僅只存在 React 中)

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

如果你在一次特定的渲染中捕獲那一次渲染所用的props或者state,你會發現他們總是會保持一致,就如同你的預期那樣:

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

你在渲染的時候就已經"捕獲"了props:。這樣,在它內部的任何程式碼(包括 showMessage)都保證可以得到這一次特定渲染所使用的props。

Hooks 的由來

但是:如果你在 render方法中定義各種函式,而不是使用class的方法,那麼使用類的意義在哪裡?

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

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

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

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

就像上面這樣, props仍舊被捕獲了 —— React將它們作為引數傳遞。 不同於 thisprops 物件本身永遠不會被React改變。

當父元件使用不同的props來渲染 ProfilePage時,React會再次呼叫 ProfilePage函式。但是我們點選的事件處理函式,"屬於"具有自己的 user值的上一次渲染,並且 showMessage回撥函式也能讀取到這個值。它們都保持完好無損。

這就是為什麼,在上面那個的函式式版本中,點選關注賬號1,然後改變選擇為賬號2,仍舊會彈出 'Followed 賬號1'

函式式元件捕獲了渲染所使用的值。

使用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>
  </>;
}

如果我傳送一條特定的訊息,元件不應該對實際傳送的是哪條訊息感到困惑。這個函式元件的 message變數捕獲了"屬於"返回了被瀏覽器呼叫的單擊處理函式的那一次渲染。所以當我點選"傳送"時 message被設定為那一刻在input中輸入的內容。

讀取最新的狀態

因此我們知道,在預設情況下React中的函式會捕獲props和state。 但是如果我們想要讀取並不屬於這一次特定渲染的,最新的props和state呢?如果我們想要["從未來讀取他們"]呢?

在類中,你通過讀取 this.props或者 this.state來實現,因為 this本身時可變的。React改變了它。在函式式元件中,你也可以擁有一個在所有的元件渲染幀中共享的可變變數。它被成為"ref":

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

}

但是,你必須自己管理它。

一個ref與一個例項欄位扮演同樣的角色。這是進入可變的命令式的世界的後門。你可能熟悉'DOM refs',但是ref在概念上更為廣泛通用。它只是一個你可以放東西進去的盒子。

甚至在視覺上, this.something就像是 something.current的一個映象。他們代表了同樣的概念。

預設情況下,React不會在函式式元件中為最新的props和state創造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,我們將得到在我們按下發送按鈕那一刻的資訊。但是當我們讀取 latestMessage.current,我們將得到最新的值 —— 即使我們在按下發送按鈕後繼續輸入。

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

通常情況下,你應該避免在渲染期間讀取或者設定refs,因為它們是可變得。我們希望保持渲染的可預測性。 然而,如果我們想要特定props或者state的最新值,那麼手動更新ref會有些煩人。我們可以通過使用一個effect來自動化實現它:

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

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

我們在一個effect 內部執行賦值操作以便讓ref的值只會在DOM被更新後才會改變。這確保了我們的變數突變不會破壞依賴於可中斷渲染的時間切片和 Suspense 等特性。

通常來說使用這樣的ref並不是非常地必要。 捕獲props和state通常是更好的預設值。 然而,在處理類似於intervals和訂閱這樣的命令式API時,ref會十分便利。你可以像這樣跟蹤 任何值 —— 一個prop,一個state變數,整個props物件,或者甚至一個函式。

這種模式對於優化來說也很方便 —— 例如當 useCallback本身經常改變時。然而,使用一個reducer 通常是一個更好的解決方式

閉包幫我們解決了很難注意到的細微問題。同樣,它們也使得在併發模式下能更輕鬆地編寫能夠正確執行的程式碼。這是可行的,因為元件內部的邏輯在渲染它時捕獲幷包含了正確的props和state。

函式捕獲了他們的props和state —— 因此它們的標識也同樣重要。這不是一個bug,而是一個函式式元件的特性。例如,對於 useEffect或者 useCallback來說,函式不應該被排除在"依賴陣列"之外。(正確的解決方案通常是使用上面說過的 useReducer或者 useRef

當我們用函式來編寫大部分的React程式碼時,我們需要調整關於優化程式碼和什麼變數會隨著時間改變的認知與直覺。

到目前為止,我發現的有關於hooks的最好的心裡規則是"寫程式碼時要認為任何值都可以隨時更改"。

React函式總是捕獲他們的值 —— 現在我們也知道這是為什麼了。

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

最後

  1. 譯者寫了一個 React + Hooks 的 UI 庫,方便大家學習和使用, (https://github.com/zhongmeizhi/z-ui)
  2. 歡迎關注公眾號「前端進階課」認真學前端,一起進階。

相關推薦

React Hooks 實現由來以及解決的問題

與React類元件相比,React函式式元件究竟有何不同? 一般的回答都是: 類元件比函式式元件多了更多的特性,比如 state,那如果有 Hooks 之後呢? 函式元件效能比類元件好,但是在現代瀏覽器中,閉包和類的原始效能只有在極端場景下才會有明顯的差別。 效能主要取決於程式碼的作用,而不是選擇函式式還

React Hooks實現非同步請求例項—useReducer、useContextuseEffect代替Redux方案

本文是學習了2018年新鮮出爐的React Hooks提案之後,針對 非同步請求資料寫的一個案例。注意,本文假設了: 1.你已經初步瞭解 hooks的含義了,如果不瞭解還請移步 官方文件。(其實有過翻譯的想法,不過印記中文一直在翻譯,就是比較慢啦) 2.你使用 Redux實現過非同步 Act

CentOS7下jenkins遷移升級以及解決磁盤空間滿的問題下

done init.d pidfile mtime 磁盤空間 查找 centos -o root jenkins遷移和升級 查看jenkins安裝包以及路徑[root@Jenkins ~]# rpm -ql jenkins/etc/init.d/jenkins/etc/lo

react中constructorsuper()以及super(props)的區別。

事件 但是 你在 返回 name 定義 創建 ren 子類 react中這兩個API出鏡率超級高,但是一直不太懂這到底是幹嘛的,有什麽用;今天整理一下,方便自己查看同時方便大家。 1.constructor( )-----super( )的基本含義 constr

javascript中bind()函式實現應用以及多次bind的結果引數位置的思考

改變物件方法裡this的值var ob = { name: 'joe', getName: function () { alert(this.name); } }; // 改變getName方法裡原本的this物件為新物件{name: 'haha'} var app = ob.getName.bi

Vue使用Axios實現http請求以及解決跨域問題

Axios 是一個基於 promise 的 HTTP 庫,可以用在瀏覽器和 node.js 中。Axios的中文文件以及github地址如下: 一、安裝Axios外掛 npm install axios --save 二、在main.js中引入Axios庫

day25之布隆過濾器的實現優缺點以及擴充套件

布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進位制向量和一系列隨機對映函式。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的演算法,缺點是有一定的誤識別率和刪除困難。 程式碼

JSP內建物件(包括requestresponse)以及解決url傳中文引數出現亂碼問題

一、內建物件簡介1、JSP內建物件是Web容器建立的一組物件,不使用new關鍵字就可以使用的內建物件。例如:out物件 <% int[] value={60,70,80}; for(int i:value){ out.println

Linux安裝nodejsnpm以及解決npm install過慢問題

  最近要裝的東西有點多,由於要為elastic search安裝head外掛需要nodejs和npm,所以這裡記錄下安裝過程,方便回看同時供大家參考。在Linux下裝東西就是麻煩,要進行各種配置檔案,好了不扯淡了,這裡用的是Centos7,使用的node是6.

內存溢出內存泄漏的區別、產生原因以及解決方案 轉

服務 har 操作 ger 遞歸調用 問題 let share 查錯 內存溢出 out of memory,是指程序在申請內存時,沒有足夠的內存空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就是內存溢出。 內

內存溢出內存泄漏的區別,產生原因以及解決方案

解決方案 集合類 釋放內存 分頁 需求 查看內存 取出 程序 tof 一、概念與區別 內存溢出 out of memory,是指程序在申請內存時,沒有足夠的內存空間供其使用,出現out of memory;比如申請 了一個integer,但給它存了long才能存下的數,那就

史上最大CPU缺陷Meltdown融毀Spectre幽靈來襲,各網絡設備廠家反饋以及解決方案匯總

dir ase agg 部分 arm 暴露 ace ado shadow 2018新年快樂 新年好,轉眼就到了2018。首先祝福大家新年快樂,萬事如意! 熱鬧的一月 就在大家剛享受完短暫的元旦假期,1月3號互聯網上就爆出了一個非常勁爆的消息,Intel,AMD,ARM的CP

Django 【第十九篇】JS實現的ajax、同源策略前端jsonp解決跨域問題

學習 tab頁 hello shortcuts 就會 coo 功能介紹 onclick decorator 一、回顧jQuery實現的ajax 首先說一下ajax的優缺點 優點: AJAX使用Javascript技術向服務器發送異步請求; AJAX無須刷新整個頁面;

使用React的static方法實現同構以及同構的常見問題

fonts eap 細致 tput isp shee 模塊 system device 代碼地址請在github查看,假設有新內容。我會定時更新。也歡迎您star,i

記憶體溢位記憶體洩漏的區別、產生原因以及解決方案【轉】

(轉自:https://www.cnblogs.com/Sharley/p/5285045.html) 記憶體溢位 out of memory,是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就

穆迪分析與普華永道合作提供“端到端”的精算、會計業務專業解決方案,以協助保險公司實現IFRS 17的合規

倫敦 -- (美國商業資訊) -- 穆迪分析與普華永道今日宣佈,雙方將合作向市場提供一流的技術、實施和諮詢解決方案,以在全球範圍內協助保險公司應對《國際財務報告準則第17號: 保險合同》(IFRS17)。 普華永道全球保險業IFRS 17部門的主管Alex Bertolotti表示:“保險公

關於mac上操作nginx的命令以及遇到的問題對應的解決辦法

1、mac上查詢nginx安裝位置 在終端輸入: nginx -V 檢視nginx版本及安裝的本地位置 ngxin -v 檢視nginx版本(此方法依然可以檢測是否安裝某一軟體,如git,hg等) 2、在Mac上用brew安裝Nginx,然後修改Nginx配置檔案,再重啟時報出如下錯

使用註解方式實現Dubbo搭建,解決消費者呼叫接口出現空指標異常以及事務等問題

    最近接手了一個對之前專案進行重構的任務,使用dubbo+maven進行整合,並且將hibernate全部改成JdbcTemplate。主流還是使用springMVC+spring進行開發。 按照之前經驗,使用xml配置方式,釋出服務到zookeeper,成功執

Spring的AOP(xml註解實現AOP,以及代理模式)

AOP術語:    連線點(Joinpoint):程式執行的某一個特定位置,如類初始前後,方法的執行前後。而Spring只支援方法的連線點。     切點(Pointcut):切點可以定位到相應的連線點,一個切點可以定位多個連線點。&

通過jndi程式設計rmi實現操作檔案的實現 以及jndi的spi實現

     JNDI是java提供的命名介面服務,需要第三方公司按照SPI所提供的服務實現,FSSP的官方資源包下載地址;http://www.oracle.com/technetwork/java/javasebusiness/downloads/java-archive-downloa