JS的链式编程

前言

链式编程实际是将多个方法(函数)通过某种方式链接在一起,使多个逻辑块能按流程逐步执行(或跳过执行),从而实现解耦,在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
    });
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容