前言
pipe其实是一种设计思想,经常被用于架构设计,也能体现‘函数式编程’ 思想 — 通过组合一系列的函数来完成一个既定的功能。而在前端开发中,本身强调 组合大于继承,所以掌握这种思想是十分重要的。
实例
pipe大概像如下那样使用
// 假设有个三个方法,分别实现+1, 求与2的乘积,-1 这三个功能
let add = n => n + 3;
let multiple = n => n * 2;
let minus = n => n - 1;
// 假设现在有个pipe方法
const pipe = () => {}
// 那么我现在可以这样
console.log(pipe()(10)); // 10
console.log(pipe(add,multiple,minus)(10)); //25
console.log(pipe(add,minus, multiple)(10)); // 24
可以看到,通过组合这三个方法,我们可以得到实现各种组合功能的函数。有这么几个优点
- 符合设计模式的单一职责原则
– 因为我们要组合不同的函数,所以会尽量拆分函数功能,一个函数只完成一个最原子性的功能。便于维护 - 符合设计模式的开闭原则
– 因为一般原子性的函数不太会修改,我们只需要修改下组合的逻辑即可,或者去构造新的组合来实现新的功能。 - 方便单元测试
– 这个很好理解,一个原子性的函数可以理解为一个便于测试的单元
下面我们简单看下pipe可以怎么实现。
实现
const pipe = (...args) => x => args.reduce((acc,cur) => cur(acc),x);
其实根据pipe的思想,在前端中很快就能想到利用reduce这个高阶函数来实现。
其他
其实还有一种compose的思想,和pipe十分类似,但是针对函数的组合是从后往前的。实现起来也非常简单
const pipe = (...args) => x => args.reduceRight((acc,cur) => cur(acc),x);
扩展
上面的例子是比较简单的,但是很好地解释了pipe和compose的思想,但是实际开发中,遇到的情况可能要复杂一些,比如,一个函数的逻辑会涉及到异步,这个时候该怎么写呢?这里我们采用promise的写法
// 本质上就是串联promise
const pipe = (...args) => {
return (x) => {
return args.reduce((acc, cur) => {
if (!(acc instanceof Promise)) {
acc = Promise.resolve(acc);
}
const ret = acc.then(cur);
return ret;
}, x);
};
};
const requestProduct = (id) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`product-tag-${id}`);
}, 1000);
});
};
const requestProductSku = (tag) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`sku-${tag}`);
}, 1000);
});
};
// 这里我们要求接受一个参数,并且返回一个promise
const asyncGetProductTag = (id) => {
return requestProduct(id);
};
const asyncGetProductSku = (tag) => {
return requestProductSku(tag);
};
const getProductThenGetSKu = pipe(asyncGetProductTag, asyncGetProductSku);
getProductThenGetSKu(10).then((res) => console.log(res));
上述代码我们先根据商品ID去获取到商品的tag,再根据tag去获取商品的sku属性。此时就是一个可以处理异步的情况,注意的是可能每一个函数不一定是异步的,所以我们在pipe函数中处理了这种情况,手动转变为promise。有兴趣的同学也可以试试看compose的异步版本
最后
上面的用法还可以再扩展,比如在管道传递的过程中,到某个节点满足一个要求后可以直接退出,这里就可以reject一个值,然后在catch中去处理。总之这种编程技艺十分重要,尤其是在前端开发的场景中,如果有不同见解欢迎讨论。