前言
链式编程实际是将多个方法(函数)通过某种方式链接在一起,使多个逻辑块能按流程逐步执行(或跳过执行),从而实现解耦,在js上最典型的链式代码:
/* 链式 */
console.log(
[1,2,3,4]
.concat(5)
.filter((item)=>(item<3))
.concat(6)
.join("")
); // 输出 126
/* 非链式 */
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(""));
实现链式反应的本质为:每次该对象(Object-A)调用其方法(Method-1)时,返回值仍为本对象(Object-A),从而后面使用链式的方式再调用另外一个方法(Method-2)时,得到的this仍为原对象(Object-A),然后返回值同样(Object-A),从而仍可通过链式的方式再调用该对象上的别的方法(Method-3),以此类推。
在js上常见的链式编程有以下几种具体应用:
- 对象方法
return this
的链式操作 - Promise
- 责任链(Chain of responsibility)
它们有不同的目标与思路,下面就逐一介绍~
一、对象方法
对同一个对象不断执行相同或不同的方法
jQuery不用多说了吧,jQuery里面有很多的方法的使用方式就是此类形式,如:
$("#myDiv")
.css('color','red')
.html('123>')
.appendTo('
dddd')
我们来写一个对象里挂载多个方法:
var myObj = {
name: '',
setName: function(newName) {
this.name = newName;
// 要实现在调用setName方法后仍能链式调用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
,所以每次调用之后返回值均为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
将多个异步逻辑块解耦,并使其能按序执行,若其中一个出现错误则退出链式,直接进入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) {
// 一秒钟后执行resolve方法
window.setTimeout(resolve, 1000);
});
myPromise.then(function() {
// 一秒钟之后将会进入此callback
console.log('!');
});
可以看到构造Promise对象需要传入一个Function,该Function接受两个参数,分别是resolve
和reject
,前者作为成功回调,后者作为失败回调。
下面展示如何使用Promise来封装异步请求的发送与处理:
/* 封装异步请求 */
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('请求错误');
}
})
});
}
/* 调用 */
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中某处出现了错误,这不再继续执行下面的语句,直接执行catch,并且将错误信息传给catch
console.log(errMsg);
})
// 最后会输出(console) '对象id不能为空'
Promise中的catch会捕捉当前链式中的最终的错误(the eventual error)
三、责任链(Chain of responsibility)
划分多个任务(责任)块,按序执行,每个任务块都有权决定是否继续交给下一个任务块
简单的来讲,就像是面试一样:
- 人事筛选简历,如果简历信息各项符合就交给技术负责人,否则就没有然后了
- 技术负责人面试,如果技术过关了交给主管
- 主管面试,如果各方面都合适了交给老板
- 老板....以此类推
其中这一个个的就是任务块(handler)
下面来个栗子:
// 任务块:筛选性别
const genderHandler = function(next, data) {
if(data.gender === 'male') {
console.log('我们不要男的');
return;
}
next(data);
};
// 任务块:筛选年龄
const ageHandler = function(next, data) {
if(data.age > 30) {
console.log('年龄太大了');
return;
}
next(data);
};
// 任务块:最终处理函数
const finalSuccHandler = function(next, data) {
console.log('emmmm...不错不错');
};
import Chain from './chain.js';
// 使用Chain来构建链式,类似于“建立生产线”
const peopleChain = new Chain()
.setNextHandler(genderHandler)
.setNextHandler(ageHandler)
.setNextHandler(finalSuccHandler);
/* 往责任链上载入不同的信息 */
peopleChain.start({
gender: 'male',
age: 21
}); // 输出 '我们不要男的'
peopleChain.start({
gender: 'female',
age: 48
}); // 输出 '年龄太大了'
peopleChain.start({
gender: 'female',
age: 18
}); // 输出 'emmmm...不错不错'
构造简单的Chain类,用以构建链式:
// chain.js
class Chain {
handlers = []; // 处理函数集合,用于存储当前链式上所有的func
cache = []; // 缓存,用于存储当前链式上还未触发的func
/* 设置下一个 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]
);
}
}
/* 开始触发链式 */
start() {
// 将 [this.handlers] 复制一份,赋给 [this.cache]
this.cache = this.handlers.map(function(fn) {
return fn;
});
// 主动触发第一个 handler
this.next(arguments[0]);
}
}
export default Chain;
在vue、react、小程序等框架中使用的话,链式内部可能需要使用到上下文(this),需要看下面的栗子:
// chain.js
class Chain {
handlers = []; // 处理函数集合,用于存储当前链式上所有的func
cache = []; // 缓存,用于存储当前链式上还未触发的func
context = null; // 上下文,用于存储外部this
/* 设置下一个 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]
);
}
}
/* 开始触发链式 */
start() {
// start 方法接受 [context] 及其他参数
const { context, ...rest } = arguments[0];
// 将 [this.handlers] 复制一份,赋给 [this.cache]
this.cache = this.handlers.map(function(fn) {
return fn;
});
// 暂存上下文
this.context = context;
// 主动触发第一个 handler
this.next(rest);
}
}
export default Chain;
// 任务块:筛选性别
const genderHandler = function(context, next, data) {
if(data.gender === 'male') {
context.showTips('我们不要男的');
return;
}
next(data);
};
// 任务块:筛选年龄
const ageHandler = function(context, next, data) {
if(data.age > 30) {
context.showTips('年龄太大了');
return;
}
next(data);
};
// 使用Chain来构建链式
const peopleChain = new Chain()
.setNextHandler(genderHandler)
.setNextHandler(ageHandler);
// 这里使用objA来作为上下文,如:在vue中的话context参数传该组件的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
});