前言
鏈?zhǔn)骄幊虒嶋H是將多個方法(函數(shù))通過某種方式鏈接在一起姆打,使多個邏輯塊能按流程逐步執(zhí)行(或跳過執(zhí)行),從而實現(xiàn)解耦,在js上最典型的鏈?zhǔn)酱a:
/* 鏈?zhǔn)?*/
console.log(
[1,2,3,4]
.concat(5)
.filter((item)=>(item<3))
.concat(6)
.join("")
); // 輸出 126
/* 非鏈?zhǔn)?*/
const arr = [1,2,3,4];
const arr1 = arr.concat(5);
const arr2 = arr1.filter((item)=>(item<3));
const arr3 = arr2.concat(6);
console.log(arr3.join(""));
實現(xiàn)鏈?zhǔn)椒磻?yīng)的本質(zhì)為:每次該對象(Object-A)調(diào)用其方法(Method-1)時,返回值仍為本對象(Object-A),從而后面使用鏈?zhǔn)降姆绞皆僬{(diào)用另外一個方法(Method-2)時霞篡,得到的this仍為原對象(Object-A)世蔗,然后返回值同樣(Object-A),從而仍可通過鏈?zhǔn)降姆绞皆僬{(diào)用該對象上的別的方法(Method-3)寇损,以此類推凸郑。
在js上常見的鏈?zhǔn)骄幊逃幸韵聨追N具體應(yīng)用:
- 對象方法
return this
的鏈?zhǔn)讲僮?/li> - Promise
- 責(zé)任鏈(Chain of responsibility)
它們有不同的目標(biāo)與思路,下面就逐一介紹~
一矛市、對象方法
對同一個對象不斷執(zhí)行相同或不同的方法
jQuery不用多說了吧芙沥,jQuery里面有很多的方法的使用方式就是此類形式,如:
$("#myDiv")
.css('color','red')
.html('<p>123</>')
.appendTo('<div>dddd</div>')
我們來寫一個對象里掛載多個方法:
var myObj = {
name: '',
setName: function(newName) {
this.name = newName;
// 要實現(xiàn)在調(diào)用setName方法后仍能鏈?zhǔn)秸{(diào)用myObj的其他方法就必須返回this浊吏,即返回myObj
return this;
},
addStr: function(str) {
this.name += str;
return this;
},
consoleName: function() {
console.log(this.name);
return this;
}
};
myObj
.setName('帥哥')
.addStr('就是我')
.consoleName(); // 輸出'帥哥就是我'
上面的三個方法的返回值都為this
而昨,所以每次調(diào)用之后返回值均為myObj
,下面我們驗證下:
var obj1 = myObj.setName('帥哥');
var obj2 = obj1.addStr('就是我');
var obj3 = obj2.consoleName();
console.log(obj1 === myObj); // true
console.log(obj2 === myObj); // true
console.log(obj3 === myObj); // true
既然obj1找田、obj2歌憨、obj3、myObj都是同一個墩衙,那我們不就可以合并代碼了嘛务嫡,不需要每次都多聲明一個變量:
/* 第一次簡化 */
myObj.setName('帥哥');
myObj.addStr('就是我');
myObj.consoleName();
/* 第二次簡化 */
myObj.setName('帥哥').addStr('就是我').consoleName();
二、Promise
將多個異步邏輯塊解耦漆改,并使其能按序執(zhí)行心铃,若其中一個出現(xiàn)錯誤則退出鏈?zhǔn)剑苯舆M(jìn)入catch
Promise為ES6新特性挫剑,用于避免寫出沖擊波式代碼(callback hell)去扣,那么就會有人問,什么是沖擊波代碼了樊破,給你們瞧一瞧:
getData(x => {
getMoreData(x, y => {
getPerson(person => {
getPlanet(person, (planet) => {
getGalaxy(planet, (galaxy) => {
getLoca(planet, (galaxy) => {
console.log(galaxy);
});
});
});
});
});
});
如果你想的話愉棱,還可以弄得更大更長~:smirk::smirk:
下面先來個最簡單的Promise使用:
var myPromise = new Promise(function(resolve, reject) {
// 一秒鐘后執(zhí)行resolve方法
window.setTimeout(resolve, 1000);
});
myPromise.then(function() {
// 一秒鐘之后將會進(jìn)入此callback
console.log('!');
});
可以看到構(gòu)造Promise對象需要傳入一個Function,該Function接受兩個參數(shù)哲戚,分別是resolve
和reject
奔滑,前者作為成功回調(diào),后者作為失敗回調(diào)顺少。
下面展示如何使用Promise來封裝異步請求的發(fā)送與處理:
/* 封裝異步請求 */
function getUserInfo(userId){
return new Promise(function(resolve,reject){
if(!userId){
reject('userId不能為空');
return;
}
// 異步請求
ajax({
url:'./getUserInfo',
method:'GET',
params:{userId},
success:function(res){
resolve(res);
},
error:function(){
reject('請求錯誤');
}
})
});
}
/* 調(diào)用 */
getUserInfo()
.then(function(data){
console.log(data)
})
.catch(function(msg){
console.log(msg)
});
// 最后輸出 'userId不能為空'
那么來修改剛開始的沖擊波代碼:
function getUserInfo(obj){
return new Promise(function(resolve,reject){
if(!obj.id){
reject('對象id不能為空');
return;
}
// 使用定時器來模擬異步請求(或其他異步操作)
window.setTimeout(
()=>resolve(obj),
3000
);
})
}
function getUserLocal(obj){
return new Promise(function(resolve,reject){
if(!obj.lastIP){
reject('對象的IP不能為空');
return;
}
window.setTimeout(resolve,2000);
})
}
getBaseInfo({id:null,lastIP:123})
.then(function(obj){
return getUserDetail(obj);
})
.catch(function(errMsg){
//在最后加catch的話档押,如果then中某處出現(xiàn)了錯誤,這不再繼續(xù)執(zhí)行下面的語句祈纯,直接執(zhí)行catch,并且將錯誤信息傳給catch
console.log(errMsg);
})
// 最后會輸出(console) '對象id不能為空'
Promise中的catch會捕捉當(dāng)前鏈?zhǔn)街械淖罱K的錯誤(the eventual error)
三叼耙、責(zé)任鏈(Chain of responsibility)
劃分多個任務(wù)(責(zé)任)塊腕窥,按序執(zhí)行,每個任務(wù)塊都有權(quán)決定是否繼續(xù)交給下一個任務(wù)塊
簡單的來講筛婉,就像是面試一樣:
- 人事篩選簡歷簇爆,如果簡歷信息各項符合就交給技術(shù)負(fù)責(zé)人癞松,否則就沒有然后了
- 技術(shù)負(fù)責(zé)人面試,如果技術(shù)過關(guān)了交給主管
- 主管面試入蛆,如果各方面都合適了交給老板
- 老板....以此類推
其中這一個個的就是任務(wù)塊(handler)
下面來個栗子:
// 任務(wù)塊:篩選性別
const genderHandler = function(next, data) {
if(data.gender === 'male') {
console.log('我們不要男的');
return;
}
next(data);
};
// 任務(wù)塊:篩選年齡
const ageHandler = function(next, data) {
if(data.age > 30) {
console.log('年齡太大了');
return;
}
next(data);
};
// 任務(wù)塊:最終處理函數(shù)
const finalSuccHandler = function(next, data) {
console.log('emmmm...不錯不錯');
};
import Chain from './chain.js';
// 使用Chain來構(gòu)建鏈?zhǔn)较烊兀愃朴凇敖⑸a(chǎn)線”
const peopleChain = new Chain()
.setNextHandler(genderHandler)
.setNextHandler(ageHandler)
.setNextHandler(finalSuccHandler);
/* 往責(zé)任鏈上載入不同的信息 */
peopleChain.start({
gender: 'male',
age: 21
}); // 輸出 '我們不要男的'
peopleChain.start({
gender: 'female',
age: 48
}); // 輸出 '年齡太大了'
peopleChain.start({
gender: 'female',
age: 18
}); // 輸出 'emmmm...不錯不錯'
構(gòu)造簡單的Chain類,用以構(gòu)建鏈?zhǔn)剑?/p>
// chain.js
class Chain {
handlers = []; // 處理函數(shù)集合哨毁,用于存儲當(dāng)前鏈?zhǔn)缴纤械膄unc
cache = []; // 緩存枫甲,用于存儲當(dāng)前鏈?zhǔn)缴线€未觸發(fā)的func
/* 設(shè)置下一個 handler */
setNextHandler(fn) {
if (typeof fn !== "function") {
throw new Error("[chain] successor must be a function.");
}
this.handlers.push(fn);
return this;
}
next() {
if (this.cache && this.cache.length > 0) {
let ware = this.cache.shift(); // 釋放隊頭 handler
ware.call(
this,
this.next.bind(this), // 遞歸
arguments && arguments[0]
);
}
}
/* 開始觸發(fā)鏈?zhǔn)?*/
start() {
// 將 [this.handlers] 復(fù)制一份,賦給 [this.cache]
this.cache = this.handlers.map(function(fn) {
return fn;
});
// 主動觸發(fā)第一個 handler
this.next(arguments[0]);
}
}
export default Chain;
在vue扼褪、react想幻、小程序等框架中使用的話,鏈?zhǔn)絻?nèi)部可能需要使用到上下文(this)话浇,需要看下面的栗子:
// chain.js
class Chain {
handlers = []; // 處理函數(shù)集合脏毯,用于存儲當(dāng)前鏈?zhǔn)缴纤械膄unc
cache = []; // 緩存,用于存儲當(dāng)前鏈?zhǔn)缴线€未觸發(fā)的func
context = null; // 上下文幔崖,用于存儲外部this
/* 設(shè)置下一個 handler */
setNextHandler(fn) {
if (typeof fn !== "function") {
throw new Error("[chain] successor must be a function.");
}
this.handlers.push(fn);
return this;
}
next() {
if (this.cache && this.cache.length > 0) {
let ware = this.cache.shift(); // 釋放隊頭 handler
ware.call(
this,
this.context,
this.next.bind(this), // 遞歸
arguments && arguments[0]
);
}
}
/* 開始觸發(fā)鏈?zhǔn)?*/
start() {
// start 方法接受 [context] 及其他參數(shù)
const { context, ...rest } = arguments[0];
// 將 [this.handlers] 復(fù)制一份食店,賦給 [this.cache]
this.cache = this.handlers.map(function(fn) {
return fn;
});
// 暫存上下文
this.context = context;
// 主動觸發(fā)第一個 handler
this.next(rest);
}
}
export default Chain;
// 任務(wù)塊:篩選性別
const genderHandler = function(context, next, data) {
if(data.gender === 'male') {
context.showTips('我們不要男的');
return;
}
next(data);
};
// 任務(wù)塊:篩選年齡
const ageHandler = function(context, next, data) {
if(data.age > 30) {
context.showTips('年齡太大了');
return;
}
next(data);
};
// 使用Chain來構(gòu)建鏈?zhǔn)? const peopleChain = new Chain()
.setNextHandler(genderHandler)
.setNextHandler(ageHandler);
// 這里使用objA來作為上下文,如:在vue中的話context參數(shù)傳該組件的vm即可
const objA = {
showTips: function(str) {
window.alert(str);
}
};
peopleChain.start({
context: objA,
gender: 'male',
age: 21
});
peopleChain.start({
context: objA,
gender: 'female',
age: 48
});