前言
无意中在掘金看到一篇写compose函数的文章《感谢 compose 函数,让我的代码屎山💩逐渐美丽了起来~》,之前这个命题我面试的时候问过很多面试者,还挺有体会的。正好谈一谈
我不会直接问你知道compose
函数吗,一般简历上写熟悉react技术栈(reudx,react-router等等),我会问知道redux
中间件的实现原理吗?这个问题其实本质上是问同步的compose
函数怎么写,什么是同步compose
呢?
compose
就是执行一系列的任务(函数),比如有以下任务队列(数组里都是函数)
let tasks = [step1, step2, step3, step4]
每一个step
都是一个步骤,按照步骤一步一步的执行到结尾,这就是一个compose
, compose
在函数式编程中是一个很重要的工具函数,在这里实现的compose
有三点说明
- 第一个函数是多元的(接受多个参数),后面的函数都是单元的(接受一个参数)
- 执行顺序的自右向左的
- 所有函数的执行都是同步的(异步的后面文章会讲到)
也就是实现如下:
(...args) => step1(step2(setp3(step4(...args))))
如何优雅的实现呢,一个reduce函数即可,redux中间件源码大致也是这样实现的
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
可能有同学看不懂这个代码,很正常,我们简单分析一下
const a = () => console.log(1);
const b = () => console.log(2);
const c = () => console.log(3);
compose(a, b, c)(); // 分别打印 3 、 2 、 1
我们看看compose组合的时候发生了啥,
const k = compose(a, b) 等价于
const k = (...args) => a(b(...args));
compose(k, c) 等价于
(...args) => k(c(...args))
所以compose(a,b,c) 等价于
(...args) => a(b(c(...args)))
如果还是看不懂,不要急,后面有迭代的方式来实现这个compose函数,就很简单了。redux这样写法更简洁一些。
然后如果对方答上来了,而且使用过koa框架,我会问koa中间件的原理是什么,能写一个吗?它跟同步compose函数有什么区别。
区别就是这些函数都是异步的,上面reduce的写法就不适用了,比如说
const a = () => setTimeout(()=>console.log(1), 1000);
const b = () => setTimeout(()=>console.log(2), 500);
const c = () => setTimeout(()=>console.log(3), 2000);
很明显,我们需要一个异步compose函数来解决问题,这个还可以引申为一道微信的面试题,叫lazyMan,很出名的一道题,大家可以去搜一下,异步compose在koa源码里面有实现,我们看看框架是怎么实现的:
假设有三个异步函数fn1、fn2、fn3,实现如下
function fn1(next) {
console.log(1);
next();
}
function fn2(next) {
console.log(2);
next();
}
function fn3(next) {
console.log(3);
next();
}
const middleware = [fn1, fn2, fn3]
参数里的next是koa函数的一个特点,比如fn1函数,它调用next,fn2才会执行,不调用不执行,同理fn2调用next,fn3才会执行。
这就是koa处理异步的方式,采用next方式(还有一种promise方式后面介绍)
// koa有个特点,调用next参数表示调用下一个函数
function fn1(next) {
console.log(1);
next();
}
function fn2(next) {
console.log(2);
next();
}
function fn3(next) {
console.log(3);
next();
}
middleware = [fn1, fn2, fn3]
function compose(middleware){
function dispatch (index){
if(index == middleware.length) return ;
var curr;
curr = middleware[index];
// 这里使用箭头函数,让函数延迟执行
return curr(() => dispatch(++index))
}
dispatch(0)
};
compose(middleware);
好了前言实在太多了。。。。我们进入正题
webpack的Tapable库
这个库是webpack实现钩子函数的核心库,为什么要提到它呢,因为它就是各种compose函数的终极解决方案,而且我们还可以好好学学这些大佬写的代码是如何封装的
我们把所有的compose做一个分类:大体是分同步和异步的
-
分类:
-
Sync*(同步版本compose):
- SyncHook (串行同步执行, 不关心返回值)
- SyncBailHook (串行同步执行,如果返回值不是null,则剩下的函数不执行)
- SyncWaterfallHook(串行同步执行,前一个函数的返回值作为后一个函数的参数,跟我们之前将的redux中间件原理是一个道理,迭代实现,更简单)
- SyncLoopHook (串行同步执行,订阅者返回true表示继续执行后面的函数,返回undefine表示不执行后面的函数)
-
Async*(异步版本compose):
-
AsyncParallelHook 不关心返回值,并发异步函数而已,没顺序要求
-
AsyncSeriesHook 异步函数数组要求按顺序调用
-
AsyncSeriesBailHook 可中断的异步函数链
-
AsyncSeriesWaterfallHook 异步串行瀑布钩子函数
所以说这几种compose掌握了,基本compose的各种类型就完全解决了,面试官跟你聊这些,你都可以秒杀他了,一般他只会考虑我们前言里面那两种compose。
为了代码好理解,我做了很微小的改动。
Sync*型Hook
SyncHook
串行同步执行,不关心返回值
class SyncHook {
constructor(name){
this.tasks = [];
this.name = name;
}
tap(task){
this.tasks.push(task);
}
call(){
this.tasks.forEach(task=>task(...arguments));
}
}
let queue = new SyncHook('name');
queue.tap(function(...args){ console.log(args); });
queue.tap(function(...args){ console.log(args); });
queue.tap(function(...args){ console.log(args); });
queue.call('hello');
// 打印
// ["hello"]
// ["hello"]
// ["hello"]
看完上面的函数,有人就会说,compose函数去哪了,我们不是要组合函数吗,上面这是什么东西?
我们可以看到tap是注册函数,call是调用函数,也就是说,compose函数本身结合了tap注册的功能和call调用的功能。所以我们也可以把这个 SyncHook 改装 成compose就是
function compose(...fns) {
return (...args) => fns.forEach(task=>task(...args))
}
这个区别很有意思的,其实就是面向对象和函数式编程的一个区别,tapable是用面向对象的思想,我们上面的compose是函数式的思想,一个是以类为中心利用数据和函数去组合功能,一个是以函数为中心组合功能。
下面的函数也是一个道理,本身代码都非常简单,一看就懂了,我就不一一转换成compose了。
SyncBailHook
串行同步执行,bail是保险丝的意思,有一个返回值不为null则跳过剩下的逻辑
class SyncBailHook {
constructor(name){
this.tasks = [];
this.name = name;
}
tap(task){
this.tasks.push(task);
}
call(){
let i= 0,ret;
do {
ret=this.tasks[i++](...arguments);
} while (!ret);
}
}
let queue = new SyncBailHook('name');
queue.tap(function(name){
console.log(name,1);
return 'Wrong';
});
queue.tap(function(name){
console.log(name,2);
});
queue.tap(function(name){
console.log(name,3);
});
queue.call('hello');
// 打印
// hello 1
SyncWaterfallHook
串行同步执行,Waterfall是瀑布的意思,前一个订阅者的返回值会传给后一个订阅者
class SyncWaterfallHook {
constructor(name){
this.tasks = [];
this.name = name;
}
tap(task){
this.tasks.push(task);
}
call(){
let [first,...tasks] = this.tasks;
tasks.reduce((ret,task) => task(ret) , first(...arguments));
}
}
let queue = new SyncWaterfallHook(['name']);
queue.tap(function(name,age){
console.log(name, age, 1);
return 1;
});
queue.tap(function(data){
console.log(data , 2);
return 2;
});
queue.tap(function(data){
console.log(data, 3);
});
queue.call('hello', 25);
// 打印
// hello 25 1
// 1 2
// 2 3
SyncLoopHook
串行同步执行,Loop是循环往复的意思,订阅者返回true表示继续列表循环,返回undefined表示结束循环
class SyncLoopHook{
constructor(name) {
this.tasks=[];
this.name = name;
}
tap(task) {
this.tasks.push(task);
}
call(...args) {
this.tasks.forEach(task => {
let ret = true;
do {
ret = task(...args);
}while(ret == true || !(ret === undefined))
});
}
}
let hook = new SyncLoopHook('name');
let total = 0;
hook.tap(function(name){
console.log('react',name)
return ++total === 3? undefined :'继续学';
})
hook.tap(function(name){
console.log('node',name)
})
hook.tap(function(name){
console.log('node',name)
})
hook.call('hello');
// 打印3次react hello,然后打印 node hello,最后再次打印node hello
Async*型Hook
AsyncParallelHook
并行异步执行,和同步执行的最大区别在于,订阅者中可以存在异步逻辑。就是个并发异步函数而已,没顺序要求
promise实现
class AsyncParallelHook{
constructor(name) {
this.tasks=[];
this.name = name;
}
tapPromise(task) {
this.tasks.push(task);
}
promise() {
let promises = this.tasks.map(task => task());
// Promise.all所有的Promsie执行完成会调用回调
return Promise.all(promises);
}
}
let queue = new AsyncParallelHook('name');
console.time('cast');queue.tapPromise(function(name){
return new Promise(function(resolve,reject){
setTimeout(function(){
console.log(1);
resolve();
},1000)
});
});
queue.tapPromise(function(name){
return new Promise(function(resolve,reject){
setTimeout(function(){
console.log(2);
resolve();
},2000)
});
});
queue.tapPromise(function(name){
return new Promise(function(resolve,reject){
setTimeout(function(){
console.log(3);
resolve();
},3000)
});
});
queue.promise('hello').then(()=>{
console.timeEnd('cast');
})
// 打印
// 1
// 2
// 3
AsyncSeriesHook
异步串行的钩子,也就是异步函数要求按顺序调用
promise实现
class AsyncSeriesHook {
constructor(name) {
this.tasks = [];
this.name = name
}
promise(...args) {
let [first, ...others] = this.tasks;
return others.reduce((p, n) => { // 类似redux源码
return p.then(() => {
return n(...args);
});
}, first(...args))
}
tapPromise(task) {
this.tasks.push(task);
}
}
let queue=new AsyncSeriesHook('name');
console.time('cost');
queue.tapPromise(function(name){
return new Promise(function(resolve){
setTimeout(function(){
console.log(1);
resolve();
},1000)
});
});
queue.tapPromise(function(name,callback){
return new Promise(function(resolve){
setTimeout(function(){
console.log(2);
resolve();
},2000)
});
});
queue.tapPromise(function(name,callback){
return new Promise(function(resolve){
setTimeout(function(){
console.log(3);
resolve();
},3000)
});
});
queue.promise('hello').then(data=>{
console.log(data);
console.timeEnd('cost');
});
// 打印
// 1
// 2
// 3
AsyncSeriesBailHook
串行异步执行,bail是保险丝的意思,任务如果return,或者reject,则阻塞了
这里的实现有一丝丝技巧,就是如何打断reduce,可以看一个简单案例
const arr = [0, 1, 2, 3, 4]
const sum = arr.reduce((prev, curr, index, currArr) => {
prev += curr
if (curr === 3) currArr.length = 0
return prev
}, 0)
console.log(sum) // 6
这就是打断reduce的办法就 --- 用一个if判断
promise实现
class AsyncSeriesBailHook {
constructor(name){
this.tasks = [];
this.name = name
}
tapPromise(task){
this.tasks.push(task);
}
promise(...args){
const [first,...others] = this.tasks;
return new Promise((resolve, reject) => {
others.reduce((pre, next, index, arr) => {
return pre
.then(() => { if((arr.length !== 0)) return next(...args)})
.catch((err=>{
arr.splice(index, arr.length - index);
reject(err);
})).then(()=>{
(arr.length === 0) && resolve();
})
}, first(...args))
})
}
}
let queue=new AsyncSeriesBailHook('name');
console.time('cast');
queue.tapPromise(function(...args){
return new Promise(function(resolve){
setTimeout(function(){
console.log(1);
resolve();
},1000)
});
});
queue.tapPromise(function(...args){
return new Promise(function(resolve, reject){
setTimeout(function(){
console.log(2);
reject(); // 使用reject那么就会直接跳出后面的逻辑
},1000)
});
});
queue.tapPromise(function(...args){
return new Promise(function(resolve){
setTimeout(function(){
console.log(3);
resolve();
},1000)
});
});
queue.promise('hello').then( data => {
console.log(data);
console.timeEnd('cast');
});// 打印
// 1
// 2
上面的实现也可以使用下面的代码,原理跟koa类似
AsyncSeriesWaterfallHook
串行异步执行,Waterfall是瀑布的意思,前一个订阅者的返回值会传给后一个订阅者
promise实现
class AsyncSeriesWaterfallHook {
constructor(){
this.name= name;
this.tasks = [];
}
tapPromise(name,task){
this.tasks.push(task);
}
promise(...args){
const [first,...others] = this.tasks;
return others.reduce((pre, next) => {
return pre.then((data)=>{
return data ? next(data) : next(...args);
})
},first(...args))
}
}
本文结束!欢迎点赞!😺