终极compose函数封装方案!

8,087 阅读8分钟

前言

无意中在掘金看到一篇写compose函数的文章《感谢 compose 函数,让我的代码屎山💩逐渐美丽了起来~》,之前这个命题我面试的时候问过很多面试者,还挺有体会的。正好谈一谈

我不会直接问你知道compose函数吗,一般简历上写熟悉react技术栈(reudx,react-router等等),我会问知道redux中间件的实现原理吗?这个问题其实本质上是问同步的compose函数怎么写,什么是同步compose呢?

compose就是执行一系列的任务(函数),比如有以下任务队列(数组里都是函数)

let tasks = [step1, step2, step3, step4]

每一个step都是一个步骤,按照步骤一步一步的执行到结尾,这就是一个composecompose在函数式编程中是一个很重要的工具函数,在这里实现的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))
  }
}

本文结束!欢迎点赞!😺