React教程:元件,Hooks和效能

瘋狂的技術宅發表於2019-03-15

翻譯:瘋狂的技術宅

原文:www.toptal.com/react/react…

正如 我們的React教程的第一部分 中所指出的,開始使用 React 相對容易。首先使用 Create React App(CRA)初始化一個新專案,然後開始開發。不過遺憾的是,隨著時間的推移,程式碼可能會變得難以維護,特別是在你不熟悉 React 的情況下。元件有可能會變大,或者你可能最終得到一堆不是元件的元件,最終你可能會到處編寫重複的程式碼。

這時候你就應該試著開始真正的 React 之旅了 —— Think in React。

每當開發一個新的程式時,你需要為其做好在以後轉換為 React 應用的新設計,首先試著確定設計草圖中的元件,如何分離它們以使其更易於管理,以及哪些元素是重複的(或他們的行為)。儘量避免新增可能“將來有用”的程式碼 —— 雖然這很誘人,但可能未來永遠也不會到來,你將留下一堆具有大量可配置選項的多餘通用功能/元件。

React教程:React元件的插圖

此外,如果一個元件大於 2 到 3 個視窗的高度,也許值得分離(如果可能的話) —— 以後更容易閱讀。

React 中的受控元件與非受控元件

在大多數應用中,需要輸入和與使用者進行某種形式的互動,允許他們輸入內容、上傳檔案、選擇欄位等。 React 用兩種不同的方式處理使用者互動 —— 受控非受控元件。

顧名思義,受控元件的值由 React 控制,能為與使用者互動的元素提供值,而不受控制的元素不獲取值屬性。多虧了這一點,我們才能把 React 狀態作為單一的事實來源,因此我們在螢幕上看到的與當前擁有的狀態是一致的。開發人員需要傳遞一個函式,該函式用來響應使用者與表單的互動,這將會改變它的狀態。

class ControlledInput extends React.Component {
 state = {
   value: ""
 };

 onChange = (e) => this.setState({ value: e.target.value });

 render() {
   return (
     <input value={this.state.value} onChange={this.onChange}/>
   );
 }
}
複製程式碼

在 React 的非受控元件中,我們不關心值的變化情況,如果想要知道其確切的值,只需通過 ref 訪問它。

class UncontrolledInput extends React.Component {
 input = React.createRef();

 getValue = () => {
   console.log(this.input.current.value);
 };

 render() {
   return (
     <input ref={this.input}/>
   );
 }
}
複製程式碼

那麼應該怎麼選擇呢?在大數情況下用受控元件是可行的,不過也有一些例外。例如使用非受控制元件的一種情況是 file 型別輸入,因為它的值是隻讀的,不能在編碼中去設定(需要使用者互動)。另外我發現受控元件更容易理解和於使用。對受控元件的驗證是基於重新渲染的,狀態可以更改,並且可以很輕鬆的顯示輸入中存在的問題(例如格式錯誤或者輸入為空)。

Refs

在前面我們提到過 refs,這是一個特殊功能,可以在類元件中使用,直到 16.8 中出現了 hooks。

refs 可以通過引用讓開發人員訪問 React 元件或DOM元素(取決於我們附加 ref 的型別)。最好僅在必須的場景中使用它們,因為它們會使程式碼難以閱讀,並打破從上到下的資料流。然而,有些情況下它們是必要的,特別是在DOM元素上(例如:用編碼方式改變焦點)。附加到 React 元件元素時,你可以自由使用所引用的元件中的方法。不過還是應該避免這種做法,因為有更好的方法來處理它(例如,提升狀態並將功能移動到父元件)。

refs 還可以做到:

  • 使用字串字面量(歷史遺留的,應該避免),
  • 使用在 ref 屬性中設定的回撥函式,
  • 通過建立 ref 作為 React.createRef() ,並將其繫結到類屬性,並通過它去訪問(請注意,在 componentDidMount 生命週期中將提供引用)。

沒有傳遞引用的一種情況是當在元件上使用高階元件時 —— 原因是可以理解的,因為 ref 不是 prop(類似於 key)所以它沒有被傳遞下來,並且它將引用 HOC 而不是被它包裹的元件。在這種情況下,我們可以使用React.forwardRef,它把 props 和 ref 作為引數,然後可以將其分配給 prop 並傳遞給我們想要訪問的元件。

function withNewReference(Component) {
 class Hoc extends React.Component {
   render() {
     const {forwardedRef, ...props} = this.props;

     return <Component ref={forwardedRef} {...props}/>;
   }
 }

 return React.forwardRef((props, ref) => {
   return <Hoc {...props} forwardedRef={ref} />;
 });
}
複製程式碼

錯誤邊界

事情越複雜,出現問題的概率就越高。這就是為什麼 React 中會有錯誤邊界。那他們是怎麼工作的呢?

如果出現問題並且沒有錯誤邊界作為其父級,則會導致整個React 應用失敗。不顯示資訊比誤導使用者並顯示錯誤資訊要好,但這並不意味著你應該放任整個應用崩潰並顯示白屏。通過錯誤邊界,可以得到更多的靈活性。你可以在整個應用程式中使用並顯示一個錯誤訊息,或者在某些小部件中使用它但是不顯示,或者顯示少量資訊來代替這些小部件。

請記住,它僅涉及宣告性程式碼的問題,而不是你為了處理某些事件或者呼叫而編寫的命令式程式碼。對於這些情況,你仍應使用常規的 try/catch 方法。

在錯誤邊界也可以將資訊傳送到你使用的 Error Logger (在 componentDidCatch 生命週期方法中)。

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    logToErrorLogger(error, info);
  }

  render() {
    if (this.state.hasError) {
      return <div>Help, something went wrong.</div>;
    }

    return this.props.children; 
  }
}
複製程式碼

高階元件

**高階元件(HOC)**經常在 React 中被提及,這是一種非常流行的模式,你可能會用到它(或者已經在用了)。如果你熟悉 HOC,可能已經在很多庫中看到過 withNavigation,connect,withRouter

HOC 只是一種把元件作為引數的函式,並且與沒有 HOC 包裝器的元件相比,能夠返回具有擴充套件功能的新元件。多虧了這一點,你可以實現一些易於擴充套件的功能,以此增強自己的元件(例如:訪問導航)。 HOC 也有一些其它形式的呼叫方式,這取決於我們當前擁有什麼,唯一的引數必須要傳入一個元件,但它也可以接受額外的引數 —— 一些選項,或者像在 connect 中一樣,首先使用configurations呼叫一個函式,該函式稍後返回一個帶參元件,並返回 HOC 。

以下是一些你應該做的和要避免做的事情:

  • 為包裝器 HOC 函式新增顯示名稱(這樣你就能知道它到底是幹什麼用的,實際上是通過更改 HOC 元件顯示名稱來做到)。
  • 不要在渲染方法中使用HOC —— 你應該在其中使用增強元件,而不是在那裡建立新的 HOC 元件,因為它一直在重新裝載並丟失其當前狀態。
  • 靜態方法不會被自動複製,所以如果你想在新建立的 HOC 中使用一些靜態方法,需要自己去複製它們。
  • 涉及到的 Refs 不會被傳遞,所以使用前面提到的 React.forwardRef 來解決這些問題。
export function importantHoc() {
   return (Component) => class extends React.Component {
       importantFunction = () => {
           console.log("Very Important Function");
       };

       render() {
           return (
               <Component
                   {...this.props}
                   importantFunction={this.importantFunction}
               />
           );
       }
   };
}
複製程式碼

樣式

樣式不一定與 React 本身有關,但出於各種原因還是值得一提的。

首先,常規 CSS/內聯樣式在這裡能夠正常應用,你只需在 className 屬性中新增 CSS 中的類名,它就能正常工作。內聯樣式與常規 HTML 樣式略有不同。樣式屬性也是使用駝峰命名法,因此 border-radius 會變成 borderRadius 。

React 似乎推廣了一些不僅在 React 中變得普遍的解決方案,例如最近整合在 CRA 中的 CSS 模組,你可以在其中簡單地匯入 name.modules.css 並用其屬性來調整元件的樣式(某些IDE(例如WebStorm)也具有自動完成功能,能告訴你可用的名稱。

在 React 中另一個流行的解決方案是 CSS-in-JS(例如,emotion 庫)。再說一點,CSS 模組和 emotion(或者一般來說是CSS-in-JS)對 React 沒有限制

React 中的 Hooks

自重寫以來,**Hooks **很可能是 React 最受熱切期待的補充。這個產品是否能不負眾望?從我的角度來看,是的,因為它確實是一個很棒的功能。它們本質上是帶來了新的體驗,例如:

  • 允許刪除許多 class 元件,這些元件我們僅僅是使用而不歸我們擁有,例如本地狀態或 ref,所以元件的程式碼看上去更容易閱讀。
  • 可以讓你用更少的程式碼來獲得相同的效果。
  • 使函式更容易理解和測試,例如:用 react-testing-library
  • 也可以攜帶引數,一個 hook 返回的結果可以很容易地被另一個 hook 使用(例如,useEffect 中的 setStateuseState 使用)。
  • 比類更好地縮小方式,這對於 minifiers 來說往往更成問題。
  • 可能會刪除 HOC 並在你的應用中渲染 props ,儘管 hook 被設計用於解決其他問題,但仍會引入新問題。
  • 能夠被熟練的React開發人員定製

預設的 React hook 很少。其中三個基本的hook是 useStateuseEffectuseContext。還有一些其它的,例如 useRefuseMemo,不過現在我們把重點放在基礎知識上。

先看一下 useState,讓我們用它來建立一個簡單的計數器的。它是如何工作的?基本上整個結構非常簡單:

export function Counter() {
 const [counter, setCounter] = React.useState(0);

 return (
   <div>
     {counter}
     <button onClick={() => setCounter(counter + 1)}>+</button>
   </div>
 );
};
複製程式碼

它用 initialState (值)呼叫,並返回一個帶有兩個元素的陣列。由於陣列解構分配,我們可以立即將變數分配給這些元素。第一個是更新後的最後一個狀態,而另一個是我們將用於更新值的函式。看起來相當容易,不是嗎?

此外,由於這些元件曾經被稱為無狀態功能元件,現在這種名稱不再適用,因為它們可以具有如上所示的狀態。所以叫類元件函式元件似乎更符合它們的實際操作,至少從16.8.0開始。

更新函式(在我們的例子中是setCounter)也可以用作一個函式,它將以前的值作為引數,格式如下:

<button onClick={() => setCounter(prevCounter => prevCounter + 1)}>+</button>
<button onClick={() => setCounter(prevCounter => prevCounter - 1)}>-</button>
複製程式碼

與執行淺合併的this.setState 類元件不同,設定函式(在我們的例子中為 setCounter )會覆蓋整個狀態。

另外,initialState 也可以是一個函式,而不僅僅是一個普通的值。這有其自身的好處,因為該函式將會只在元件的初始渲染期間執行,之後將不再被呼叫。

const [counter, setCounter] = useState(() =>  calculateComplexInitialValue());
複製程式碼

最後,如果我們要使用 setCounter 與在當前狀態(counter)的同一時刻完全相同的值,那麼元件 將不會 重新渲染。

另一方面,useEffect 為我們的功能元件新增副作用,無論是訂閱、API呼叫、計時器、還是任何我們認為有用的東西。我們傳給 useEffect 的任何函式都將在 render 之後執行,並且是在每次渲染之後執行,除非我們新增一個限制,把應該重新執行時需要更改的屬性作為函式的第二個引數。如果我們只想在 mount 上執行它並在unmount 上清理,那麼只需要在其中傳遞一個空陣列。

const fetchApi = async () => {
 const value = await fetch("https://jsonplaceholder.typicode.com/todos/1");
 console.log(await value.json());
};

export function Counter() {
 const [counter, setCounter] = useState(0);
 useEffect(() => {
   fetchApi();
 }, []);


 return (
   <div>
     {counter}
     <button onClick={() => setCounter(prevCounter => prevCounter + 1)}>+</button>
     <button onClick={() => setCounter(prevCounter => prevCounter - 1)}>-</button>
   </div>
 );
};
複製程式碼

由於把空陣列作為第二個引數,所以上面的程式碼只執行一次。在這種情況下它類似於 componentDidMount,但稍後會觸發它。如果你想在瀏覽器處理之前呼叫一個類似的 hook,可以用 useLayoutEffect,但這些更新將會被同步應用,這一點與 useEffect 不同。

useContext 似乎是最容易理解的,因為我們提供了想要訪問的上下文(由 createContext 函式返回的物件提供),而它為我們提供了該上下文的值。

const context = useContext(Context);
複製程式碼

最後,要編寫自己的hook,你可以像這樣寫:

function useWindowWidth() {
 let [windowWidth, setWindowWidth] = useState(window.innerWidth);

 function handleResize() {
   setWindowWidth(window.innerWidth);
 }

 useEffect(() => {
   window.addEventListener('resize', handleResize);
   return () => window.removeEventListener('resize', handleResize);
 }, []);

 return windowWidth;
}
複製程式碼

基本上,我們使用常規的 useState hook,我們將其指定為視窗寬度的初始值,然後在 useEffect 中新增一個監聽器,它將在視窗調整大小時觸發 handleResize。在元件被解除安裝後會我們會及時知道(檢視 useEffect 中的返回值)。是不是很簡單?

注意: use 在 hook 中很重要。之所以使用它,是因為它允許 React 檢查你是否做了不好的事情,例如從常規JS函式呼叫hook。

型別檢查

在支援 Flow 和 TypeScript 之前,React有自己的屬性檢查機制。

PropTypes 檢查 React 元件接收的屬性(props)是否與我們的內容一致。如果一致(例如:應該是物件而不是陣列),將會在控制檯中收到警告。請務必注意:PropTypes 僅在開發模式下進行檢查,因為它們會影響效能並在控制檯中顯示上述警告。

從React 15.5開始,PropTypes 被放到了不同的包裡,需要單獨安裝。它在名為 propTypes(surprise)的靜態屬性中對屬性進行宣告,可以把它與 defaultProps 結合使用,如果屬性未定義就會使用它們(undefined是唯一的情況)。 DefaultProps 與 PropTypes 無關,不過它們可以解決由於 PropTypes 而可能出現的一些警告。

另外兩個選擇是 Flow 和 TypeScript,它們現在更受歡迎(特別是 TypeScript )。

  • TypeScript是 Microsoft 開發的 JavaScript 的型別超集,它可以在程式執行之前檢查錯誤,併為開發工作提供卓越的自動完成功能。它還極大地改善了重構過程。由於受到 Microsoft 的支援,它有豐富的型別語言特徵,也是一個相當安全的選擇。
  • Flow與TypeScript不同,它不是一種語言,而是 JavaScript 的靜態型別檢查器,因此它更像是 JavaScript 中的工具而並非語言。 Flow 背後的整個思路與 TypeScript 完全相似。它允許你新增型別,以便在執行程式碼之前杜絕可能出現的錯誤。就像 TypeScript 一樣,CRA(建立React App)從一開始就支援 Flow。

我發現 TypeScript 更快(幾乎是即時的),特別是在自動完成中,Flow 似乎有點慢。值得注意的是,我自己用的 WebStorm 等 IDE 使用 CLI 與 Flow 整合。但是在檔案中整合可選用法似乎更容易,只需要在檔案開頭新增 // @flow 就可進行型別檢查。另外據我所知,似乎 TypeScript 最終贏得了與 Flow 的戰鬥 —— 它現在更受歡迎,並且一些最流行的庫正在從 Flow 轉向 TypeScript。

官方文件中還提到了更多的選擇,例如 Reason(由Facebook開發並在React社群中獲得普及),Kotlin(由JetBrains開發的語言)等等。

顯然,對於前端開發人員來說,最簡單的方法是使用 Flow 和 TypeScript,而不是切換到 Kotlin 或F#。但是,對於正在轉型到前端的後端開發人員來說,這可能更容易入手。

生產模式和 React 效能

對於生產模式,你需要做的最基本和明顯的改變是:把 DefinePlugin 切換到 “production”,並在Webpack的情況下新增UglifyJsPlugin。在使用 CRA 的情況下,它就像使用 npm run build(將執行react-scripts build)一樣簡單。請注意,Webpack 和 CRA 不是唯一的選項,因為你可以使用其他構建工具,如 Brunch。這通常包含在官方文件中,無論是官方的 React 文件還是特定工具的文件。要確保模式設定正確,你可以使用React Developer Tools,它會告訴你正在用的那種構建(生產與開發)模式應該怎麼配置。上述步驟會使你的應用在沒有來自 React 的檢查和警告的情況下執行,並且 bundle 本身也將被最小化。

你還可以為 React 應用做更多的事。你如何處理構建的 JS 檔案?如果尺寸相對較小,你可以從 “bundle.js” 開始,或者做一些類似 “vendor + bundle” 或者 “vendor + 最小化需要部件 + 在需要時匯入東西” 之類的處理。當你是處理一個非常大的應用時,不需要在一開始就匯入所有內容。請注意,在主 bundle 中去 bundling 一些不會被使用的 JavaScript 程式碼只會增加 bundle 包的大小,並會使應用在啟動時的載入速度變慢。

如果你計劃凍結庫的版本,並認為它們可能長時間內不會被更改,那麼 Vendor bundles 可能很有用。此外,更大的檔案更適合用 gzipping,因此從拆分獲得的好處有時可能不值得。這取決於檔案大小,有時你需要自己去嘗試。

程式碼拆分

程式碼拆分的方式比這裡給出的建議多得多,但讓我們關注 CRA 和 React 本身可用的內容。基本上,為了將程式碼分成不同的塊,可以使用 import(),這可以用 Webpack 支援( import本身是第3階段的提案,所以它還不是語言標準的一部分)。每當 Webpack 看到 import 時,它就會知道需要在這個階段開始拆分程式碼,並且不能將它包含在主包中(它在import中的程式碼)。

現在我們可以將它與 React.lazy() 連線起來,它需要 import() 一個檔案路徑,其中包含需要在那個地方渲染的元件。接下來,我們可以用 React.suspense(),它會在該位置顯示不同的元件,一直到匯入的元件全部載入完畢。有人可能會想,如果我要匯入單個元件,是不是就不需要它了呢?

實際上並非如此,因為 React.lazy() 將顯示我們 import() 的元件,但 import() 可能會獲取比單個元件更大的塊。例如這個元件可能包含其他庫,或更多程式碼,所以不只是需要一個檔案 —— 它可能是綁在一起的多個檔案。最後,我們可以將所有這些包裝在 ErrorBoundary 中*(你可以在本文關於錯誤邊界的那部分中找到程式碼)* 如果某些內容因我們想要匯入的元件而失敗(例如出現網路錯誤),這將作為備用方案。

import ErrorBoundary from './ErrorBoundary';

const ComponentOne = React.lazy(() => import('./ComponentOne'));

function MyComponent() {
   return (
       <ErrorBoundary>
           <React.Suspense fallback={<div>Loading...</div>}>
               <ComponentOne/>
           </React.Suspense>
       </ErrorBoundary>
   );
}
複製程式碼

這是一個簡單的例子,但顯然你可以做得更多。你可以使用 importReact.lazy 進行動態路由劃分(例如:管理員與常規使用者)。請注意,React.lazy 僅支援預設匯出,並且不支援伺服器端呈現。

React 程式碼效能

關於效能,如果你的 React 應用執行緩慢,有兩種工具可以幫助你找出問題。

第一個是 Chrome Performance Tab,它會告訴你每個元件會發生什麼(例如,mount,update )。有了它你應該能夠確定哪個元件可能會出現效能問題,然後進行優化。

另一種選擇是 DevTools Profiler ,它在 React 16.5+ 中可用,並與 shouldComponentUpdate 配合(或PureComponent,在本教程的第一部分中解釋),我們可以提高一些關鍵元件的效能。

顯然,對網路進行基本優化是最佳的,例如對一些事件進行去抖動(例如,滾動),對動畫保持謹慎(使用變換而不是通過改變高度並實現動畫)等等。這些問題很容易被忽略,特別是如果你剛剛掌握了 React。

2019年及以後的 React 現狀

如果要討論 React 的未來,我個人不會太在意。從我的角度來看,React 在 2019 年及以後的地位很難被撼動。

React 擁有如此強大的地位,在一個大社群的支援下很難被廢棄。 React社群非常棒,它總是產生新的創意,核心團隊一直在不斷努力改進 React,並新增新功能和修復舊問題。 React 也得到了一家大公司的支援,但許可證已經不是問題 —— 它現在使用 MIT license。

是的,有一些事情有望改變或改進;例如,使 React 稍微小一些(提到的一個措施是刪除合成事件)或將 className 重新命名為 class。當然,即使這些看似微小的變化也可能導致諸如影響瀏覽器相容性等問題。就個人而言,我也想知道當 WebComponent 獲得更多人氣時會發生什麼,因為它可能會增加一些 React 經常用到的東西。我不相信他們會成為一個徹頭徹尾的替代者,但我相信他們可以很好地相互補充。

至於短期,hook 剛剛被加入到 React。這可能是自 React 重寫以來發生的最大變化,因為它們將帶來更多可能性並增強更多功能元件(現在他們真的被大肆宣傳)。

最後,正如我最近所做的那樣,有React Native。對我來說,這是一項偉大的技術,在過去的幾年中發生了很大的變化。 React Native正在重寫它的核心,這應該以與 React 重寫類似的方式完成(它全部是內部的,幾乎沒有任何東西應該為開發人員改變)。非同步渲染成為本機和 JavaScript 之間更快更輕量級的橋樑。當然還有更多改變。

在 React 生態中有很多值得期待的東西,但 hook(以及React Native,如果有人喜歡手機應用的話)的更新可能將會是我們在2019年所能看到的最重要的變化。

歡迎關注京程一燈公眾號,獲取更多前端乾貨。

相關文章