JS的链式编程

4,362 阅读5分钟

前言

链式编程实际是将多个方法(函数)通过某种方式链接在一起,使多个逻辑块能按流程逐步执行(或跳过执行),从而实现解耦,在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('<p>123</>')
    .appendTo('<div>dddd</div>')

我们来写一个对象里挂载多个方法:

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接受两个参数,分别是resolvereject,前者作为成功回调,后者作为失败回调

下面展示如何使用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
});