前言
鏈?zhǔn)骄幊虒?shí)際是將多個(gè)方法(函數(shù))通過某種方式鏈接在一起,使多個(gè)邏輯塊能按流程逐步執(zhí)行(或跳過執(zhí)行),從而實(shí)現(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(""));
實(shí)現(xiàn)鏈?zhǔn)椒磻?yīng)的本質(zhì)為:每次該對(duì)象(Object-A)調(diào)用其方法(Method-1)時(shí),返回值仍為本對(duì)象(Object-A),從而后面使用鏈?zhǔn)降姆绞皆僬{(diào)用另外一個(gè)方法(Method-2)時(shí),得到的this仍為原對(duì)象(Object-A),然后返回值同樣(Object-A),從而仍可通過鏈?zhǔn)降姆绞皆僬{(diào)用該對(duì)象上的別的方法(Method-3),以此類推。
在js上常見的鏈?zhǔn)骄幊逃幸韵聨追N具體應(yīng)用:
- 對(duì)象方法
return this的鏈?zhǔn)讲僮?/li> - Promise
- 責(zé)任鏈(Chain of responsibility)
它們有不同的目標(biāo)與思路,下面就逐一介紹~
一、對(duì)象方法
對(duì)同一個(gè)對(duì)象不斷執(zhí)行相同或不同的方法
jQuery不用多說了吧,jQuery里面有很多的方法的使用方式就是此類形式,如:
$("#myDiv")
.css('color','red')
.html('<p>123</>')
.appendTo('<div>dddd</div>')
我們來寫一個(gè)對(duì)象里掛載多個(gè)方法:
var myObj = {
name: '',
setName: function(newName) {
this.name = newName;
// 要實(shí)現(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(); // 輸出'帥哥就是我'
上面的三個(gè)方法的返回值都為this,所以每次調(diào)用之后返回值均為myObj,下面我們驗(yàn)證下:
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都是同一個(gè),那我們不就可以合并代碼了嘛,不需要每次都多聲明一個(gè)變量:
/* 第一次簡(jiǎn)化 */
myObj.setName('帥哥');
myObj.addStr('就是我');
myObj.consoleName();
/* 第二次簡(jiǎn)化 */
myObj.setName('帥哥').addStr('就是我').consoleName();
二、Promise
將多個(gè)異步邏輯塊解耦,并使其能按序執(zhí)行,若其中一個(gè)出現(xiàn)錯(cuò)誤則退出鏈?zhǔn)?,直接進(jìn)入catch
Promise為ES6新特性,用于避免寫出沖擊波式代碼(callback hell),那么就會(huì)有人問,什么是沖擊波代碼了,給你們瞧一瞧:
getData(x => {
getMoreData(x, y => {
getPerson(person => {
getPlanet(person, (planet) => {
getGalaxy(planet, (galaxy) => {
getLoca(planet, (galaxy) => {
console.log(galaxy);
});
});
});
});
});
});
如果你想的話,還可以弄得更大更長(zhǎng)~:smirk::smirk:
下面先來個(gè)最簡(jiǎn)單的Promise使用:
var myPromise = new Promise(function(resolve, reject) {
// 一秒鐘后執(zhí)行resolve方法
window.setTimeout(resolve, 1000);
});
myPromise.then(function() {
// 一秒鐘之后將會(huì)進(jìn)入此callback
console.log('!');
});
可以看到構(gòu)造Promise對(duì)象需要傳入一個(gè)Function,該Function接受兩個(gè)參數(shù),分別是resolve和reject,前者作為成功回調(diào),后者作為失敗回調(diào)。
下面展示如何使用Promise來封裝異步請(qǐng)求的發(fā)送與處理:
/* 封裝異步請(qǐng)求 */
function getUserInfo(userId){
return new Promise(function(resolve,reject){
if(!userId){
reject('userId不能為空');
return;
}
// 異步請(qǐng)求
ajax({
url:'./getUserInfo',
method:'GET',
params:{userId},
success:function(res){
resolve(res);
},
error:function(){
reject('請(qǐng)求錯(cuò)誤');
}
})
});
}
/* 調(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('對(duì)象id不能為空');
return;
}
// 使用定時(shí)器來模擬異步請(qǐng)求(或其他異步操作)
window.setTimeout(
()=>resolve(obj),
3000
);
})
}
function getUserLocal(obj){
return new Promise(function(resolve,reject){
if(!obj.lastIP){
reject('對(duì)象的IP不能為空');
return;
}
window.setTimeout(resolve,2000);
})
}
getBaseInfo({id:null,lastIP:123})
.then(function(obj){
return getUserDetail(obj);
})
.catch(function(errMsg){
//在最后加catch的話,如果then中某處出現(xiàn)了錯(cuò)誤,這不再繼續(xù)執(zhí)行下面的語(yǔ)句,直接執(zhí)行catch,并且將錯(cuò)誤信息傳給catch
console.log(errMsg);
})
// 最后會(huì)輸出(console) '對(duì)象id不能為空'
Promise中的catch會(huì)捕捉當(dāng)前鏈?zhǔn)街械淖罱K的錯(cuò)誤(the eventual error)
三、責(zé)任鏈(Chain of responsibility)
劃分多個(gè)任務(wù)(責(zé)任)塊,按序執(zhí)行,每個(gè)任務(wù)塊都有權(quán)決定是否繼續(xù)交給下一個(gè)任務(wù)塊
簡(jiǎn)單的來講,就像是面試一樣:
- 人事篩選簡(jiǎn)歷,如果簡(jiǎn)歷信息各項(xiàng)符合就交給技術(shù)負(fù)責(zé)人,否則就沒有然后了
- 技術(shù)負(fù)責(zé)人面試,如果技術(shù)過關(guān)了交給主管
- 主管面試,如果各方面都合適了交給老板
- 老板....以此類推
其中這一個(gè)個(gè)的就是任務(wù)塊(handler)
下面來個(gè)栗子:
// 任務(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...不錯(cuò)不錯(cuò)');
};
import Chain from './chain.js';
// 使用Chain來構(gòu)建鏈?zhǔn)?,類似于“建立生產(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...不錯(cuò)不錯(cuò)'
構(gòu)造簡(jiǎn)單的Chain類,用以構(gòu)建鏈?zhǔn)剑?/p>
// chain.js
class Chain {
handlers = []; // 處理函數(shù)集合,用于存儲(chǔ)當(dāng)前鏈?zhǔn)缴纤械膄unc
cache = []; // 緩存,用于存儲(chǔ)當(dāng)前鏈?zhǔn)缴线€未觸發(fā)的func
/* 設(shè)置下一個(gè) 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(); // 釋放隊(duì)頭 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;
});
// 主動(dòng)觸發(fā)第一個(gè) handler
this.next(arguments[0]);
}
}
export default Chain;
在vue、react、小程序等框架中使用的話,鏈?zhǔn)絻?nèi)部可能需要使用到上下文(this),需要看下面的栗子:
// chain.js
class Chain {
handlers = []; // 處理函數(shù)集合,用于存儲(chǔ)當(dāng)前鏈?zhǔn)缴纤械膄unc
cache = []; // 緩存,用于存儲(chǔ)當(dāng)前鏈?zhǔn)缴线€未觸發(fā)的func
context = null; // 上下文,用于存儲(chǔ)外部this
/* 設(shè)置下一個(gè) 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(); // 釋放隊(duì)頭 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;
// 主動(dòng)觸發(fā)第一個(gè) 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
});