<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【重構(gòu)】使用 Hooks 讓代碼更易于變更

          共 7313字,需瀏覽 15分鐘

           ·

          2020-10-18 08:11

          作者:alisecued

          來源:SegmentFault 思否社區(qū)




          重構(gòu)過程中,肯定會遇到新的代碼如何做技術(shù)選型的問題,要考慮到這套技術(shù)的生命力,也就是他是否是更新的技術(shù),還有他的靈活和拓展性,期望能夠達(dá)到在未來至少 3 年內(nèi)不需要做大的技術(shù)棧升級。我的這次重構(gòu)經(jīng)歷是把 jQuery 的代碼變?yōu)?React ,你品品,算是最難,勞動最密集的重構(gòu)任務(wù)了吧。看多了之前代碼動輒上千行的 Class ,混亂的全局變量使用,越來越覺得,代碼一定要寫的簡單,不要使用過多的黑科技,尤其是各種設(shè)計模式,為了復(fù)用而迭代出來的海量 if 判斷。代碼不是給機(jī)器看的,是給人看的,他需要讓后來人快速的看懂,還要能讓別人在你的代碼的基礎(chǔ)上快速的迭代新的需求。所以我們需要想清楚,用什么技術(shù)棧,怎么組織代碼。




          為什么要用 Function Component


          對于 Class Component 和 Function Component 之爭由來已久。從我自身的實踐來看,我覺得這是兩種不同的編程思路。


          Class Component?面向?qū)ο缶幊?/th>繼承生命周期
          Function Component?函數(shù)式編程組合數(shù)據(jù)驅(qū)動


          為什么不用 Class


          首先,如果我們使用面向?qū)ο筮@種編程方式,我們要注意,他不只是定義一個 Class 那么簡單的事情,我們知道面向?qū)ο笥腥筇匦?,繼承,封裝,多態(tài)。


          首先前端真的適合繼承的方式嗎?準(zhǔn)確的說,UI 真的適合繼承的方式嗎?在真實世界里,抽象的東西更適合定義成一個類,類本來的意思就是分類和類別,正如我們把老虎,貓,獅子這些生物統(tǒng)稱為動物,所以我們就可以定義一個動物的類,但是真實世界并沒有動物這種實體,但是頁面 UI 都是真實存在可以看到的東西,我們可以把一個頁面分成不同的區(qū)塊,然后區(qū)塊之間采用的是「組合」的方式。因此我認(rèn)為 UI 組件不適合繼承,更應(yīng)該組合。如果你寫過繼承類的組件,你將很難去重構(gòu),甚至是重寫他。


          封裝講究使用封裝好的方法對外暴露類中的屬性,但是我們的組件基本是通過 props 暴露內(nèi)部事件和數(shù)據(jù),通過 Ref 暴露內(nèi)部方法,本質(zhì)上并沒有使用封裝的特性。


          多態(tài)就更少用了,多態(tài)更多是基于接口,或者抽象類的,但是 JS 這塊比較弱,用 TS 或許會好一些。


          綜上,作為前端 UI 編程,我更傾向于使用函數(shù)組合的方式。




          為什么要用數(shù)據(jù)變化驅(qū)動


          不論是在 React 或者在 Vue 里,都講究數(shù)據(jù)的變化,數(shù)據(jù)與視圖的綁定關(guān)系,數(shù)據(jù)驅(qū)動,數(shù)據(jù)的變化引起 UI 的重新渲染,但是生命周期在描述這個問題的時候,并不直接,在 Class Component 里,我們?nèi)绾螜z測某個數(shù)據(jù)的變化呢,基本是用 shouldUpdate 的生命周期,為什么我們在編程的時候,正在關(guān)注數(shù)據(jù)和業(yè)務(wù)的時候,還要關(guān)心一個生命周期呢,這部分內(nèi)容對于業(yè)務(wù)來說更像是副作用,或者不應(yīng)該暴露給開發(fā)者的。


          綜上,是我認(rèn)為 Function Component + Hooks 編程體驗更好的地方,但是這也只是一個相對片面的角度,并沒有好壞之分,畢竟連 React 的官方都說,兩種寫法沒有好壞之分,性能差距也幾乎可以忽略,而且 React 會長期支持這兩種寫法。


          hooks:真正的響應(yīng)式編程


          到底是什么是響應(yīng)式編程?大家各執(zhí)一詞,模模糊糊,懵懵懂懂。很多人沒有把他的本質(zhì)說明白。從我多年的編程經(jīng)驗來看,響應(yīng)式編程就是「使用異步數(shù)據(jù)流編程」。我們來看看前端在處理異步操作的時候通常是怎么做的,常見的異步操作有異步請求和頁面的鼠標(biāo)操作事件,在處理這樣的操作的時候,我們通常采取的方法是事件循環(huán),也就是異步事件流的方式。但是事件循環(huán)并沒有顯式的解決事件依賴問題,而是需要我們自己在編碼的時候做好調(diào)用順序的管理,比如:


          const?x?=?1;
          const?a?=?(x)?=>?new?Promise((r,?j)=>{
          ??const?y?=?x?+?1;
          ????r(y);
          });
          const?b?=?(y)?=>?new?Promise((r,?j)=>{
          ??const?z?=?y?+?1;
          ????r(z);
          });
          const?c?=?(z)?=>??new?Promise((r,?j)=>{
          ??const?w?=?z?+?1;
          ????r(w);
          });
          //?上面是三個異步請求,他們之間有依賴關(guān)系,我們通常的操作是
          a(x).then((y)=>{
          ????b(y).then((z)=>{
          ??????c(z).then((w)=>{
          ??????????//?最終的結(jié)果
          ??????console.log(w);
          ??????})
          ??})
          })


          上述的基于事件流的回調(diào)方式,我們使用 Hooks 來替換的話,就是這樣的:


          import?{?useState,?useEffect?}?from?'react';

          const?useA?=?(x)?=>?{
          ????const?[y,?setY]?=?useState();
          ??useEffect(()=>{
          ????//?假設(shè)此處包含異步請求
          ??????setY(x?+?1);
          ??},?[x]);
          ??return?y;
          }

          const?useB?=?(y)?=>?{
          ????const?[z,?setZ]?=?useState();
          ??useEffect(()=>{
          ????//?假設(shè)此處包含異步請求
          ??????setZ(y?+?1);
          ??},?[y]);
          ??return?z;
          }

          const?useC?=?(z)?=>?{
          ????const?[w,?setW]?=?useState();
          ??useEffect(()=>{
          ????//?假設(shè)此處包含異步請求
          ??????setW(z?+?1);
          ??},?[z]);
          ??return?w;
          }

          //?上面是三個是自定義?Hooks,他表明了每個變量數(shù)據(jù)之間的依賴關(guān)系,你甚至不需要
          //?知道他們每個異步請求的返回順序,只需要知道數(shù)據(jù)是否發(fā)生了變化。
          const?x?=?1;
          const?y?=?useA(x);
          const?z?=?useB(y);
          const?w?=?useC(z);
          //?最終的結(jié)果
          console.log(w);


          我們從上面的例子看到, Hooks 的寫法,簡直就像是在進(jìn)行簡單的過程式編程一樣,步驟化,邏輯清晰,而且每個自定義 Hooks 你可以把他理解為一個函數(shù),他不需要與外界共享狀態(tài),他是自封閉的,可以很方便的進(jìn)行測試。




          開始精簡代碼


          我們基于 React Hooks 提供的工具和上面講的響應(yīng)式編程的思維,開始我們的精簡代碼之旅,這次旅程可以概括為:遇到千行代碼文件怎么辦?拆分最有效!怎么拆分?先按照功能模塊來分文件,這里的功能模塊是指相同的語法結(jié)構(gòu),比如副作用函數(shù),事件處理函數(shù)等。單個文件內(nèi)可以按照具體實現(xiàn)寫多個自定義 Hooks 和函數(shù)。這樣做的最終目的就是,讓主文件里只保留這個組件要實現(xiàn)的業(yè)務(wù)邏輯的步驟。



          為什么會有上千行的單個代碼文件?


          如果我們把一個組件的所有代碼都寫到一個組件里,那么極有可能會出現(xiàn)一個文件里有上千行代碼的情況,如果你用的是 Function Component 來寫這個組件的話,那么就會出現(xiàn)一個函數(shù)里有上千行代碼的情況。當(dāng)然上千行代碼的文件對于一個健全的開發(fā)者來說都是不可忍受的,對于后來的重構(gòu)者來說也是一個大災(zāi)難。


          為什么要把這個代碼都放到一個文件里?拆分下不香嗎?那下面的問題就變成了如何拆分一個組件,要拆分一個組件,我們要先知道一個典型的組件是什么樣子的。



          一個典型的組件


          Hooks 是個新東西,他像函數(shù)一樣靈活,甚至不包含我選用了上面的方式來編寫新的代碼,那我們來看看一個典型的基于 Function Component + Hooks 的組件包含什么?


          import?React,?{?useState,?useEffect?}?from?'react';
          import?PropTypes?from?'prop-types';
          import?{
          ??Row,?Select,
          }?from?'antd';
          import?Service?from?'@/services';

          let?originList?=?[];
          const?Demo?=?({
          ??onChange,
          ??value,
          ??version,
          })?=>?{
          ??//?狀態(tài)管理
          ??const?[list,?setList]?=?useState([]);

          ??//?副作用函數(shù)?
          ??useEffect(()?=>?{
          ????const?init?=?async?()?=>?{
          ????????const?list?=?await?Service.getList(version);
          ????????originList?=?list;
          ????????setList(list);
          ????};
          ????init();
          ??},?[]);

          ??//?事件?handler
          ??const?onChangeHandler?=?useCallback((data)?=>?{
          ????const?item?=?{?...val,?value:?val.code,?label:?val.name?};
          ????onChange(item);
          ??},?[onChange]);
          ??
          ??const?onSearchHandler?=?useCallback((val)?=>?{
          ????if?(val)?{
          ??????const?listFilter?=?originList.filter(item?=>?item.name.indexOf(val)?>?-1);
          ??????setList(listFilter);
          ????}?else?{
          ??????setList(originList);
          ????}
          ??},?[]);
          ??
          ??//?UI?組件渲染
          ??return?(
          ????
          ????????????????labelInValue
          ????????showSearch
          ????????filterOption={false}
          ????????value={value}
          ????????onSearch={onSearchHandler}
          ????????onChange={onChangeHandler}
          ????????>
          ?????????{list.map(option?=>?({option.name}))}
          ????????
          ????

          ??);
          };

          export?default?Demo;


          從上面的例子我們可以看出,一個基本的 Function Component 包含哪些功能模塊:


          • useState 為主的狀態(tài)管理
          • useEffect 為主的副作用管理
          • useCallback 為主的事件 handler
          • UI 部分
          • 轉(zhuǎn)換函數(shù),用于請求返回數(shù)據(jù)的轉(zhuǎn)換,或者一些不具有通用性的工具函數(shù)

          拆分功能模塊


          首先,我們把上面講到的功能模塊拆分成多個文件:

          |—?container
          ????????|—?hooks.js?//?各種自定義的?hooks
          ??????|—?handler.js?//?轉(zhuǎn)換函數(shù),以及不需要?hooks?的事件處理函數(shù)
          ????????|—?index.js?//?主文件,只保留實現(xiàn)步驟
          ????????|—?index.css?//?css?文件

          什么樣的代碼一看就懂?


          我重構(gòu)過太多別人的代碼,但凡遇到那種看著邏輯代碼一大堆放在一起的,我就頭大,后來發(fā)現(xiàn),這些代碼都犯了一個相同的錯誤。沒有分清楚什么是步驟,什么是實現(xiàn)細(xì)節(jié)。當(dāng)你把步驟和細(xì)節(jié)寫在一起的時候,災(zāi)難也就發(fā)生了,尤其是那種長年累月迭代出來的代碼,if 遍地。

          Hooks 是一個做代碼拆分的高效工具,但是他也非常的靈活,業(yè)界一直沒有比較通用行的編碼規(guī)范,但是我有點不同的觀點,我覺得他不需要像 Redux 一樣的模式化的編碼規(guī)范,因為他就是函數(shù)式編程,他遵循函數(shù)式編程的一般原則,函數(shù)式編程最重要的是拆分好步驟和實現(xiàn)細(xì)節(jié),這樣的代碼就好讀,好讀的代碼才是負(fù)責(zé)任的代碼。

          到底怎么區(qū)分步驟和細(xì)節(jié)?有一個很簡單的方法,在你梳理需求的時候,你用一個流程圖把你的需求表示出來,這時候的每個節(jié)點基本就是步驟,因為他不牽扯到具體的實現(xiàn)。解釋太多,有點啰嗦了,相信你肯定懂,對吧。

          步驟和細(xì)節(jié)分清楚以后,對重構(gòu)也有很大的好處,因為每個步驟都是一個函數(shù),不會有像 class 中 this 這種全局變量,當(dāng)你需要刪除一個步驟或者重寫這個步驟的時候,不用影響到其他步驟函數(shù)。

          同樣,函數(shù)化以后,無疑單元測試就變得非常簡單了。

          按照步驟拆分主文件


          目的是主文件里只保留業(yè)務(wù)步驟。

          import?React,?{?useState,?useEffect?}?from?'react';
          import?PropTypes?from?'prop-types';
          import?{
          ??Row,?Select,
          }?from?'antd';
          import?{?onChangeHandler?}?from?'./handler';
          import?{?useList?}?from?'./hooks';
          import?Service?from?'@/services';

          const?Demo?=?({
          ??onChange,
          ??value,
          ??version,
          })?=>?{
          ??//?list?狀態(tài)的操作,其中有搜索改變?list?
          ??const?[originList,?list,?onSearchHandler]?=?useList(version);
          ??
          ??//?UI?組件渲染
          ??return?(
          ????
          ????????????labelInValue
          ??????showSearch
          ??????filterOption={false}
          ??????value={value}
          ??????onSearch={onSearchHandler}
          ??????onChange={()?=>?onChangeHandler(originList,?data,?onChange)}
          ??????>
          ????????{list.map(option?=>?({option.name}))}
          ??????
          ????

          ??);
          };

          export?default?Demo;

          看到上面是基于步驟和細(xì)節(jié)分離的思路,將上面的組件做了一次重構(gòu),只包含兩步:

          • 對 list 數(shù)據(jù)的操作
          • UI 渲染

          通過拆分以后主文件代碼里就只包含一些步驟了,全部使用自定義的 hooks 替換了,自定義的 hooks 可以寫到 hooks.js 文件中。

          hooks.js 里文件內(nèi)容如下:

          import?{?useState,?useEffect,?useCallback?}?from?'react';

          let?originList?=?[];
          export?const?useList?=?(version)?=>?{
          ??//?狀態(tài)管理
          ??const?[list,?setList]?=?useState([]);
          ???//?副作用函數(shù)?
          ??useEffect(()?=>?{
          ????const?init?=?async?()?=>?{
          ????????const?list?=?await?Service.getList(version);
          ????????originList?=?list;
          ????????setList(list);
          ????};
          ????init();
          ??},?[]);
          ??
          ??//?處理?select?搜索
          ??const?onSearchHandler?=?useCallback((val)?=>?{
          ????if?(val)?{
          ??????const?listFilter?=?originList.filter(item?=>?item.name.indexOf(val)?>?-1);
          ??????setList(listFilter);
          ????}?else?{
          ??????setList(originList);
          ????}
          ??},?[]);
          ??
          ??return?[originList,?list,?onSearchHandler];
          }

          可以看到 hooks.js 文件里包含的就是數(shù)據(jù)和改變數(shù)據(jù)的方法,所有的副作用函數(shù)都包含在里面。同時建議所有的異步請求都是用 await 來處理。啥好處可以自行 Google。

          handler.js 文件內(nèi)容如下:

          //?事件?handler
          export?const?onChangeHandler?=?(originList,?data,?onChange)?=>?{
          ??const?val?=?originList.find(option?=>?(option.id?===?data.value));
          ??const?item?=?{?...val,?value:?val.code,?label:?val.name?};
          ??onChange(item);
          };

          上面的例子非常簡單,你可能覺得根本不需要這樣重構(gòu),因為本來代碼量就不大,這樣拆分增加了太多文件。很好!這樣抬杠說明你有了思考,我同意你的觀點,一些簡單的組件根本不需要如此拆分,但是我將這種重構(gòu)方法不是一種規(guī)范,不是一種強(qiáng)制要求,相反他是一種價值觀,一種對于什么是好的代碼的價值觀。這種價值觀歸根結(jié)底就是一句話:讓你的代碼易于變更。Easier To Change! 簡稱 ETC。



          編碼價值觀 ETC


          ETC 這種編碼的價值觀是很多好的編碼原則的本質(zhì),比如單一職責(zé)原則,解耦原則等,他們都體現(xiàn)了 ETC 這種價值觀念。能適應(yīng)使用者的就是好的設(shè)計,對于代碼而言,就是要擁抱變化,適應(yīng)變化。因此我們需要信奉 ETC 。價值觀念是幫助你在寫代碼的時候做決定的,他告訴你應(yīng)該做這個?還是做那個?他幫助你在不同編碼方式之間做選擇,他甚至應(yīng)該成為你編碼時的一種潛意識,如果你接受這種價值觀,那么在編碼的時候,請時刻提醒自己,遵循這種價值觀。



          參考:

          • 應(yīng)式編程?

            https://zhuanlan.zhihu.com/p/27678951

          • 《程序員修煉之道》Andrew Hunt, David Thomas




          點擊左下角閱讀原文,到?SegmentFault 思否社區(qū)?和文章作者展開更多互動和交流。


          -?END -

          瀏覽 39
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  夜夜艹 | 黄色成人在线 | 黑丝3级黄片 | 国产综合色婷婷精品久久 | 女人被男人操黄色午夜网止 |