2020-09-07

減少前端代碼耦合

https://zhuanlan.zhihu.com/p/24495650
原作者:李銀城
什么是代碼耦合?代碼耦合的表現(xiàn)是改了一點(diǎn)毛發(fā)而牽動(dòng)了全身,或者是想要改點(diǎn)東西逢艘,需要在一堆代碼里面找半天。由于前端需要組織js/css/html,耦合的問(wèn)題可能會(huì)更加明顯,下面按照耦合的情況分別說(shuō)明:

1. 避免全局耦合

這應(yīng)該是比較常見的耦合汞贸。全局耦合就是幾個(gè)類、模塊共用了全局變量或者全局?jǐn)?shù)據(jù)結(jié)構(gòu)印机,特別是一個(gè)變量跨了幾個(gè)文件矢腻。例如下面,在html里面定義了一個(gè)變量:

<script>
    var PAGE = 20;
</script>

<script src="main.js"></script>

上面在head標(biāo)簽里面定義了一個(gè)PAGE的全局變量射赛,然后在main.js里面使用踏堡。這樣子PAGE就是一個(gè)全局變量,并且跨了兩個(gè)文件咒劲,一個(gè)html,一個(gè)js诫隅。然后在main.js里面突然冒出來(lái)了個(gè)PAGE的變量腐魂,后續(xù)維護(hù)這個(gè)代碼的人看到這個(gè)變量到處找不到它的定義,最后找了半天發(fā)現(xiàn)原來(lái)是在xxx.html的head標(biāo)簽里面定義了逐纬。這樣就有點(diǎn)egg pain了蛔屹,并且這樣的變量容易和本地的變量發(fā)生命名沖突。

所以如果需要把數(shù)據(jù)寫在頁(yè)面上的話豁生,一個(gè)改進(jìn)的辦法是在頁(yè)面寫一個(gè)form兔毒,數(shù)據(jù)寫成form里面的控件數(shù)據(jù),如下:

<form id="page-data">
    <input type="hidden" name="page" value="2">
    <textarea name="list" style="display:none">[{"userName": ""yin"},{}]</textarea>
</form>

上面使用了input和textarea甸箱,使用textarea的優(yōu)點(diǎn)是支持特殊符號(hào)育叁。再把form的數(shù)據(jù)序列化,序列化也是比較簡(jiǎn)單的芍殖,可以查看Effective前端2:優(yōu)化html標(biāo)簽

第二種是全局?jǐn)?shù)據(jù)結(jié)構(gòu)豪嗽,這種可能會(huì)使用模塊化的方法,如下:

//data.js
module.exports = {
    houseList: null
}

//search.js 獲取houseList的數(shù)據(jù)
var data = require("data");
data.houseList = ajax();
require("format-data").format();

//format-data.js 對(duì)houseList的數(shù)據(jù)做格式化
function format(){
    var data = require("data");
    process(data);
    require("show-result").show();
}

//show-result.js 將數(shù)據(jù)顯示出來(lái)
function show(){
    showData(require("data").houseList)
}

上面四個(gè)模塊各司其職豌骏,乍一眼看上去好像沒(méi)什么問(wèn)題龟梦,但是他們都用了一個(gè)data的模塊共用數(shù)據(jù)。這樣確實(shí)很方便窃躲,但是這樣就全局耦合了计贰。因?yàn)橛玫耐粋€(gè)data,所以你無(wú)法保證蒂窒,其它人也會(huì)加載了這個(gè)模塊然后做了些修改躁倒,或者是在你的某一個(gè)業(yè)務(wù)的異步回調(diào)也改了這個(gè)荞怒。第二個(gè)問(wèn)題:你不知道這個(gè)data是從哪里來(lái)的,誰(shuí)可能會(huì)對(duì)它做了修改樱溉,這個(gè)過(guò)程對(duì)于后續(xù)的模塊來(lái)說(shuō)都是不透明的挣输。

所以這種應(yīng)該考慮使用傳參的方式,降低耦合度福贞,把data作為一個(gè)參數(shù)傳遞:

//去掉data.js
//search.js 獲取數(shù)據(jù)并傳遞給下一個(gè)模塊
var houseList = ajax();
require("format-data").format(houseList);

//format-data.js 對(duì)houseList的數(shù)據(jù)做格式化
function format(houseList){
    process(houseList);
    require("show-result").show(houseList);
}

//show-result.js 將數(shù)據(jù)顯示出來(lái)
function show(houseList){
    showData(houseList)
}

可以看到撩嚼,search里面獲取到data后,交給format-data處理挖帘,format-data處理完之后再給show-result完丽。這樣子就很清楚地知道數(shù)據(jù)的處理流程,并且保證了houseList不會(huì)被某個(gè)異步回調(diào)不小心改了拇舀。如果單獨(dú)從某個(gè)模塊來(lái)說(shuō)逻族,show-result這個(gè)模塊并不需要關(guān)心houseList的經(jīng)過(guò)了哪些流程和處理,它只需要關(guān)心輸入是符合它的格式要求的就可以骄崩。

這個(gè)時(shí)候你可能會(huì)有一個(gè)問(wèn)題:這個(gè)data被逐層傳遞了這么多次聘鳞,還不如像最上面的那樣寫一個(gè)data的模塊,大家都去改那里要拂,豈不是簡(jiǎn)單了很多抠璃?對(duì),這樣是簡(jiǎn)單了脱惰,但是一個(gè)數(shù)據(jù)結(jié)構(gòu)被跨了幾個(gè)文件使用搏嗡,這樣會(huì)出現(xiàn)我上面說(shuō)的問(wèn)題。有時(shí)候可能出現(xiàn)一些意想不到的情況拉一,到時(shí)候可能得找bug找個(gè)半天采盒。所以這種解耦是值得的,除非你定義的變量并不會(huì)跨文件蔚润,它的作用域只在它所在的文件磅氨,這樣會(huì)好很多〕槁担或者是data是常量的悍赢,data里面的數(shù)據(jù)定義好之后值就再也不會(huì)改變,這樣應(yīng)當(dāng)也是可取的货徙。

2. js/css/html的耦合

這種耦合在前端里面應(yīng)該最常見左权,因?yàn)檫@三者通常具有交集,需要使用js控制樣式和html結(jié)構(gòu)痴颊。如果使用js控制樣式赏迟,很多人都喜歡在js里面寫樣式,例如當(dāng)頁(yè)面滑動(dòng)到某個(gè)地方之后要把某個(gè)條吸頂:

image

頁(yè)面滑到下面那個(gè)灰色的條再繼續(xù)往下滑的時(shí)候蠢棱,那個(gè)灰色條就要保持吸頂狀態(tài):

image

可能不少人會(huì)這么寫:

$(".bar").css({
    position: fixed;
    top: 0;
    left: 0;
});

然后當(dāng)用戶往上滑的時(shí)候取消fixed:

$(".bar").css({
    position: static;
});

如果你用react锌杀,你可能會(huì)設(shè)置一個(gè)style的state數(shù)據(jù)甩栈,但其實(shí)這都一樣,都把css雜合到j(luò)s里面了糕再。某個(gè)想要檢查你樣式的人量没,想要給你改個(gè)bug,他檢查瀏覽器發(fā)現(xiàn)有個(gè)標(biāo)簽style里的屬性突想,然后他找半天找不到是在哪里設(shè)置的殴蹄,最后他發(fā)現(xiàn)是在某個(gè)js的某個(gè)隱蔽的角落設(shè)置了。你在js里面設(shè)置了樣式猾担,然后css里面也會(huì)有樣式袭灯,在改css的時(shí)候,如果不知道js里面也有設(shè)置了樣式绑嘹,那么可能會(huì)發(fā)生沖突稽荧,在某種條件下觸發(fā)了js里面設(shè)置樣式。

所以不推薦直接在js里面更改樣式屬性工腋,而應(yīng)該通過(guò)增刪類來(lái)控制樣式姨丈,這樣子樣式還是回歸到css文件里面。例如上面可以改成這樣:

//增加fixed
$(".bar").addClass("fixed");

//取消fixed
$(".bar").removeClass("fixed");

fixed的樣式:

.bar.fixed{
    position: fixed;
    left: 0;
    top: 0;
}

可以看到擅腰,這樣的邏輯就非常清晰构挤,并且回滾fixed,不需要把它的position還原為static惕鼓,因?yàn)樗灰欢ㄊ莝tatic,也有可能是relative唐础,這種方式在取消掉一個(gè)類的時(shí)候箱歧,不需要去關(guān)心原本是什么,該是什么就會(huì)是什么一膨。

但是有一種是避免不了的呀邢,就是監(jiān)聽scroll事件或者mousemove事件,動(dòng)態(tài)地改變位置豹绪。

這種通過(guò)控制類的方式還有一個(gè)好處价淌,就是當(dāng)你給容器動(dòng)態(tài)地增刪一個(gè)類時(shí),你可以借助子元素選擇器瞒津,用這個(gè)類控制它的子元素的樣式,也是很方便。

還有很多人可能會(huì)覺(jué)得html和css/js脫耦镀钓,那就是不能在html里面寫style啦扬,不能在html里面寫script標(biāo)簽,但是凡事都不是絕對(duì)的屁柏,如果有一個(gè)標(biāo)簽啦膜,它和其它標(biāo)簽就一個(gè)font-size不一樣有送,那你直接給它寫一個(gè)font-size的內(nèi)聯(lián)樣式,又何嘗不可呢僧家,在性能上來(lái)說(shuō)雀摘,如果你寫個(gè)class,它還得去匹配這個(gè)class八拱,比不上style高效吧阵赠。或者是你這個(gè)html文件就那么20乘粒、30行css豌注,那直接在head標(biāo)簽加個(gè)style,直接寫在head里面好了灯萍,這樣你就少管理了一個(gè)文件轧铁,并且瀏覽器不用去加載一個(gè)外鏈的文件。

有時(shí)候直接在html寫script標(biāo)簽是必要的旦棉,它的優(yōu)勢(shì)也是不用加載外鏈文件齿风,處理速度會(huì)很快,幾乎和dom渲染同時(shí)绑洛,這個(gè)在解決頁(yè)面閃動(dòng)的時(shí)候比較有用救斑。因?yàn)槿绻胘s動(dòng)態(tài)地改變已經(jīng)加載好的dom,放在外鏈里面肯定會(huì)閃一下真屯,而直接寫的script就不會(huì)有這個(gè)問(wèn)題脸候,即使這個(gè)script是放在了body的后面。例如下面:

image

原始數(shù)據(jù)是帶p標(biāo)簽的绑蔫,但是在textarea里面展示的時(shí)候需要把p改成換行\(zhòng)r\n运沦,如果在dom渲染之后再在外鏈里面更新dom就會(huì)出現(xiàn)上面的閃動(dòng)的情況。你可能會(huì)說(shuō)我用react配深,數(shù)據(jù)都是動(dòng)態(tài)渲染的携添,渲染前已經(jīng)處理好了,不會(huì)出現(xiàn)上面的情況篓叶。那么烈掠,好吧,至少你了解一下吧缸托。

和耦合相對(duì)的是內(nèi)聚左敌,寫代碼的原則就是低耦合、高聚合俐镐。所謂內(nèi)聚就是說(shuō)一個(gè)模塊的職責(zé)功能十分緊密母谎,不可分割,這個(gè)模塊就是高內(nèi)聚的京革。我們先從重復(fù)代碼說(shuō)起:

3. 減少重復(fù)代碼

假設(shè)有一段代碼在另外一個(gè)地方也要被用到奇唤,但又不太一樣幸斥,那么最簡(jiǎn)單的方法當(dāng)然是copy一下,然后改一改咬扇。這也是不少人采取的辦法甲葬,這樣就導(dǎo)致了:如果以后要改一個(gè)相同的地方就得同時(shí)改好多個(gè)地方,就很麻煩了懈贺。

例如有一個(gè)搜索的界面:

image

用戶可以通過(guò)點(diǎn)擊search按鈕觸發(fā)搜索经窖,也可以通過(guò)點(diǎn)擊下拉或者通過(guò)輸入框的change觸發(fā)搜索,所以你可能會(huì)這么寫:

$("#search").on("click", function(){
    var formData = getFormData();
    $.ajax({
        url: '/search',
        data: formData,
        success: function(data){
            showResult(data);
        }
    });
});

在change里面又重新發(fā)請(qǐng)求:

$("input").on("change", function(){
    //把用戶的搜索條件展示進(jìn)行改變
    changeInputFilterShow();
    var formData = getFormData();
    $.ajax({
        url: '/search',
        data: formData,
        success: function(data){
            showResult(data);
        }
    });
});

change里面需要對(duì)搜索條件的展示進(jìn)行更改梭灿,和click事件不太一樣画侣,所以圖一時(shí)之快就把代碼拷了一下。但是這樣是不利于代碼的維護(hù)的堡妒,所以你可能會(huì)想到把獲取數(shù)據(jù)和發(fā)請(qǐng)求的那部分代碼單獨(dú)抽離封裝在一個(gè)函數(shù)配乱,然后兩邊都調(diào)一下:

function getAndShowData(){
    var formData = getFormData();
    $.ajax({
        url: '/search',
        data: formData,
        success: function(data){
            showResult(data);
        }
    });
}

$("#search").on("click", getAndShowData);
$("input").on("change", function(){
    changeInputFilterShow();
    getAndShowData();
});

在抽成一個(gè)函數(shù)的基礎(chǔ)上,又發(fā)現(xiàn)這個(gè)函數(shù)其實(shí)有點(diǎn)大皮迟,因?yàn)檫@里面要獲取表單數(shù)據(jù)搬泥,還要對(duì)數(shù)據(jù)進(jìn)行格式化,用做請(qǐng)求的參數(shù)伏尼。如果用戶觸發(fā)得比較快忿檩,還要記錄上次請(qǐng)求的xhr,在每次發(fā)請(qǐng)求前cancle掉上一次的xhr爆阶,并且可能對(duì)請(qǐng)求做一個(gè)loading效果燥透,增加用戶體驗(yàn),還要對(duì)出錯(cuò)的情況進(jìn)行處理辨图,全部都要在ajax里面兽掰。所以最好對(duì)getAndShowData繼續(xù)拆分,很自然地會(huì)想到把它分離成一個(gè)模塊徒役,一個(gè)單獨(dú)的文件,叫做search-ajax窖壕。所有發(fā)請(qǐng)求的處理都在這個(gè)模塊里面統(tǒng)一操作忧勿。對(duì)外只提供一個(gè)search.ajax的接口,傳的參數(shù)為當(dāng)前的頁(yè)數(shù)即可瞻讽。所有需要發(fā)請(qǐng)求的都調(diào)一下這個(gè)模塊的這個(gè)接口就好了鸳吸,除了上面的兩種情況,還有點(diǎn)擊分頁(yè)的情景速勇。這樣不管哪種情景都很方便晌砾,我不需要關(guān)心請(qǐng)求是怎么發(fā)的,結(jié)果是怎么處理的烦磁,我只要傳一個(gè)當(dāng)前的頁(yè)數(shù)給你就好了养匈。

再往下哼勇,會(huì)發(fā)現(xiàn),在顯示結(jié)果那里呕乎,即上面代碼的第7行积担,需要對(duì)有結(jié)果、無(wú)結(jié)果的情況分別處理猬仁,所以又搞了一個(gè)函數(shù)叫做showResult帝璧,這個(gè)函數(shù)有點(diǎn)大,它里面的邏輯也比較復(fù)雜湿刽,有結(jié)果的時(shí)候除了更新列表結(jié)果的烁,還要更新結(jié)果總數(shù)、更新分頁(yè)的狀態(tài)诈闺。因此這個(gè)showResult一個(gè)函數(shù)難以擔(dān)當(dāng)大任渴庆。所以把這個(gè)show-result也當(dāng)獨(dú)分離出一個(gè)模塊,負(fù)責(zé)結(jié)果的處理买雾。

到此把曼,我們整一個(gè)search的UML圖應(yīng)該是這樣的:

image

注意上面把發(fā)請(qǐng)求的又再單獨(dú)封裝成了一個(gè)模塊,因?yàn)檫@個(gè)除了搜索發(fā)請(qǐng)求外漓穿,其它的請(qǐng)求也可以用到嗤军。同時(shí)search-result會(huì)用到兩個(gè)展示的模板。

由于不只一個(gè)頁(yè)面會(huì)用到搜索的功能晃危,所以再把上面繼續(xù)抽象叙赚,把它封裝成一個(gè)search-app的模塊,需要用到的頁(yè)面只需require這個(gè)search-app僚饭,調(diào)一下它的init函數(shù)震叮,然后傳些定制的參數(shù)就可以用了。這個(gè)search-app就相當(dāng)于一個(gè)搜索的插件鳍鸵。

所以整一個(gè)的思路是這樣的:出現(xiàn)了重復(fù)代碼 -> 封裝成一個(gè)函數(shù) -> 封裝成一個(gè)模塊 -> 封裝成一個(gè)插件苇瓣,抽象級(jí)別不斷提高,將共有的特性和有差異的地方分離出來(lái)偿乖。當(dāng)你走在抽象與封裝的路上的時(shí)候击罪,那你應(yīng)該也是走在了大神的路上。

當(dāng)然贪薪,如果兩個(gè)東西并沒(méi)有共同點(diǎn)媳禁,但是你硬是要搞在一起,那是不可取的画切。

我這里說(shuō)的封裝并不是說(shuō)竣稽,你一定要使用requirejs、es6的import或者是webpack的require,關(guān)鍵在于你要有這種模塊化的思想毫别,并不是指工具上的娃弓,不管你用的哪一個(gè),只要你有這種抽象的想法拧烦,那都是可取的忘闻。

模塊化的極端是拆分粒度太細(xì),一個(gè)簡(jiǎn)單的功能恋博,明明十行代碼寫在一起就可以搞定的事情齐佳,硬是寫了七、八層函數(shù)棧债沮,每個(gè)函數(shù)只有兩炼吴、三行。這樣除了把你的邏輯搞得太復(fù)雜之外疫衩,并沒(méi)有太多的好處硅蹦。當(dāng)你出現(xiàn)了重復(fù)代碼,或者是一個(gè)函數(shù)太大闷煤、功能太多童芹,又或是邏輯里面寫了三層循環(huán)又再嵌套了三層if,再或是你預(yù)感到你寫的這個(gè)東西其他人也可能會(huì)用到鲤拿,這個(gè)時(shí)候你才考慮模塊化假褪,進(jìn)行拆分比較合適。

上面不管是search-result還是search-ajax他們?cè)诠δ苌隙际歉叨葍?nèi)聚的近顷,每個(gè)模塊都有自己的職責(zé)生音,不可拆分,這在面向?qū)ο缶幊汤锩娼凶鰡我回?zé)職原則窒升,一個(gè)模塊只負(fù)責(zé)一個(gè)功能缀遍。

再舉一個(gè)例子,我在怎樣實(shí)現(xiàn)前端裁剪上傳圖片功能里面提到一個(gè)上傳裁剪的實(shí)現(xiàn)饱须,這里面包含裁剪域醇、壓縮上傳、進(jìn)度條三大功能蓉媳,所以我把它拆成三個(gè)模塊:

image

這里提到的模塊大部分是一個(gè)單例的object譬挚,不會(huì)去實(shí)例它,一般可以滿足大部分的需求督怜。在這個(gè)單例的模塊里面,它自己的“私有”函數(shù)一般是通過(guò)傳參調(diào)用狠角,但是如果需要傳遞的數(shù)據(jù)比較多的時(shí)候号杠,就有點(diǎn)麻煩了,這個(gè)時(shí)候可以考慮把它封裝成一個(gè)類。

3. 封裝成一個(gè)類

在上面的裁剪上傳里面的進(jìn)度條progress-bar姨蟋,一個(gè)頁(yè)面里可能有幾個(gè)要上傳的地方屉凯,每個(gè)上傳的地方都會(huì)有進(jìn)度條,每個(gè)進(jìn)度條都有自己的數(shù)據(jù)眼溶,所以不能像在最上面說(shuō)的悠砚,在一個(gè)文件的最上面定義一些變量然后為這個(gè)模塊里面的函數(shù)共用,只能是通過(guò)傳遞參數(shù)的形式堂飞,即在最開始調(diào)用的時(shí)候定義一些數(shù)據(jù)灌旧,然后一層一層地傳遞下去。如果這些數(shù)據(jù)很多的話就有點(diǎn)麻煩绰筛。

所以稍微變通一下枢泰,把progress-bar封裝成一個(gè)類:

function ProgressBar($container){
    this.$container = $container; //進(jìn)度條外面的容器
    this.$meter = null;           //進(jìn)度條可視部分
    this.$bar = null;             //進(jìn)度條存放可視部分的容器
    this.$barFullWidth = $container.width() * 0.9; //進(jìn)度條的寬度
    this.show();                  //new一個(gè)對(duì)象的時(shí)候就顯示
}

或者你用ES6的class,但是本質(zhì)上是一樣的铝噩,然后這個(gè)ProgressBar的成員函數(shù)就可以使用定義的這些“私有”變量衡蚂,例如設(shè)置進(jìn)度條的進(jìn)度函數(shù):

ProgressBar.prototype.setProgress = function(percentage, time){
    time = typeof time === "undefined" ? 100 : time;
    this.$meter.stop().animate({width: parseInt(this.$barFullWidth * percentage)}, time);
};

這個(gè)使用了兩個(gè)私有變量,如果再加上原先兩個(gè)骏庸,用傳參的方式就得傳四個(gè)毛甲。

使用類是模塊化的一種思想,另外一種常用的還有策略模式具被。

4. 使用策略模式

假設(shè)要實(shí)現(xiàn)下面三個(gè)彈框:

image

這三個(gè)彈框無(wú)論是在樣式上還是在功能上都是一樣的玻募,唯一的區(qū)別是上面標(biāo)題文案是不一樣的。最簡(jiǎn)單的可能是把每個(gè)彈框的html都copy一下硬猫,然后改一改补箍。如果你用react,你可能會(huì)用拆分組件的方式啸蜜,上面一個(gè)組件坑雅,下面一個(gè)組件,那么好吧衬横,你就這樣搞吧裹粤。如果你沒(méi)用react,你可能得想辦法組織下你的代碼蜂林。

如果你有策略模式的思想遥诉,你可能會(huì)想到把上面的標(biāo)題當(dāng)作一個(gè)個(gè)的策略。首先定義不同彈框的類型噪叙,一一標(biāo)志不同的彈框:

var popType = ["register", "favHouse", "saveSearch"];

定義三種popType一一對(duì)應(yīng)上面的三個(gè)彈框矮锈,然后每種popType都有對(duì)應(yīng)的文案:

Data.text.pop = {
    register: {
        titlte: "Create Your Free Account",
        subTitle: "Search Homes and Exclusive Property Listings"
    },
    favHouse: {title: "xxx", subTitle: "xxx" },
    saveSearch: {title: "xxx", subTitle: "xxx"}
};

{tittle: “”, subtitle: “”}這個(gè)就當(dāng)作是彈框文案策略,然后再寫彈框的html模板的時(shí)候引入一個(gè)占位變量:

<section>
    {{title}}
    {{subTitile}}
    <div>
        <!--其它內(nèi)容-->
    </div>
</section>

在渲染這個(gè)彈框的時(shí)候睁蕾,根據(jù)傳進(jìn)來(lái)的popType映射到不同的文案:

function showPop(popType){
    Mustache.render(popTemplate, Data.text.pop[popType])
}

這里用Data.text.pop[popType]映射到了對(duì)應(yīng)的文案苞笨,如果用react你把一個(gè)個(gè)的標(biāo)題封裝成一個(gè)組件债朵,其實(shí)思想是一樣的。

但是這個(gè)并不是嚴(yán)格的策略模式瀑凝,因?yàn)椴呗跃褪且袌?zhí)行的東西嘛序芦,我們這里其實(shí)是一個(gè)寫死的文案,但是我們借助了策略模式的思想粤咪。接下來(lái)繼續(xù)說(shuō)使用策略模式做一些執(zhí)行的事情谚中。

在上面的彈框的觸發(fā)機(jī)制分別是:用戶點(diǎn)擊了注冊(cè)、點(diǎn)擊了收藏房源寥枝、點(diǎn)擊了保存搜索條件宪塔。如果用戶沒(méi)有登陸就會(huì)彈一個(gè)注冊(cè)框,當(dāng)用戶注冊(cè)完之后脉顿,要繼續(xù)執(zhí)行用戶原本的操作蝌麸,例如該收藏還是收藏,所以必須要有一個(gè)注冊(cè)后的回調(diào)艾疟,并且這個(gè)回調(diào)做的事情還不一樣来吩。

當(dāng)然,你可以在回調(diào)里面寫很多的if else或者是case:

function popCallback(popType){
    switch(popType){
        case "register": 
            //do nothing
            break;
        case: "favHouse": 
            favHouse();
            break;
        case: "saveSearch":
            saveSearch();
            break;
    }
}

但是當(dāng)你的case很多的時(shí)候蔽莱,看起來(lái)可能就不是特別好了弟疆,特別是if else的那種寫法。這個(gè)時(shí)候就可以使用策略模式盗冷,每個(gè)回調(diào)都是一個(gè)策略:

var popCallback = {
    favHouse: function(){
        //do sth.
    },
    saveSearch: function(){
        //do sth.
    }
}

然后根據(jù)popType映射調(diào)用相應(yīng)的callback怠苔,如下:

var popCallback = require("pop-callback");
if(typeof popCallback[popType] === "function"){
    popCallback[popType]();
}

這樣它就是一個(gè)完整的策略模式了,這樣寫有很多好處仪糖。如果以后需要增加一個(gè)彈框類型popType柑司,那么只要在popCallback里面添加一個(gè)函數(shù)就好了,或者要?jiǎng)h掉一個(gè)popType锅劝,相應(yīng)地注釋掉某個(gè)函數(shù)即可攒驰。并不需要去改動(dòng)原有代碼的邏輯,而采用if else的方式就得去修改原有代碼的邏輯故爵,所以這樣對(duì)擴(kuò)展是開放的玻粪,而對(duì)修改是封閉的,這就是面向?qū)ο缶幊汤锩娴拈_閉原則诬垂。

在js里面實(shí)現(xiàn)策略模式或者是其它設(shè)計(jì)模式都是很自然的方式劲室,因?yàn)閖s里面function可以直接作為一個(gè)普通的變量,而在C++/Java里面需要用一些技巧结窘,玩一些OO的把戲才能實(shí)現(xiàn)很洋。例如上面的策略模式,在Java里面需要先寫一個(gè)接口類隧枫,里面定義一個(gè)接口函數(shù)喉磁,然后每個(gè)策略都封裝成一個(gè)類棺克,分別實(shí)現(xiàn)接口類的接口函數(shù)。而在js里面的設(shè)計(jì)模式往往幾行代碼就寫出來(lái)线定,這可能也是做為函數(shù)式編程的一個(gè)優(yōu)點(diǎn)。

前端和設(shè)計(jì)模式經(jīng)常打交道的還有訪問(wèn)者模式

4. 訪問(wèn)者模式

事件監(jiān)聽就是一個(gè)訪問(wèn)者模式确买,一個(gè)典型的訪問(wèn)者模式可以這么實(shí)現(xiàn)斤讥,首先定義一個(gè)Input的類,初始化它的訪問(wèn)者列表

function Input(inputDOM){
    //用來(lái)存放訪問(wèn)者的數(shù)據(jù)結(jié)構(gòu)
    this.visitiors = {
        "click": [],
        "change": [],
        "special": [] //自定義事件
    }
    this.inputDOM = inputDOM;
}

然后提供一個(gè)對(duì)外的添加訪問(wèn)者的接口:

Input.prototype.on = function(eventType, callback){
    if(typeof this.visitiors[eventType] !== "undefined"){
        this.visitiors[eventType].push(callback);
    }
};

使用者調(diào)用on湾趾,傳遞兩個(gè)參數(shù)芭商, 一個(gè)是事件類型,即訪問(wèn)類型搀缠,另外一個(gè)是具體的訪問(wèn)者铛楣,這里是回調(diào)函數(shù)。Input就會(huì)將訪問(wèn)者添加到它的訪問(wèn)者列表艺普。

同時(shí)Input還提供了一個(gè)刪除訪問(wèn)者的接口:

Input.prototype.off = function(eventType, callback){
    var visitors = this.visitiors[eventType];
    if(typeof visitiors !== "undefined"){
        var index = visitiors.indexOf(callback);
        if(index >= 0){
            visitiors.splice(index, 1);
        }
    }
};

這樣子簸州,Input就和訪問(wèn)者建立起了關(guān)系,或者說(shuō)訪問(wèn)者已經(jīng)成功地向接收者都訂閱了消息歧譬,一旦接書者收到了消息會(huì)向它的訪問(wèn)者一一傳遞:

Input.prototype.trigger = function(eventType, event){
    var visitors = this.visitiors[eventType];
    var eventFormat = processEvent(event); //獲取消息并做格式化
    if(typeof visitors !== "undefined"){
        for(var i = 0; i < visitors.length; i++){
            visitors[i](eventFormat);
        }
    }
};

trigger可能是用戶調(diào)的岸浑,也可能是底層的控件調(diào)用的。在其它領(lǐng)域瑰步,它可能是一個(gè)光感控件觸發(fā)的矢洲。不管怎樣,一旦有人觸發(fā)了trigger缩焦,接收者就會(huì)一一下發(fā)消息读虏。

如果你知道了事件監(jiān)聽的模式是這樣的,可能對(duì)你寫代碼會(huì)有幫助袁滥。例如點(diǎn)擊下面的搜索條件的X盖桥,要把上面的搜索框清空,同時(shí)還要觸發(fā)搜索呻拌,并把輸入框右邊的X去掉葱轩。要附帶著做幾件事情。

image

這個(gè)時(shí)候你可能會(huì)這樣寫:

$(".icon-close").on("click", function(){
    $(this).parent().remove(); //刪除本身的展示
    $("#search-input").val("");
    searchAjax.ajax();         //觸發(fā)搜索
    $("#clear-search").hide(); //隱藏輸入框x
});

但其實(shí)這樣有點(diǎn)累贅藐握,因?yàn)樵谏厦娴乃阉鬏斎肟蚩隙ㄒ矔?huì)相應(yīng)的操作靴拱,當(dāng)用戶輸入為空時(shí),自動(dòng)隱藏右邊的x猾普,并且輸入框change的時(shí)候會(huì)自動(dòng)搜索袜炕,也就是說(shuō)所有附加的事情輸入框那邊已經(jīng)有了,所以其實(shí)只需要觸發(fā)下輸入框的change事件就好了:

$(".icon-close").on("click", function(){
    $(this).parent().remove(); //刪除本身的展示
    $("#search-input").val("").trigger("change");
});

輸入框?yàn)榭諘r(shí)初家,該怎么處理偎窘,search輸入框會(huì)相應(yīng)地處理乌助,下面那個(gè)條件展示的x不需要去關(guān)心。觸發(fā)了change之后陌知,會(huì)把相應(yīng)的消息下發(fā)給search輸入框的訪問(wèn)者們他托。

當(dāng)然,你用react你可能不會(huì)這樣想了仆葡,你應(yīng)該是在研究組件間怎么通信地好赏参。

上文提及使用傳參避免全局耦合,然后在js里面通過(guò)控制class減少和css的耦合沿盅,和耦合相對(duì)的是內(nèi)聚把篓,出發(fā)點(diǎn)是重復(fù)代碼,減少拷貝代碼會(huì)有一個(gè)抽象和封裝的過(guò)程:function -> 模塊 -> 插件/框架腰涧,封裝常用的還有封裝成一個(gè)類韧掩,方便控制私有數(shù)據(jù)。這樣可實(shí)現(xiàn)高內(nèi)聚窖铡,除此方法疗锐,還有設(shè)計(jì)模式的思想,上面介紹了策略模式和訪問(wèn)者模式的原理和應(yīng)用费彼,以及在寫代碼的啟示窒悔。

希望上文能對(duì)你有所啟迪,如有不對(duì)之處還請(qǐng)指出敌买。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末简珠,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子虹钮,更是在濱河造成了極大的恐慌聋庵,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芙粱,死亡現(xiàn)場(chǎng)離奇詭異祭玉,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)春畔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門脱货,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人律姨,你說(shuō)我怎么就攤上這事振峻。” “怎么了择份?”我有些...
    開封第一講書人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵扣孟,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我荣赶,道長(zhǎng)凤价,這世上最難降的妖魔是什么鸽斟? 我笑而不...
    開封第一講書人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮利诺,結(jié)果婚禮上富蓄,老公的妹妹穿的比我還像新娘。我一直安慰自己慢逾,他們只是感情好格粪,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著氛改,像睡著了一般。 火紅的嫁衣襯著肌膚如雪比伏。 梳的紋絲不亂的頭發(fā)上胜卤,一...
    開封第一講書人閱讀 52,475評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音赁项,去河邊找鬼葛躏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛悠菜,可吹牛的內(nèi)容都是我干的舰攒。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼悔醋,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼摩窃!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起芬骄,我...
    開封第一講書人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤猾愿,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后账阻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蒂秘,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年淘太,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了姻僧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蒲牧,死狀恐怖撇贺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情冰抢,我是刑警寧澤显熏,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站晒屎,受9級(jí)特大地震影響喘蟆,放射性物質(zhì)發(fā)生泄漏缓升。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一蕴轨、第九天 我趴在偏房一處隱蔽的房頂上張望港谊。 院中可真熱鬧,春花似錦橙弱、人聲如沸歧寺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)斜筐。三九已至,卻和暖如春蛀缝,著一層夾襖步出監(jiān)牢的瞬間顷链,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工屈梁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嗤练,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓在讶,卻偏偏與公主長(zhǎng)得像煞抬,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子构哺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361