React教程:組件,Hooks和性能

翻譯:瘋狂的技術宅
原文: https://www.toptal.com/react/...

本文首發微信公衆號:jingchengyideng
歡迎關注,天天都給你推送新鮮的前端技術文章javascript


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

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

每當開發一個新的程序時,你須要爲其作好在之後轉換爲 React 應用的新設計,首先試着肯定設計草圖中的組件,如何分離它們以使其更易於管理,以及哪些元素是重複的(或他們的行爲)。儘可能避免添加可能「未來有用」的代碼 —— 雖然這很誘人,但可能將來永遠也不會到來,你將留下一堆具備大量可配置選項的多餘通用功能/組件。前端

clipboard.png

此外,若是一個組件大於 2 到 3 個窗口的高度,也許值得分離(若是可能的話) —— 之後更容易閱讀。java

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

在大多數應用中,須要輸入和與用戶進行某種形式的交互,容許他們輸入內容、上傳文件、選擇字段等。 React 用兩種不一樣的方式處理用戶交互 —— 受控非受控組件。react

顧名思義,受控組件的值由 React 控制,能爲與用戶交互的元素提供值,而不受控制的元素不獲取值屬性。多虧了這一點,咱們才能把 React 狀態做爲單一的事實來源,所以咱們在屏幕上看到的與當前擁有的狀態是一致的。開發人員須要傳遞一個函數,該函數用來響應用戶與表單的交互,這將會改變它的狀態。git

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 類型輸入,由於它的值是隻讀的,不能在編碼中去設置(須要用戶交互)。另外我發現受控組件更容易理解和於使用。對受控組件的驗證是基於從新渲染的,狀態能夠更改,而且能夠很輕鬆的顯示輸入中存在的問題(例如格式錯誤或者輸入爲空)。github

Refs

在前面咱們提到過 refs,這是一個特殊功能,能夠在類組件中使用,直到 16.8 中出現了 hooks。web

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年所能看到的最重要的變化。


    歡迎繼續閱讀本專欄其它高贊文章:


    本文首發微信公衆號:jingchengyideng

    歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

    歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

    相關文章
    相關標籤/搜索