React 教程:組件、鉤子和性能
已發表: 2022-03-11正如我們在 React 教程的第一部分中所指出的,React 入門相對容易。 首先使用 Create React App (CRA),初始化一個新項目,然後開始開發。 可悲的是,隨著時間的推移,您可能會遇到代碼變得相當難以維護的情況,尤其是如果您是 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}/> ); } }
那麼什麼時候應該使用呢? 我會說受控組件在大多數情況下都是可行的方法,但也有一些例外。 例如,您需要在 React 中使用不受控制的組件的一種情況是file
類型輸入,因為它的值是只讀的並且不能以編程方式設置(需要用戶交互)。 此外,我發現受控組件更易於閱讀和使用。 對受控組件進行驗證是基於重新渲染,狀態可以改變,我們可以很容易地指出輸入有問題(例如,格式或為空)。
參考文獻
我們已經提到了refs
,這是一個在類組件中可用的特殊功能,直到鉤子出現在 16.8 中。
Refs 可以讓開發人員通過引用訪問 React 組件或 DOM 元素(取決於我們附加 ref 的類型)。 嘗試避免使用它們並僅在必備場景中使用它們被認為是一種好習慣,因為它們會使代碼更難閱讀並破壞自上而下的數據流。 然而,在某些情況下它們是必要的,尤其是在 DOM 元素上(例如,以編程方式改變焦點)。 附加到 React 組件元素時,您可以自由地使用您所引用的該組件中的方法。 儘管如此,應該避免這種做法,因為有更好的方法來處理它(例如,提升狀態並將函數移動到父組件)。
Refs 也有三種不同的實現方式:
- 使用字符串文字(遺留,應該避免),
- 使用在 ref 屬性中設置的回調函數,
- 通過將 ref 創建為
React.createRef()
並將其綁定到類屬性並通過它訪問它(請注意,引用將在 componentDidMount 生命週期中可用)。
最後,在某些情況下,沒有傳遞 refs,並且有時您想從當前組件訪問更深層次的引用元素(例如,您有一個具有內部<input>
DOM 元素的<Button>
組件,而現在您位於<Row>
組件中,並且從您想要訪問的行組件中輸入 DOM 焦點功能。這就是您將使用forwardRef
的地方)。
沒有傳遞引用的一種情況是在組件上使用了更高階的組件 - 原因很容易理解,因為ref
不是prop
(類似於key
)所以它沒有被傳遞,所以它會引用HOC
而不是被它包裝的組件。 在這種情況下,我們可以使用React.forwardRef
,它將 props 和 refs 作為參數,然後可以將其分配給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方法。
錯誤邊界也是您可以將信息發送到您使用的錯誤記錄器的地方(在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
中一樣,你首先調用一個帶有配置的函數,然後返回一個函數它接受一個參數組件並返回 HOC。
您可以添加並應避免以下幾點:
- 為您的包裝器 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 樣式有點不同。 該字符串不是與樣式一起傳遞的,而是與每個具有正確值的對像一起傳遞的。 樣式屬性也是camelCased,所以border-radius變成borderRadius等等。
React 似乎已經普及了一些不僅在 React 中變得司空見慣的解決方案,例如最近集成到 CRA 中的 CSS 模塊,您可以在其中簡單地導入name.modules.css
並使用它的類(如屬性)來設置組件的樣式(一些 IDE ,例如,WebStorm,也有自動完成功能,它會告訴您可用的名稱)。
在 React 中也很流行的另一個解決方案是 CSS-in-JS(例如, emotion
庫)。 再次指出,CSS 模塊和情感(或一般的 CSS-in-JS)不限於 React。
React 中的鉤子
自重寫以來, Hooks很可能是 React 中最受期待的新增功能。 產品是否辜負了炒作? 從我的角度來看,是的,因為它們確實是一個很棒的功能。 它們本質上是開闢新機會的功能,例如:
- 允許刪除許多我們只使用因為我們不能擁有的
class
組件,例如本地狀態或引用,因此組件的代碼看起來更容易閱讀。 - 使您能夠使用更少的代碼來獲得相同的效果。
- 使函數更容易思考和測試,例如,通過使用 react-testing-library。
- 也可以帶參數,一個的結果可以很容易地被另一個鉤子使用(例如,
setState
fromuseState
inuseEffect
)。 - 比類更好地縮小方法,這對於縮小器來說往往有點問題。
- 可能會在您的應用程序中刪除 HOC 和渲染道具模式,儘管這些模式是為了解決其他問題而設計的。
- 能夠由任何熟練的 React 開發人員定制。
默認包含的 React 鉤子很少。 三個基本的是useState
、 useEffect
和useContext
。 還有幾個附加的,例如useRef
和useMemo
,但現在,我們將專注於基礎知識。
讓我們看一下useState
,讓我們用它來創建一個簡單的計數器示例。 它是如何工作的? 好吧,基本上,整個結構非常簡單,看起來像:
export function Counter() { const [counter, setCounter] = React.useState(0); return ( <div> {counter} <button onClick={() => setCounter(counter + 1)}>+</button> </div> ); };
它使用initialState
(value) 調用並返回一個包含兩個元素的數組。 由於數組解構分配,我們可以立即將變量分配給這些元素。 第一個總是更新後的最後一個狀態,而另一個是我們將用來更新值的函數。 看起來相當容易,不是嗎?
此外,由於此類組件過去被稱為無狀態功能組件,因此此類名稱不再合適,因為它們可以具有如上所示的狀態。 因此,類組件和函數組件的名稱似乎更符合它們的實際用途,至少從 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());
最後,如果我們要使用與當前狀態( counter
)相同時刻完全相同的值的setCounter
,那麼組件將不會重新渲染。
另一方面, useEffect
是為我們的功能組件添加副作用,無論是訂閱、API 調用、計時器,還是我們可能認為有用的任何東西。 我們將傳遞給useEffect
的任何函數都將在渲染後運行,並且它將在每次渲染後運行,除非我們添加關於哪些屬性的更改應作為函數的第二個參數重新運行的限制。 如果我們只想在掛載時運行它並在卸載時清理,那麼我們只需要在其中傳遞一個空數組。
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
,但它會稍後觸發。 如果您想在瀏覽器繪製之前調用類似的鉤子,請使用useLayoutEffect
,但這些更新將同步應用,與useEffect
不同。
useContext
似乎是最容易理解的,因為它提供了我們想要訪問的上下文(由createContext
函數返回的對象),作為回報,它為我們提供了該上下文的值。
const context = useContext(Context);
最後,要編寫自己的鉤子,您可以編寫如下內容:
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
鉤子,我們將其分配為初始值窗口寬度。 然後在useEffect,
我們添加了一個偵聽器,它將在每次調整窗口大小時觸發handleResize
。 我們還清除了組件將被卸載後(查看useEffect
中的返回)。 簡單?
注意:所有鉤子中的使用這個詞很重要。 之所以使用它是因為它允許 React 檢查你是否在做壞事,例如,從常規 JS 函數調用鉤子。
檢查類型
在 Flow 和 TypeScript 成為一個選項之前,React 有自己的 props 檢查。
PropTypes 檢查 React 組件接收到的屬性(props)是否與我們所擁有的一致。 每當發生不同的情況(例如,對象而不是數組)時,我們都會在控制台中收到警告。 需要注意的是,PropTypes 僅在開發模式下檢查,因為它們對性能的影響和前面提到的控制台警告。
從 React 15.5 開始,PropTypes 位於不同的包中,需要單獨安裝。 它們在名為propTypes
的靜態屬性(驚喜)中與屬性一起聲明,將它們與defaultProps
結合使用,如果屬性未定義(唯一的情況是未定義),則使用 defaultProps。 DefaultProps 與 PropTypes 無關,但它們可以解決一些由於 PropTypes 可能出現的警告。
另外兩個選項是 Flow 和 TypeScript,它們現在更流行(尤其是 TypeScript)。
- TypeScript是由 Microsoft 開發的 JavaScript 的類型化超集,它甚至可以在應用程序運行之前檢查錯誤,並為開發提供卓越的自動完成功能。 它還極大地改進了重構。 由於 Microsoft 支持,它在類型語言方面擁有豐富的經驗,因此它也是一個相當安全的選擇。
- 與 TypeScript 不同, Flow不是一種語言。 它是 JavaScript 的靜態類型檢查器,因此它更像是包含在 JavaScript 中的工具,而不是一種語言。 Flow 背後的整個想法與 TypeScript 提供的非常相似。 它允許您添加類型,因此在運行代碼之前不太可能出現任何錯誤。 就像 TypeScript 一樣,現在 CRA(創建 React 應用程序)從一開始就支持 Flow。
就個人而言,我發現 TypeScript 更快(幾乎是瞬時的),尤其是在自動完成方面,Flow 似乎有點慢。 值得注意的是,我個人使用的 WebStorm 等 IDE 使用 CLI 與 Flow 集成。 但是,在文件中集成可選使用似乎更容易,您只需在文件開頭添加// @flow
即可開始類型檢查。 此外,據我所知,TypeScript 似乎最終贏得了與 Flow 的戰鬥——它現在更流行了,一些最受歡迎的庫正在從 Flow 重構為 TypeScript。
官方文檔中也提到了更多選項,例如 Reason(由 Facebook 開發並在 React 社區中廣受歡迎)、Kotlin(由 JetBrains 開發的語言)等等。
顯然,對於前端開發人員來說,最簡單的方法是加入並開始使用 Flow 和 TypeScript,而不是切換到 Kotlin 或 F#。 但是,對於正在過渡到前端的後端開發人員來說,這些實際上可能更容易上手。
生產和反應性能
對於生產模式,您需要做的最基本和最明顯的更改是為DefinePlugin
切換到“生產”,並在UglifyJsPlugin
的情況下添加 UglifyJsPlugin。 在 CRA 的情況下,它就像使用npm run build
一樣簡單(它將運行react-scripts build
)。 請注意,Webpack 和 CRA 不是唯一的選擇,因為您可以使用其他構建工具,例如 Brunch。 這通常包含在官方文檔中,無論是官方 React 文檔還是特定工具的文檔。 為確保模式設置正確,您可以使用 React 開發者工具,它會指示您正在使用哪種構建(生產與開發)。 上述步驟將使您的應用程序在沒有來自 React 的檢查和警告的情況下運行,並且捆綁包本身也將被最小化。
你可以為你的 React 應用做更多的事情。 你如何處理構建的 JS 文件? 如果大小相對較小,您可以只從“bundle.js”開始,或者可以執行“供應商 + 捆綁”或“供應商 + 最小所需部分 + 在需要時導入東西”之類的操作。 當您處理一個非常大的應用程序並且您不需要在一開始就導入所有內容時,這很有用。 請注意,在主包中捆綁一些甚至不使用的 JavaScript 代碼只會增加包的大小,並使應用程序在一開始時加載速度變慢。
如果您計劃在意識到庫的版本可能很長時間不會更改(如果有的話)後凍結它們,那麼供應商捆綁包可能會很有用。 此外,更大的文件更適合 gzip,因此您從分離中獲得的好處有時可能不值得。 這取決於文件大小,有時您只需要自己嘗試一下。
代碼拆分
代碼拆分的方式可能比這裡建議的更多,但讓我們關注我們在 CRA 和 React 本身中可用的內容。 基本上,為了將代碼分成不同的塊,我們可以使用import()
,這要歸功於 Webpack ( import
本身是目前第 3 階段的提議,因此它還不是語言標準的一部分)。 每當 Webpack 看到import
,它就會知道它需要在這個階段開始代碼拆分,並且不能將它包含在 main bundle 中(它在 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> ); }
這是一個基本示例,但您顯然可以做得更多。 您可以使用import
和React.lazy
進行動態路由拆分(例如,管理員與普通用戶,或者只是帶來很多東西的非常大的路徑)。 請注意, React.lazy
僅支持默認導出,不支持服務器端渲染。
反應代碼性能
關於性能,如果您的 React 應用程序運行緩慢,有兩個工具可以幫助您找出問題所在。
第一個是 Chrome 性能選項卡,它將告訴您每個組件發生了什麼(例如,掛載、更新)。 多虧了這一點,您應該能夠確定哪個組件出現性能問題,然後對其進行優化。
另一種選擇是使用在 React 16.5+ 中可用的 DevTools Profiler,在 shouldComponentUpdate(或 PureComponent,在本教程的第一部分中解釋)的合作下,我們可以提高一些關鍵組件的性能。
顯然,對 web 使用基本的最佳實踐是最佳的,例如去抖動一些事件(例如,滾動),對動畫保持謹慎(使用變換而不是改變高度並為其設置動畫)等等。 使用最佳實踐很容易被忽視,尤其是當你剛剛開始接觸 React 時。
2019 年及以後的 React 狀態
如果我們要討論 React 的未來,就個人而言,我不會太擔心。 從我的角度來看,React 在 2019 年及以後保持其寶座將毫無困難。
React 擁有如此強大的地位,並得到大量社區的支持,以至於它很難被淘汰。 React 社區很棒,它並沒有枯竭的想法,核心團隊一直在努力改進 React,添加新功能並修復舊問題。 React 也得到了一家大公司的支持,但是許可問題已經解決了——它現在是 MIT 許可的。
是的,有一些事情需要改變或改進; 例如,使 React 更小一些(提到的措施之一是刪除合成事件)或將className
重命名為class.
當然,即使是這些看似微小的更改也可能導致影響瀏覽器兼容性等問題。 就個人而言,我也想知道當 WebComponent 越來越受歡迎時會發生什麼,因為它可能會增加 React 今天經常使用的一些東西。 我不相信它們會完全替代,但我相信它們可以很好地互補。
至於短期,鉤子剛剛來到 React。 這可能是自 React 重寫以來最大的變化,因為它們將開闢大量可能性並增強更多功能組件(而且它們現在真的被大肆宣傳了)。
最後,正如我最近一直在做的那樣,還有 React Native。 對我來說,這是一項偉大的技術,在過去的幾年中發生瞭如此大的變化(缺乏 react-native 鏈接可能是大多數人最大的問題,而且顯然存在大量錯誤)。 React Native 正在重寫其核心,這應該以與 React 重寫類似的方式完成(這都是內部的,對於開發人員來說,什麼都不應該或幾乎什麼都不應該改變)。 異步渲染、原生和 JavaScript 之間更快、更輕量級的橋樑等等。
React 生態系統有很多值得期待的地方,但 hooks(如果有人喜歡手機,還有 React Native)更新可能是我們將在 2019 年看到的最重要的變化。