资讯专栏INFORMATION COLUMN

更好的共用化封装是程序员不断追求的目标

Jonathan Shieber / 363人阅读

摘要:动态数据的区别是数据的变动性,可能是用户操作构造,可能是服务端查询数据返回,可能是本地缓存需要反复更新修改的数据等等。

程序员总是在做重复性的工作,常常因为80%公用的内容,但有20%的不同之处,导致要重写,或复制修改;

更好的共用化封装是程序员不断追求的目标,设计的公用性与适用度还有效率之间要找平衡点;

举些例子,分享给新手!(示例来自我的 fixedstar 引擎)

1. 附加功能封包

辅助功能设计尽可能不影响原函数设计,也支持原函数的变动升级;

只关注辅助功能本身使用场景,不受业务应用场景限制;

单一功能实现,不要多功能集成;

示例1:

高频触发的行为常常会产生不必要的消耗,可以设计节流函数来控制使用频率。

/**
 * 获取在控制执行频率的节流函数
 *
 * - 与上一次执行时间超过等待时间时可以直接触发;
 * - 间隔时间之内调用不会触发,保留最后一次调用定时在等待时间后触发;
 *
 * 可用于高频发事件的回调,如:onresize onscroll 等等
 *
 * @param {function} cb 回调
 * @param {object} [thisObj] 回调函数的this对象
 * @param {number} [wait=0] 设置两次执行间隔最小等待时间,没有设置时间则为下一帧解锁
 * @return {function} 有限制的回调
 */
function throttle(cb, thisObj, wait) {
    if( wait===undefined ) wait = 0;
    var timer = null;
    var locked = false;
    var isfirst = true;
    var now;
    return function () {
        var args = arguments;
        var _this = this;
        // 定时执行
        var fn = function () {
            locked = false;
            isfirst = false;
            now = Date.now();
            timer = setTimeout(function () {
                locked = false;
                timer = null;
                isfirst = true;
            }, wait);
            cb.apply(thisObj || _this, args);
        };
        // 如果锁了,说明前面有调用并且没有到两次间隔最小执行等待时间
        if(locked) {
            clearTimeout(timer);
            timer = null;
            // 如果到了最大等待时间,直接执行,并延时等待
            if( Date.now()-now>=wait ) {
                fn();
                now = Date.now();
            }
        } else {
            // 没有锁定的第一次直接执行,并延时等待
            if( isfirst ) fn();
            // 在第一次或执行后的第一次调用中,进行锁定
            now = Date.now();
            locked = true;
        }
        // 如果没有定时任务加上
        if( !timer ) timer = setTimeout(fn, wait-(Date.now()-now));
    };
};

上例中代码重新编辑过,setTimeout 会影响效率,原代码中使用 thread 进行调用,此处为了方便演示,暂使用 setTimeout 替换,关于 thread 有时间再开新贴。

调用:

var t, st = 0;
var fn = fx.object.throttle(function (i) {
    var now = Date.now();
    console.log(i, "ok", now-st);
    st = now;
}, null, 30, 2000);
// 下面代码只执行 1和4,因为1是直接调用,2、3、4都在1调用之后没有到间隔时间会一个个覆盖,最后留下来是4,会定时间隔执行
fn(1);fn(2);fn(3);fn(4);

// 测试高频率10ms时,仍然按照30ms间隔进行触发
t = setInterval(fn, 10);
//clearInterval(t);
// 测试较低频率50ms时,会按照50ms间隔进行触发
//t = setInterval(fn, 50);
//clearInterval(t);
示例2:

如通常做优化时,需要计算代码运行时间,工具有时不能很细致也受到环境限制,所以不可避免需要写一些计时的脚本调试或统计:

/**
 * 生成一个计算函数运行时间的新函数(可以打印或使用回调函数返回运行时间值)
 *
 * 新函数与原函数调用及功能不变;
 *
 * @param {function} cb    测试函数
 * @param {object} [thisObj=cb.this]    测试函数的 this,如果参数为 undefined 时 this 为函数内部绑定的this
 * @param {function} [timeCb] 用来获取运行时间回调函数
 * @param {number} [timeCb.time] 运行时间 ms
 * @param {number} [timeCb.cb] 原函数
 * @param {...} [timeCb.args] 多参数,传入原函数的参数
 * @return {function} 返回cb原功能函数增加了计算功能后封包的函数
 */
function runTime(cb, thisObj, timeCb) {
    var t;
    return function () {
        var args = Array.prototype.slice.call(arguments);
        t = Date.now();
        if( thisObj===undefined ) thisObj = this;
        var ret = cb.apply(thisObj, args);
        var v = Date.now()-t;
        if( timeCb ) {
            timeCb.apply(null, [v, cb].concat(args));
        } else {
            console.log(cb.name, "runTime:", v, "ms");
        }
        return ret;
    };
};

调用:

 function sum(n) {
    var m = 0;
    for (var i = 0; i < n; i++) {
      m+=i;
    }
    return m;
 }


 function getRunTime(time) {
    if( time<1000 ) {
        console.log("运行时间1秒内可以接受", time);
    } else {
        console.warn("运行时间超过了1秒不能接受", time);
    }
 }

 var sum2 = runTime(sum);
 // 临时调试打印结果
 console.log(sum2(100000000));
 // 需要获取时间处理相关逻辑
 var sum3 = runTime(sum, null, getRunTime);
 sum3(100000000);
 sum3(5000000000);
 // 如果不想在生产时使用,可以设计运行条件,或在gulp生成时排除
 sum4 = window.isDebug ? runTime(sum) : sum;
 sum4(100000000);
2. 通用的动态处理

通常获取数据属性都是静态的,也就是 o.x 类似这样,但当数据有一定不确定性时,如 o.point.x 这时如果 o.point 有可能为 undefined 时,就会报错 Cannot read property "x" of undefined;

有人可能会问,为什么不将数据构造好,避免这样的情况呢!或在发现问题补上数据即可...

上面说的是静态数据的处理方式,如系统配置数据。动态数据的区别是数据的变动性,可能是用户操作构造,可能是服务端查询数据返回,可能是本地缓存需要反复更新修改的数据等等。

动态数据会产生较多结构设计固定,但实际内容不确定,或一定要写成固定式的就会增加很多结构维护消耗。如:

var users = {
    user1: {
        info: {
            hobby: ["玩游戏", "蓝球", "看电影"]
        },
        // ...
    },
    user2: {
        info: {
            hobby: []
        },
        // ...
    },
    user3: {}
    // ...
};

如果业务需求要将所有没有填写爱好的用户收集起来,常规判断方法是:

(试想一下,再增加些写数据的需求,复杂度就更高了,不仅需要判断对象存在性,还要一层层创建数据,如将填写了爱好的用户增加系统动态评分等等)

var ret = [];
for( var k in users ) {
    var user = users[k];
    if( user && user.info && user.info.hobby ) {
        var fristHobby = user.info.hobby[0];
        if( fristHobby ) ret.push(fristHobby);
    }
}
console.log(ret);

如果你习惯这样的多重判断,反复出现或在不同业务逻辑中返回硬性封装而不感到厌烦,那就可以跳过此文。

实际我们想一次能拿到属性,理想调用如下:

var ret = [];
for( var k in users ) {
    var fristHobby = attr(users, k + ".info.hobby[0]");
    if( fristHobby ) ret.push(fristHobby);
}
console.log(ret);

代码实现:

/**
 * 获取或设置对象属性,允许多级自动创建
 *
 * @param {object} obj 需要属性的对象,通常操作JSON
 * @param {String} name 属性名称(.[]是关键字不可以命名),可以多级,如:name 或 msg.title 或 msg.list[0].user
 * @param value 赋值
 * @return 返回对象属性,未找到返回 undefined
 */
function attr(obj, name, value) {
    if (!obj || typeof obj != "object") return undefined;
    var pAry = name.split(".");
    var key = null;
    var isSet = (arguments.length > 2);
    for (var i = 0; i < pAry.length; i++) {
        key = pAry[i].trim();
        // 如果此键为数组,先要指向数组元素
        if (/^[^[]+[d+]$/.test(key)) {
            var tempAry = key.split(/[[]]/g);
            key = tempAry[0];
            if (!obj[key]) {
                if (!isSet) return undefined;
                obj[key] = [];
            }
            obj = obj[key];
            key = parseInt(tempAry[1]);
        }
        // 然后判断是否最后一级,则直接赋值 结束
        if (i >= pAry.length - 1) {
            if (!isSet) return obj[key];
            obj[key] = value;
            break;
        }
        // 否则还有下级,则指向下级对象
        if (!obj[key] || typeof obj[key] != "object") {
            if (!isSet) return undefined;
            obj[key] = {};
        }
        obj = obj[key];
    }
    return value;
};

使用这个封装完成用户增加系统动态评分就简单了,此方法支持多级写数据:

var ret = [];
for( var k in users ) {
    var n = 0;
    
    var fristHobby = attr(users, k + ".info.hobby[0]");
    if( fristHobby ) {
        ret.push(fristHobby);
        n++;
    }
    
    var score = attr(users, k + ".info.score");
    attr(users, k + ".info.score", score ? score + n : n);
}
console.log(ret, users);
更多例子

未完待续...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81141.html

相关文章

  • FEDAY2016之旅

    摘要:前戏补上参会的完整记录,这个问题从一开始我就是准备自问自答的,希望可以通过这种形式把大会的干货分享给更多人。 showImg(http://7xqy7v.com1.z0.glb.clouddn.com/colorful/blog/feday2.png); 前戏 2016/3/21 补上参会的完整记录,这个问题从一开始我就是准备自问自答的,希望可以通过这种形式把大会的干货分享给更多人。 ...

    red_bricks 评论0 收藏0
  • PHP面试之面向对象(1)

    摘要:二面向对象有什么特征面向对象的主要特征有抽象继承封装和多态。析构函数析构函数是在引入的,它的作用与调用时机和构造函数刚好相反,它在对象被销毁时自动执行。 PHP面试专栏正式起更,每周一、三、五更新,提供最好最优质的PHP面试内容。PHP中面向对象常考的知识点有以下7点,我将会从以下几点进行详细介绍说明,帮助你更好的应对PHP面试常考的面向对象相关的知识点和考题。整个面向对象文章的结构涉...

    phodal 评论0 收藏0
  • 前端进击巨人(五):学会函数柯里(curry)

    摘要:函数柯里化是把支持多个参数的函数变成接收单一参数的函数,并返回一个函数能接收处理剩余参数,而反柯里化就是把参数全部释放出来。但在一些复杂的业务逻辑封装中,函数柯里化能够为我们提供更好的应对方案,让我们的函数更具自由度和灵活性。 showImg(https://segmentfault.com/img/bVburN1?w=800&h=600); 柯里化(Curring, 以逻辑学家Has...

    chengtao1633 评论0 收藏0
  • 追求极致设计理念!用 RISC-V 从头设计 CKB 虚拟机

    摘要:目前,比特币使用的是来进行交易签名,并且在共识协议中使用了哈希算法。尽管的实现提供的是最流行的加密算法,但我们鼓励社区提供更优化的加密算法实现以减少运行时开销。 Nervos 底层公链 CKB 的虚拟机(CKB-VM)是基于 RISC-V 指令集打造的区块链虚拟机。在上一堂分享中,我们简单介绍了区块链虚拟机,以及我们理想中的区块链虚拟机的样子。在本篇文章中,CKB-VM 设计者将详细的...

    he_xd 评论0 收藏0
  • 能源行业数字转型方向与人才培养

    摘要:能源行业数字化转型方向与人才培养大数据微软雅黑全球能源行业数字化转型的使命是实现敏捷能源。微软雅黑油气行业数字化转型聚焦生产服务技术新业务运营人才生态七大重点方向,结合企业发展实际,挖掘数据驱动价值增值的关键点,布局未来。 随着中国能源结构持续大幅优化,清洁低碳化进程不断加快,中国市场蕴含巨大机遇,国内对能源行业的人才需求...

    番茄西红柿 评论0 收藏2637

发表评论

0条评论

最新活动
阅读需要支付1元查看
<