成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

如何寫出更優雅的 React 組件 - 設計思維篇

開發 前端
在 React 表單管理中有兩個經常使用的術語: 受控輸入和非受控輸入。簡單來說,受控的意思是當前組件的狀態成為該表單的唯一數據源。

[[440142]]

我們從設計思維的角度來談談如何設計一個更優雅的 React 組件。

基本原則

單一職責

單一職責的原則是讓一個模塊都專注于一個功能,即讓一個模塊的責任盡量少。若一個模塊功能過多,則應當拆分為多個模塊,這樣更有利于代碼的維護。

就如同一個人最好專注做一件事,將負責的每件事情做到最好。而組件也是如此,要求將組件限制在一個合適可被復用的粒度。如果一個組件的功能過于復雜就會導致代碼量變大,這個時候就需要考慮拆分為職責單一的小組件。每個小組件只關心自己的功能,組合起來就能滿足復雜需求。

單一組件更易于維護和測試,但也不要濫用,必要的時候才去拆分組件,粒度最小化是一個極端, 可能會導致大量模塊, 模塊離散化也會讓項目變得難以管理。

劃分邊界

如何拆分組件,如果兩個組件的關聯過于緊密,從邏輯上無法清晰定義各自的職責,那么這兩個組件不應該被拆分。否則各自職責不清,邊界不分,則會產生邏輯混亂的問題。那么拆分組件最關鍵在于確定好邊界,通過抽象化的參數通信,讓每個組件發揮出各自特有的能力。

高內聚/低耦合

高質量的組件要滿足高內聚和低耦合的原則。

高內聚意思是把邏輯緊密相關的內容聚合在一起。在 jQuery 時代,我們將一個功能的資源放在了 js、html、css 等目錄,開發時,我們需要到不同的目錄中尋找相關的邏輯資源。再比如 Redux 的建議將 actions、reducers、store 拆分到不同的地方,將一個很簡單的功能邏輯分散開來。這很不滿足高內聚的特點。拋開 Redux,在 React 組件化的思維本身很滿足高內聚的原則,即一個組件是一個自包含的單元, 它包含了邏輯/樣式/結構, 甚至是依賴的靜態資源。

低耦合指的是要降低不同組件之間的依賴關系,讓每個組件要盡量獨立。也就是說平時寫代碼都是為了低耦合而進行。通過責任分離、劃分邊界的方式,將復雜的業務解耦。

遵循基本原則好處:

  1. 降低單個組件的復雜度,可讀性高
  2. 降低耦合,不至于牽一發而動全身
  3. 提高可復用性
  4. 邊界透明,易于測試
  5. 流程清晰,降低出錯率,并調試方便

進階設計

受控/非受控狀態

在 React 表單管理中有兩個經常使用的術語: 受控輸入和非受控輸入。簡單來說,受控的意思是當前組件的狀態成為該表單的唯一數據源。表明這個表單的值在當前組件的控制中,并只能通過 setState 來更新。

受控/非受控的概念在組件設計上極為常見。受控組件通常以 value 與 onChange 成對出現。傳入到子組件中,子組件無法直接修改這個 value,只能通過 onChange 回調告訴父組件更新。非受控組件則可以傳入 defaultValue 屬性來提供初始值。

Modal 組件的 visible 受控/非受控:

  1. // 受控 
  2. <Modal visible={visible} onVisibleChange={handleVisibleChange} /> 
  3.  
  4. // 非受控 
  5. <Modal defaultVisible={visible} /> 

若該狀態作為組件的核心邏輯時,那么它應該支持受控,或兼容非受控模式。若該狀態為次要邏輯,可以根據實際情況選擇性支持受控模式。

例如 Select 組件處理受控與非受控邏輯:

  1. function Select(props: SelectProps) { 
  2.   // value 和 onChange 為核心邏輯,支持受控。兼容傳入 defaultValue 成為非受控 
  3.   // defaultOpen 為次要邏輯,可以非受控 
  4.   const { value: controlledValue, onChange: onControlledChange, defaultValue, defaultOpen } = props; 
  5.   // 非受控模式使用內部 state 
  6.   const [innerValue, onInnerValueChange] = React.useState(defaultValue); 
  7.   // 次要邏輯,選擇框展開狀態 
  8.   const [visible, setVisible] = React.useState(defaultOpen); 
  9.  
  10.   // 通過檢測參數上是否包含 value 的屬性判斷是否為受控,盡管 value 為 undefined 
  11.   const shouldControlled = Reflect.has(props, 'value'); 
  12.  
  13.   // 支持受控和非受控處理 
  14.   const value = shouldControlled ? controlledValue : innerValue; 
  15.   const onChange = shouldControlled ? onControlledChange : onInnerValueChange; 
  16.  
  17.   // ... 

配合 hooks 受控

一個組件是否受控,通常來說針對其本身的支持,現在自定義 hooks 的出現可以突破此限制。復雜的組件,配合 hooks 會更加得心應手。

封裝此類組件,將邏輯放在 hooks 中,組件本身則被掏空,其作用是主要配合自定義 hooks 進行渲染。

  1. function Demo() { 
  2.   // 主要的邏輯在自定義 hook 中 
  3.   const sheet = useSheetTable(); 
  4.  
  5.   // 組件本身只接收一個參數,為 hook 的返回值 
  6.   <SheetTable sheet={sheet} />; 

這樣做的好處是邏輯與組件徹底分離,更利于狀態提升,可以直接訪問 sheet 所有的狀態,這種模式受控會更加徹底。簡單的組件也許不適合做成這種模式,本身沒這么大的受控需求,這樣封裝會增加一些使用復雜度。

單一數據源

單一數據源原則,指組件的一個狀態以 props 的形式傳給子組件,并且在傳遞過程中具有延續性。也就是說狀態在傳遞到各個子組件中不用 useState 去接收,這會使傳遞的狀態失去響應特性。

以下代碼違背了單一數據源的原則,因為在子組件中定義了狀態 searchResult 緩存了搜索結果,這會導致 options 參數在 onFilter 后與子組件失去響應特性。

  1. function SelectDropdown({ options = [], onFilter }: SelectDropdownProps) { 
  2.   // 緩存搜索結果 
  3.   const [searchResult, setSearchResult] = React.useState<Option[] | undefined>(undefined); 
  4.  
  5.   return ( 
  6.     <div> 
  7.       <Input.Search 
  8.         onSearch={(keyword) => { 
  9.           setSearchResult(keyword ? onFilter(keyword) : undefined); 
  10.         }} 
  11.       /> 
  12.  
  13.       <OptionList options={searchResult ?? options} /> 
  14.     </div> 
  15.   ); 

 應當遵循單一數據源的原則。將關鍵詞存為 state,通過響應 keyword 變化生成新的 options:

  1. function SelectDropdown({ options = [], onFilter }: SelectDropdownProps) { 
  2.   // 搜索關鍵詞 
  3.   const [keyword, setKeyword] = React.useState<string | undefined>(undefined); 
  4.   // 使用過濾條件篩選數據 
  5.   const currentOptions = React.useMemo(() => { 
  6.     return keyword && onFilter ? options.filter((n) => onFilter(keyword, n)) : options; 
  7.   }, [options, onFilter, keyword]); 
  8.  
  9.   return ( 
  10.     <div> 
  11.       <Input.Search 
  12.         onSearch={(text) => { 
  13.           setKeyword(text); 
  14.         }} 
  15.       /> 
  16.       <OptionList options={currentOptions} /> 
  17.     </div> 
  18.   ); 

減少 useEffect

useEffect 即副作用。如果沒有必要,盡量減少 useEffect 的使用。React 官方將這個 API 的使用場景歸納為改變 DOM、添加訂閱、異步任務、記錄日志等。先來看一段代碼:

  1. function Demo({ value, onChange }) { 
  2.   const [labelList, setLabelList] = React.useState(() => value.map(customFn)); 
  3.  
  4.   // value 變化后,使內部狀態更新 
  5.   React.useEffect(() => { 
  6.     setLabelList(value.map(customFn)); 
  7.   }, [value]); 

上面代碼為了保持 labelList 與 value 的響應,使用了 useEffect。也許你現在看這個代碼的本身能正常執行。如果現在有個需求:labelList 變化后也同步到 value,字面理解下你可能會寫出如下代碼:

  1. React.useEffect(() => { 
  2.   onChange(labelList.map(customFn)); 
  3. }, [labelList]); 

你會發現應用進入了永久循環中,瀏覽器失去控制,這就是沒必要的 useEffect ??梢岳斫鉃椴蛔龈淖? DOM、添加訂閱、異步任務、記錄日志等場景的操作,就盡量別用 useEffect,比如監聽 state 再改變別的 state。結局就是應用復雜度達到一定程度,不是瀏覽器先崩潰,就是開發者崩潰。

那有好的方式解決嗎?我們可以將邏輯理解為 動作 + 狀態。其中 狀態 的變更只能由 動作 觸發。這就能很好解決上面代碼中的問題,將 labelList 的狀態提升,找出改變 value 的 動作,封裝一個聯動改變 labelList 的方法給各個 動作,越復雜的場景這種模式越高效。

通用性原則

通用性設計其實是一定意義上放棄對 DOM 的掌控,而將 DOM 結構的決定權轉移給開發者,比如預留自定義渲染。

舉個例子, antd 中的 Table通過 render 函數將每個單元格渲染的決定權交給使用者,這樣極大提高了組件的可擴展性:

  1. const columns = [ 
  2.   { 
  3.     title: '名稱'
  4.     dataIndex: 'name'
  5.     width: 200, 
  6.     render(text) { 
  7.       return<em>{text}</em>; 
  8.     }, 
  9.   }, 
  10. ]; 
  11.  
  12. <Table columns={columns} />; 

 優秀的組件,會通過參數預設默認的渲染行為,同時支持自定義渲染。

統一 API

當各個組件數量變多之后,組件與組件直接可能存在某種契合的關系,我們可以統一某種行為 API 的一致性,這樣可以降低使用者對各個組件 API 名稱的心智負擔。否則組件傳參就會如同一根一根數面條一樣痛苦。

舉個例子,經典的 value 與 onChange 的 API 可以在各個不同的表單域上出現??梢酝ㄟ^包裝的方式導出更多高階組件,這些高階組件又可以被表單管理組件所容納。

我們可以約定在各個組件上比如 visible、onVisibleChange、bordered、size、allowClear 這樣的 API,使其在各個組件中保持一致性。

不可變狀態

對于函數式編程范式的 React 來說,不可變狀態與單向數據流是其核心概念。如果一個復雜的組件手動保持不可變狀態繁雜程度也是相當高,這里推薦使用 immer 做不可變數據管理。如果一個對象內部屬性變化了,那么整個對象就是全新的,不變的部分會保持引用,這樣天生契合 React.memo 做淺對比,減少 shouldComponentUpdate 比較的性能消耗。

注意陷阱

React 在某個意義上說一個狀態機,每次 render 所定義的變量會重新聲明。

Context 陷阱

  1. exportfunction ThemeProvider(props) { 
  2.   const [theme, switchTheme] = useState(redTheme); 
  3.  
  4.   // 這里每一次渲染 ThemeProvider, 都會創建一個新的 value 從而導致強制渲染所有使用該 Context 的組件 
  5.   return<Context.Provider value={{ theme, switchTheme }}>{props.children}</Context.Provider>; 

所以傳遞給 Context 的 value 做一下記憶緩存:

  1. exportfunction ThemeProvider(props) { 
  2.   const [theme, switchTheme] = useState(redTheme); 
  3.   const value = React.useMemo(() => ({ theme, switchTheme }), [theme]); 
  4.  
  5.   return<Context.Provider value={value}>{props.children}</Context.Provider>; 

render props 陷阱

render 方法里創建函數,那么使用 render props 會抵消使用 React.memo 帶來的優勢。因為淺比較 props 的時候總會得到 false,并且在這種情況下每一個 render 對于 render props 將會生成一個新的值。

  1. <CustomComponent renderFooter={() => <em>Footer</em>} /> 

可以使用 useMethods 代替:github.com/MinJieLiu/heo/blob/main/src/useMethods.tsx

社區實踐

高階組件/裝飾器模式

  1. const HOC = (Component) => EnhancedComponent; 

裝飾器模式是在不改變原對象的基礎上,通過對其進行包裝擴展(添加屬性或方法),使原有對象可以滿足用戶的更復雜需求,滿足開閉原則,也不會破壞現有的操作。組件是將 props 轉化成 UI ,然而高階組件將一個組件轉化成另外一個組件。

例如漫威電影中的鋼鐵俠,本身就是一個普通人,可以行走、跳躍。經過戰衣的裝飾,可以跑得更快,還具備飛行能力。

在普通組件中包裝一個 withRouter(react-router),就具備了操作路由的能力。包裝一個 connect(react-redux),就具備了操作全局數據的能力。

Render Props

  1. <Component render={(props) => <EnhancedComponent {...props} />} /> 

Render Props 用于使用一個值為函數的 prop 在 React 組件之間的代碼共享。Render Props 其實和高階組件一樣,是為了給純函數組件加上 state,響應 react 的生命周期。它以一種回調的方式,傳入一個函數給子組件調用,獲得狀態可以與父組件交互。

鏈式 Hooks

在 React Hooks 時代,高階組件和 render props 使用頻率會下降很多,很多場景下會被 hooks 所替代。

我們看看 hooks 的規則:

  • 只在最頂層使用 Hook
  • 不在循環,條件或嵌套函數中調用 Hook
  • 只在 React 函數中調用 Hook

hook 都是按照一定的順序調用,因為其內部使用鏈表實現。我們可以通過 單一職責 的概念將每個 hook 作為模塊去呈現,通過組合自定義 hook 就可以實現漸進式功能增強。如同 rxjs 一樣具備鏈式調用的同時又可以操作其狀態與生命周期。

示例:

  1. function Component() { 
  2.   const value = useSelectStore(); 
  3.   const keyboardEvents = useInteractive(value); 
  4.   const label = useSelectPresent(keyboardEvents); 
  5.   // ... 

用過語義化組合可以選擇使用需要的 hooks 來創造出適應各個需求的自定義組件。在某種意義上說最小單元不止是組件,也可以是自定義 hooks。

結語

 

希望每個人都能寫出高質量的組件。

 

責任編輯:姜華 來源: 前端星辰
相關推薦

2021-12-07 08:16:34

React 前端 組件

2023-12-21 10:26:30

??Prettier

2022-05-13 08:48:50

React組件TypeScrip

2022-03-11 12:14:43

CSS代碼前端

2016-11-25 13:50:15

React組件SFC

2017-09-01 14:18:50

前端React組件

2018-07-12 14:20:33

SQLSQL查詢編寫

2019-09-20 15:47:24

代碼JavaScript副作用

2021-01-04 07:57:07

C++工具代碼

2022-08-09 13:22:26

Hooksreactvue

2020-05-14 09:15:52

設計模式SOLID 原則JS

2017-05-17 15:50:34

開發前端react

2022-05-30 21:47:21

技術目標PRD

2020-05-08 14:45:00

JS代碼變量

2020-07-15 08:17:16

代碼

2024-02-23 08:57:42

Python設計模式編程語言

2021-11-30 10:20:24

JavaScript代碼前端

2020-05-11 15:23:58

CQRS代碼命令

2013-06-07 14:00:23

代碼維護

2021-09-01 08:55:20

JavaScript代碼開發
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 一区二区三区免费在线观看 | 天天综合网天天综合色 | 狠狠躁躁夜夜躁波多野结依 | 精品久久久久一区二区国产 | 午夜精品一区二区三区三上悠亚 | 国产精品欧美一区二区三区不卡 | 伊人影院在线观看 | 国产视频2021 | 色综合99| 亚洲综合色站 | 色在线免费视频 | 欧美综合一区二区三区 | 秋霞电影一区二区三区 | 九九精品在线 | 欧美一级在线 | 日韩在线三级 | 成人午夜在线 | 天天躁人人躁人人躁狂躁 | 亚洲国产自产 | 99精品视频在线 | 高清av一区 | 欧美久久一级特黄毛片 | 日韩资源| 久久亚洲国产 | 中文av电影| 激情婷婷成人 | 亚洲v日韩v综合v精品v | 日韩精品一区二区久久 | 婷婷久久综合 | 日韩免费1区二区电影 | 欧美三级久久久 | 日韩一区二区福利视频 | 日韩一区二区三区视频在线观看 | 国产精品毛片无码 | 日韩高清国产一区在线 | 99精品在线观看 | 亚洲精品1 | 国精产品一区一区三区免费完 | 一区二区三区播放 | 五月天综合网 | 国产98色在线 | 日韩 |