自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如何寫(xiě)出更優(yōu)雅的 React 組件 - 設(shè)計(jì)思維篇

開(kāi)發(fā) 前端
在 React 表單管理中有兩個(gè)經(jīng)常使用的術(shù)語(yǔ): 受控輸入和非受控輸入。簡(jiǎn)單來(lái)說(shuō),受控的意思是當(dāng)前組件的狀態(tài)成為該表單的唯一數(shù)據(jù)源。

[[440142]]

我們從設(shè)計(jì)思維的角度來(lái)談?wù)勅绾卧O(shè)計(jì)一個(gè)更優(yōu)雅的 React 組件。

基本原則

單一職責(zé)

單一職責(zé)的原則是讓一個(gè)模塊都專(zhuān)注于一個(gè)功能,即讓一個(gè)模塊的責(zé)任盡量少。若一個(gè)模塊功能過(guò)多,則應(yīng)當(dāng)拆分為多個(gè)模塊,這樣更有利于代碼的維護(hù)。

就如同一個(gè)人最好專(zhuān)注做一件事,將負(fù)責(zé)的每件事情做到最好。而組件也是如此,要求將組件限制在一個(gè)合適可被復(fù)用的粒度。如果一個(gè)組件的功能過(guò)于復(fù)雜就會(huì)導(dǎo)致代碼量變大,這個(gè)時(shí)候就需要考慮拆分為職責(zé)單一的小組件。每個(gè)小組件只關(guān)心自己的功能,組合起來(lái)就能滿足復(fù)雜需求。

單一組件更易于維護(hù)和測(cè)試,但也不要濫用,必要的時(shí)候才去拆分組件,粒度最小化是一個(gè)極端, 可能會(huì)導(dǎo)致大量模塊, 模塊離散化也會(huì)讓項(xiàng)目變得難以管理。

劃分邊界

如何拆分組件,如果兩個(gè)組件的關(guān)聯(lián)過(guò)于緊密,從邏輯上無(wú)法清晰定義各自的職責(zé),那么這兩個(gè)組件不應(yīng)該被拆分。否則各自職責(zé)不清,邊界不分,則會(huì)產(chǎn)生邏輯混亂的問(wèn)題。那么拆分組件最關(guān)鍵在于確定好邊界,通過(guò)抽象化的參數(shù)通信,讓每個(gè)組件發(fā)揮出各自特有的能力。

高內(nèi)聚/低耦合

高質(zhì)量的組件要滿足高內(nèi)聚和低耦合的原則。

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

低耦合指的是要降低不同組件之間的依賴關(guān)系,讓每個(gè)組件要盡量獨(dú)立。也就是說(shuō)平時(shí)寫(xiě)代碼都是為了低耦合而進(jìn)行。通過(guò)責(zé)任分離、劃分邊界的方式,將復(fù)雜的業(yè)務(wù)解耦。

遵循基本原則好處:

  1. 降低單個(gè)組件的復(fù)雜度,可讀性高
  2. 降低耦合,不至于牽一發(fā)而動(dòng)全身
  3. 提高可復(fù)用性
  4. 邊界透明,易于測(cè)試
  5. 流程清晰,降低出錯(cuò)率,并調(diào)試方便

進(jìn)階設(shè)計(jì)

受控/非受控狀態(tài)

在 React 表單管理中有兩個(gè)經(jīng)常使用的術(shù)語(yǔ): 受控輸入和非受控輸入。簡(jiǎn)單來(lái)說(shuō),受控的意思是當(dāng)前組件的狀態(tài)成為該表單的唯一數(shù)據(jù)源。表明這個(gè)表單的值在當(dāng)前組件的控制中,并只能通過(guò) setState 來(lái)更新。

受控/非受控的概念在組件設(shè)計(jì)上極為常見(jiàn)。受控組件通常以 value 與 onChange 成對(duì)出現(xiàn)。傳入到子組件中,子組件無(wú)法直接修改這個(gè) value,只能通過(guò) onChange 回調(diào)告訴父組件更新。非受控組件則可以傳入 defaultValue 屬性來(lái)提供初始值。

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

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

若該狀態(tài)作為組件的核心邏輯時(shí),那么它應(yīng)該支持受控,或兼容非受控模式。若該狀態(tài)為次要邏輯,可以根據(jù)實(shí)際情況選擇性支持受控模式。

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

  1. function Select(props: SelectProps) { 
  2.   // value 和 onChange 為核心邏輯,支持受控。兼容傳入 defaultValue 成為非受控 
  3.   // defaultOpen 為次要邏輯,可以非受控 
  4.   const { value: controlledValue, onChange: onControlledChange, defaultValue, defaultOpen } = props; 
  5.   // 非受控模式使用內(nèi)部 state 
  6.   const [innerValue, onInnerValueChange] = React.useState(defaultValue); 
  7.   // 次要邏輯,選擇框展開(kāi)狀態(tài) 
  8.   const [visible, setVisible] = React.useState(defaultOpen); 
  9.  
  10.   // 通過(guò)檢測(cè)參數(shù)上是否包含 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 受控

一個(gè)組件是否受控,通常來(lái)說(shuō)針對(duì)其本身的支持,現(xiàn)在自定義 hooks 的出現(xiàn)可以突破此限制。復(fù)雜的組件,配合 hooks 會(huì)更加得心應(yīng)手。

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

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

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

單一數(shù)據(jù)源

單一數(shù)據(jù)源原則,指組件的一個(gè)狀態(tài)以 props 的形式傳給子組件,并且在傳遞過(guò)程中具有延續(xù)性。也就是說(shuō)狀態(tài)在傳遞到各個(gè)子組件中不用 useState 去接收,這會(huì)使傳遞的狀態(tài)失去響應(yīng)特性。

以下代碼違背了單一數(shù)據(jù)源的原則,因?yàn)樵谧咏M件中定義了狀態(tài) searchResult 緩存了搜索結(jié)果,這會(huì)導(dǎo)致 options 參數(shù)在 onFilter 后與子組件失去響應(yīng)特性。

  1. function SelectDropdown({ options = [], onFilter }: SelectDropdownProps) { 
  2.   // 緩存搜索結(jié)果 
  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.   ); 

 應(yīng)當(dāng)遵循單一數(shù)據(jù)源的原則。將關(guān)鍵詞存為 state,通過(guò)響應(yīng) keyword 變化生成新的 options:

  1. function SelectDropdown({ options = [], onFilter }: SelectDropdownProps) { 
  2.   // 搜索關(guān)鍵詞 
  3.   const [keyword, setKeyword] = React.useState<string | undefined>(undefined); 
  4.   // 使用過(guò)濾條件篩選數(shù)據(jù) 
  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 即副作用。如果沒(méi)有必要,盡量減少 useEffect 的使用。React 官方將這個(gè) API 的使用場(chǎng)景歸納為改變 DOM、添加訂閱、異步任務(wù)、記錄日志等。先來(lái)看一段代碼:

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

上面代碼為了保持 labelList 與 value 的響應(yīng),使用了 useEffect。也許你現(xiàn)在看這個(gè)代碼的本身能正常執(zhí)行。如果現(xiàn)在有個(gè)需求:labelList 變化后也同步到 value,字面理解下你可能會(huì)寫(xiě)出如下代碼:

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

你會(huì)發(fā)現(xiàn)應(yīng)用進(jìn)入了永久循環(huán)中,瀏覽器失去控制,這就是沒(méi)必要的 useEffect ??梢岳斫鉃椴蛔龈淖? DOM、添加訂閱、異步任務(wù)、記錄日志等場(chǎng)景的操作,就盡量別用 useEffect,比如監(jiān)聽(tīng) state 再改變別的 state。結(jié)局就是應(yīng)用復(fù)雜度達(dá)到一定程度,不是瀏覽器先崩潰,就是開(kāi)發(fā)者崩潰。

那有好的方式解決嗎?我們可以將邏輯理解為 動(dòng)作 + 狀態(tài)。其中 狀態(tài) 的變更只能由 動(dòng)作 觸發(fā)。這就能很好解決上面代碼中的問(wèn)題,將 labelList 的狀態(tài)提升,找出改變 value 的 動(dòng)作,封裝一個(gè)聯(lián)動(dòng)改變 labelList 的方法給各個(gè) 動(dòng)作,越復(fù)雜的場(chǎng)景這種模式越高效。

通用性原則

通用性設(shè)計(jì)其實(shí)是一定意義上放棄對(duì) DOM 的掌控,而將 DOM 結(jié)構(gòu)的決定權(quán)轉(zhuǎn)移給開(kāi)發(fā)者,比如預(yù)留自定義渲染。

舉個(gè)例子, antd 中的 Table通過(guò) render 函數(shù)將每個(gè)單元格渲染的決定權(quán)交給使用者,這樣極大提高了組件的可擴(kuò)展性:

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

 優(yōu)秀的組件,會(huì)通過(guò)參數(shù)預(yù)設(shè)默認(rèn)的渲染行為,同時(shí)支持自定義渲染。

統(tǒng)一 API

當(dāng)各個(gè)組件數(shù)量變多之后,組件與組件直接可能存在某種契合的關(guān)系,我們可以統(tǒng)一某種行為 API 的一致性,這樣可以降低使用者對(duì)各個(gè)組件 API 名稱的心智負(fù)擔(dān)。否則組件傳參就會(huì)如同一根一根數(shù)面條一樣痛苦。

舉個(gè)例子,經(jīng)典的 value 與 onChange 的 API 可以在各個(gè)不同的表單域上出現(xiàn)。可以通過(guò)包裝的方式導(dǎo)出更多高階組件,這些高階組件又可以被表單管理組件所容納。

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

不可變狀態(tài)

對(duì)于函數(shù)式編程范式的 React 來(lái)說(shuō),不可變狀態(tài)與單向數(shù)據(jù)流是其核心概念。如果一個(gè)復(fù)雜的組件手動(dòng)保持不可變狀態(tài)繁雜程度也是相當(dāng)高,這里推薦使用 immer 做不可變數(shù)據(jù)管理。如果一個(gè)對(duì)象內(nèi)部屬性變化了,那么整個(gè)對(duì)象就是全新的,不變的部分會(huì)保持引用,這樣天生契合 React.memo 做淺對(duì)比,減少 shouldComponentUpdate 比較的性能消耗。

注意陷阱

React 在某個(gè)意義上說(shuō)一個(gè)狀態(tài)機(jī),每次 render 所定義的變量會(huì)重新聲明。

Context 陷阱

  1. exportfunction ThemeProvider(props) { 
  2.   const [theme, switchTheme] = useState(redTheme); 
  3.  
  4.   // 這里每一次渲染 ThemeProvider, 都會(huì)創(chuàng)建一個(gè)新的 value 從而導(dǎo)致強(qiáng)制渲染所有使用該 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 方法里創(chuàng)建函數(shù),那么使用 render props 會(huì)抵消使用 React.memo 帶來(lái)的優(yōu)勢(shì)。因?yàn)闇\比較 props 的時(shí)候總會(huì)得到 false,并且在這種情況下每一個(gè) render 對(duì)于 render props 將會(huì)生成一個(gè)新的值。

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

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

社區(qū)實(shí)踐

高階組件/裝飾器模式

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

裝飾器模式是在不改變?cè)瓕?duì)象的基礎(chǔ)上,通過(guò)對(duì)其進(jìn)行包裝擴(kuò)展(添加屬性或方法),使原有對(duì)象可以滿足用戶的更復(fù)雜需求,滿足開(kāi)閉原則,也不會(huì)破壞現(xiàn)有的操作。組件是將 props 轉(zhuǎn)化成 UI ,然而高階組件將一個(gè)組件轉(zhuǎn)化成另外一個(gè)組件。

例如漫威電影中的鋼鐵俠,本身就是一個(gè)普通人,可以行走、跳躍。經(jīng)過(guò)戰(zhàn)衣的裝飾,可以跑得更快,還具備飛行能力。

在普通組件中包裝一個(gè) withRouter(react-router),就具備了操作路由的能力。包裝一個(gè) connect(react-redux),就具備了操作全局?jǐn)?shù)據(jù)的能力。

Render Props

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

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

鏈?zhǔn)?Hooks

在 React Hooks 時(shí)代,高階組件和 render props 使用頻率會(huì)下降很多,很多場(chǎng)景下會(huì)被 hooks 所替代。

我們看看 hooks 的規(guī)則:

  • 只在最頂層使用 Hook
  • 不在循環(huán),條件或嵌套函數(shù)中調(diào)用 Hook
  • 只在 React 函數(shù)中調(diào)用 Hook

hook 都是按照一定的順序調(diào)用,因?yàn)槠鋬?nèi)部使用鏈表實(shí)現(xiàn)。我們可以通過(guò) 單一職責(zé) 的概念將每個(gè) hook 作為模塊去呈現(xiàn),通過(guò)組合自定義 hook 就可以實(shí)現(xiàn)漸進(jìn)式功能增強(qiáng)。如同 rxjs 一樣具備鏈?zhǔn)秸{(diào)用的同時(shí)又可以操作其狀態(tài)與生命周期。

示例:

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

用過(guò)語(yǔ)義化組合可以選擇使用需要的 hooks 來(lái)創(chuàng)造出適應(yīng)各個(gè)需求的自定義組件。在某種意義上說(shuō)最小單元不止是組件,也可以是自定義 hooks。

結(jié)語(yǔ)

 

希望每個(gè)人都能寫(xiě)出高質(zhì)量的組件。

 

責(zé)任編輯:姜華 來(lái)源: 前端星辰
相關(guān)推薦

2021-12-07 08:16:34

React 前端 組件

2022-05-13 08:48:50

React組件TypeScrip

2023-12-21 10:26:30

??Prettier

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查詢編寫(xiě)

2021-01-04 07:57:07

C++工具代碼

2019-09-20 15:47:24

代碼JavaScript副作用

2022-08-09 13:22:26

Hooksreactvue

2020-05-14 09:15:52

設(shè)計(jì)模式SOLID 原則JS

2022-05-30 21:47:21

技術(shù)目標(biāo)PRD

2017-05-17 15:50:34

開(kāi)發(fā)前端react

2020-05-08 14:45:00

JS代碼變量

2020-07-15 08:17:16

代碼

2020-05-11 15:23:58

CQRS代碼命令

2021-09-01 08:55:20

JavaScript代碼開(kāi)發(fā)

2013-06-07 14:00:23

代碼維護(hù)

2017-03-15 13:41:16

數(shù)據(jù)庫(kù)SQL調(diào)試

2024-02-23 08:57:42

Python設(shè)計(jì)模式編程語(yǔ)言
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)