JS函数链式调用的几种方式

7,780 阅读6分钟

方式1:通过返回实例实现链式

通过例子来说明:

class N{
        constructor(value) {
            this.value = value || 0;
        }
        add(count) {
            this.value += count;
            return this;
        }
        minus(count) {
            this.value -= count;
            return this;
        }
        get() {
            console.log(this.value);
            return this.value;
        }
    }

const n = new N(2);
n.add(198).minus(100).get(); // 100

每个方法调用执行完成之后,返回实例本身,不涉及任何函数的嵌套,简单。

方式2:通过递归调用实现链式

这种方式常用于基础框架中作为一种封装使用。比如在koa或者express中,会用到各种各样的中间件作为扩展或增强,如果是简单的遍历执行所有的中间件函数恐怕难以满足业务需要,比如异步流控制、执行顺序等。因此如何对中间件组织封装的本质就是以何种方式实现函数的链式调用。

举一栗:

// 模拟一系列函数    
function fn1(ctx, next) {
    console.log('函数fn1执行...');
}
function fn2(ctx, next) {
    console.log('函数fn2执行...');
}
function fn3(ctx, next) {
    console.log('函数fn3执行...');
}    

let fns = [fn1, fn2, fn3];

// 定义一个触发函数
const trigger = (fns) => {
    fns.forEach(fn => {
        fn();
    })
}
// 执行触发,所有函数依次执行
trigger(fns); // 

这就是最简单的形式,接下来我们要实现的是 上一个函数通过调用next 来调用下一个函数 实现「洋葱模型」。

// 模拟一系列函数    
function fn1(ctx, next) {
    console.log('函数fn1执行...'); // 打印顺序 1
    next();
    console.log('fn1 ending'); // 打印顺序 6
}
function fn2(ctx, next) {
    console.log('函数fn2执行...'); // 打印顺序 2
    next();
    console.log('fn2 ending'); // 打印顺序 5
}
function fn3(ctx, next) {
    console.log('函数fn3执行...'); // 打印顺序 3
    next();
    console.log('fn3 ending'); // 打印顺序 4
}   

我们期望的结果应该是如上图那样,下面进行思路拆分:

1,实现一个包装函数,此函数的功能就是返回一个新函数fn,并在其内部执行数组的第一个函数

2,在每个函数内部遇到next则终止当前函数逻辑,执行下一个函数形成链式

因此,我们期望的结果应该是这样的:const fn = wrap(fns); fn(ctx) // 即可调用到所有的函数

来实现这两个函数:

// 省略所有容错判断
function wrap(fns) {
    // 必然会返回一个函数...
    return (ctx) => {
        // 闭包保留fns数组的长度
        let l = fns.length;
        // 调用时从第一个函数开始
        return next(0);

        function next(i) {
            // 此时已经是最后一个函数了,因为已经没有下一个函数了,因此直接返回即可
            if (i === l) return;
            // 拿到相应的函数
            let fn = fns[i];
            // 执行当下函数,将参数透传过来,每个函数的next是一个函数,因此通过bind返
            // 回,留在每个函数内部调用,并保留参数,实现递归
            return fn(ctx, next.bind(null, (i + 1)));
        }
    }
}

这是此函数的基本实现,来验证一下:

// 依然是上面三个函数的数组
let arr = [fn1, fn2, fn3];
// 组合后的函数
let fn = wrap(arr);
// 执行 并 传入ctx
fn({ word: 'winter is comming!' });
// 控制台依次打印出
// 函数fn1执行...
   函数fn2执行...
   函数fn3执行...
   fn3 ending
   fn2 ending
   fn1 ending

这样就初步形成了链式。如果想让其支持异步,则将next改动一下

// 省略所有容错判断
function wrap(fns) {
    // 必然会返回一个函数...
    return (ctx) => {
        // 省略... 
        function next(i) {
            // 省略... 
            // 将此处以一个promise返回即可
            return Promise.resolve(fn(ctx, next.bind(null, (i + 1))));
        }
    }
}

而相应的函数也应该是async/await形式

async function fn1(ctx, next) {
    // ...
    await next();
    // ...
}
// 读者可定义一个delay函数模拟异步延迟,自行验证
function delay(time) {
    return new Promise(resolve => {
        setTimeout(() => resolve(), time);
    })
}

整体回顾一下此方式实现函数的链式调用,首先定义了一个函数接收数组返回新函数,此函数执行数组第一个函数,而得以实现链式的核心在于,在每个函数内部需要的时候调用next函数,来完成整个递归。它的特点在于:函数之间不存在依赖关系,只控制顺序关系。

通过组合实现链式

另一种场景是,需要对一个对象进行层层处理(包装),而每个函数侧重的点又不同,所以会将每一层函数剥离开来,形成多个函数,这时函数之间不仅需要控制顺序,而且存在值的依赖关系。如下面fn3的返回值为fn2的参数,fn2的返回值为fn1的参数...。

这时候我们的思路是:将所有的函数组合,形成一个复杂的函数,通过调用这个复杂函数,执行所有的逻辑,

期望的结果是:const fn = compose(arr); fn(arg) // 执行所有的逻辑。下面就实现compose函数:

// 模拟几个函数
function fn1(arg1) {
    // ...对arg1的操作逻辑
    console.log('fn1的参数:', arg1); 
    let arg = arg1 + 30;
    return arg;
}
function fn2(arg2) {
    // ...对arg2的操作逻辑
    console.log('fn2的参数:', arg2); 
    let arg = arg2 + 20;
    return arg;
}
function fn3(arg3) {
    // ...对arg3的操作逻辑
    console.log('fn3的参数:', arg3); 
    let arg = arg3 + 10;
    return arg;
}
// 省略所有容错判断
function compose(fns) {
    let l = fns.length;
    if (!l) throw new Error('至少得有一个函数呀...');
    
    // 一个,就直接返回这个函数...
    if (l === 1) return fns[0];
    
    // 数组迭代,返回一个函数,函数的实体为后一个函数执行的返回值作为前一个函
    // 数的参数,然后前一个函数执行,最终返回第一个函数的返回值
    return fns.reduce((a, b) => (...arg) => (a(b(...arg))));
}

这就是compose的基本实现,验证一下:

let fns = [fn1, fn2, fn3];

// 将函数组合,形成复杂函数
let fn = compose(fns);

// 执行
let r = fn(10); // 结果r为70
// 执行过程打印
fn3的参数: 10
fn2的参数: 20
fn1的参数: 40

此时我们是通过reduce函数迭代实现,传入数组的顺序为fn1,fn2,fn3,而执行的顺序为3,2,1,这是因为a(b(...arg))代码中,以后一个函数的返回值作为前一个函数的参数执行,如果换成b(a(...arg))或将reduce改成reduceRight,则执行顺序将与数组中函数的顺序一致。总结一下组合方式,就是将所有的函数组装成为一个复杂的函数,函数之间存在依赖关系,常用于对于某个对象的加强。最后调用时,执行所有的逻辑。

介绍完这三种链式调用的方式,各自都有适用的场景。熟悉源码的同学应该知道,第一种返回实例是jquery的实现方式,第二种通过next递归是koa的实现方式,第三种组合是redux的实现方式,当然还有其他的实现方式,而实质都是函数的链式调用,因此把这部分抽丝剥缕,总结一下。