专栏首页Super 前端JavaScript组件设计思想

JavaScript组件设计思想

上个周,并肩作战的田老师离职了,尽管在一起愉快玩耍的时间不到一年,自己仍然还是从其身上学到、体会到了好多关于知识、理想的东西。对于大多数年轻人关于“晚上想想千条路,早上起来走原路”的现状,他那种敢于甩掉一切去做自己感兴趣、梦想的事的勇气是我所钦佩的。在此,祝愿田老师一切顺利。 在最后一次交接会议上,田老师阐述了一个观点,“当你学会了用‘分层思想’去看待事情,任何的问题都不是问题,都可以实现”。当然,这里说的是在程序设计方面。自己觉的很有道理,但是体会不是很深。 紧跟着,这个周期盼已久的“重构版热图”上线了,“低bug率、高速度”等在各方面指标瞬间秒杀“旧版热图”,让大家眼前一亮。随即,我们组织了分享讨论会,让匡哥讲述其重构过程中的设计思路。 大致思想如下:将每个功能点最小颗粒化、然后将其封装成模块;创建数据中心,使各个模块不在互相调用嵌套,所有的依赖和调用全部通过数据中心(这里使用自定义事件实现的观察者模式);所有的网状的需求点,划点成线,最终形成操作流。 这不就是“分层思想”的一种体现吗?我陷入了沉思~~~ 现在,大前端流行组件化、模块化。然而,我们的模块又该如何设计实现呢?

下面的实例,参考自【javascript组件开发方式】【GitHub示例地址

文本框内输入内容,后面动态显示输入的字符长度。

<div id="container">
    <input id="content" />
</div>

1. 函数式写法

$(function() {
    var $content = $("#content");
    // 获取字数
    function getNum() {
        return $content.val().length;
    }
    // 渲染元素
    function render() {
        var num = getNum();
        if($("#contentCount").length === 0) {
            // 不存在统计字符的DOM元素
            $content.after("<span id='contentCount'></span>");
        }
        $("#contentCount").html(num + "个字");
    }
    // 监听时间
    $content.on("keyup", function() {
        render();
    });
});

缺点:变量混乱,没有很好的隔离作用域,当页面变得复杂的时候,很难维护。

2. 使用变量模拟单个命名空间,统一入口调用方法

var textCount = {
    input: null,
    init: function(config) {
        this.input = $(config.id);
        this.bind();
        return this;    // 方便实现链式调用
    },
    bind: function() {
        var self = this;
        this.input.on("keyup", function() {
            self.render();
        });
    },
    render: function() {
        var num = this.getNum();
        if($("#contentCount").length === 0) {
            this.input.after("<span id='contentCount'></span>");
        }
        $("#contentCount").html(num + "个字");
    },
    getNum: function() {
        return this.input.val().length;
    }
};
$(function() {
    textCount.init({id: '#content'}).render();
});

缺点:这种写法没有私有的概念。其他代码可以很随意的改动这些,容易出现变量重复,或被修改的问题。

3. 函数闭包的写法

把所有的东西都包在了一个自动执行的闭包里面,所以不会受到外面的影响,并且只对外公开了TextCountFun构造函数,生成的对象只能访问到init,render方法。事实上大部分的jQuery插件都是这种写法。

var textCount = (function() {
    // 私有方法
    var _bind = function(that) {
        that.input.on("keyup", function() {
            that.render();
        });
    };
    var _getNum = function(that) {
        return that.input.val().length;
    };
    var TextCountFun = function() {};
    TextCountFun.prototype.init = function(config) {
        this.input = $(config.id);
        _bind(this);
        return this;
    };
    TextCountFun.prototype.render = function() {
        var num = _getNum(this);
        if($("#contentCount").length === 0) {
            this.input.after("<span id='contentCount'></span>");
        }
        $("#contentCount").html(num + "个字");
    };
    // 返回构造函数
    return TextCountFun;
 })();
 $(function() {
    new textCount().init({id: '#content'}).render();
 });    

4.面向对象

var Class = (function() {
  var _mix = function(r, s) {
        for (var p in s) {
            if (s.hasOwnProperty(p)) {
                r[p] = s[p];
            }
        }
  }
  var _extend = function() {
        //开关 用来使生成原型时,不调用真正的构成流程init 
        this.initPrototype = true;
        var prototype = new this();
        this.initPrototype = false;
        var items = Array.prototype.slice.call(arguments) || [];
        var item;
        //支持混入多个属性,并且支持{}也支持Function 
        while (item = items.shift()) {
            _mix(prototype, item.prototype || item);
        }
      // 这边是返回的类,其实就是我们返回的子类 
        function SubClass() {
            if (!SubClass.initPrototype && this.init) {
                this.init.apply(this, arguments); //调用init真正的构造函数 
            }
        }
      // 赋值原型链,完成继承 
        SubClass.prototype = prototype 
        // 改变constructor引用 
        SubClass.prototype.constructor = SubClass 
        // 为子类也添加extend方法 
        SubClass.extend = _extend 
        return SubClass 
    }
    //超级父类 
    var Class = function() {};
    //为超级父类添加extend方法 
    Class.extend = _extend;
    return Class;
})();

var TextCount = Class.extend({
  init: function(config){
        this.input = $(config.id);
        this._bind();
        this.render();
  },
  render: function() {
        var num = this._getNum();
        if ($('#contentCount').length == 0) {
            this.input.after('<span id="contentCount"></span>');
        }
        $('#contentCount').html(num + '个字');
  },
  _getNum: function(){
        return this.input.val().length;
  },
  _bind: function(){
        var self = this;
        self.input.on('keyup', function() {
            self.render();
        });
  }
});
$(function() {
  new TextCount({id:"#content"});
});

缺点:当一个页面特别复杂,当我们需要的组件越来越多,当我们需要做一套组件。仅仅用这个就不行了。首先的问题就是,这种写法太灵活了,写单个组件还可以。如果我们需要做一套风格相近的组件,而且是多个人同时在写。那真的是噩梦。

5. 引入事件机制(观察者模式)

下述创建对象采用《构造函数和原型模式组合使用》,此方式最广泛、认同度最高。

function Event(config) {
    // 私用,外部不允许直接调用
    this._config = config;  // 存储相关配置信息
    this._events = {};      // 存储所有处理函数 
 };
 Event.prototype = {
    constructor: Event,
    // 监听事件 key:事件类型,listener:事件处理函数(可以同时绑定多个不同类型事件)
    on: function(keys, listener) {
        var keyList = keys.split(/[\,\s\;]/);   // 支持同时绑定多个事件,用【逗号、分号或空格隔开】
        var index = keyList.length;
        while (index) {
            index--;
            var key = keyList[index];
            // 不存在当前类型的事件
            if (!this._events[key]) {
                this._events[key] = [];     // 这里指定为数组,可以多次绑定同一事件
            }
            this._events[key].push(listener);
        }
    },
    // 只能移除指定类型事件(一个)
    off: function (key, listener) {
        // 不指定事件类型,移除全部事件
        if (!key) {
            this._events = {};
            return;
        }
        var event = this._events[key];
        // 不存在要移除的事件,直接返回
        if (!event) {
            return;
        }
        // 不指定事件处理程序,移除指定类型
        if (!listener) {
            delete this._events[key];
        } else {
            var length = event.length;
            while (length > 0) {
                length--;
                if (event[length] === listener) {
                    event.splice(length, 1);        // 移除指定类型、指定处理程序的事件
                }
            }
        }
    },      
    // 触发对应类型的事件,私有,外部不允许调用(为达到统一出口目的)
    _emit: function (key, args) {
        var event = this._events[key];
        if (event) {
            var length = event.length;
            var i = 0;
            while (i < length) {
                event[i](args);
                i++;
            }
        }
    }
 }
 Event.prototype.setConfig = function(config) {
    this._config = config;
 };
 Event.prototype.getConfig = function() {
    return this._config;
 };
 Event.prototype.setInput = function(input) {
    this.setConfig(input)
    // input信息改变,触发自定义change事件
    this._emit("inputChange");
 }

var customerEvent = new Event();
// 监听自定义inputchange事件
customerEvent.on("inputChange", function() {
    var $input = customerEvent.getConfig();
    var num = $input.val().length;
    if ($('#contentCount').length == 0) {
        $input.after('<span id="contentCount"></span>');
    }
    $('#contentCount').html(num + '个字');
});

$("#content").on("keyup", function() {
    customerEvent.setInput($(this));
});

说明:由于功能比较单一,所以不能很好的体会到上述“观察者模式”的好处。试想,将上述抽离为两个业务模块,即当input内容长度发生改变(模块A),要通知另一个业务模块去改变对应显示(模块B)。如果不采用上述模式,很容易造成模块之间的互相调用。很容易造成在不知情的情况下修改了模块A导致了模板B不能正常使用。而上述方式,提供了一种分层的方式。A模块处理A的任务、B模块处理B的任务。模块之间的调用和耦合全局交给中间控制层(上述Event所在层)去控制。 注意:所有的时间触发,都在中间控制层;而相关的事件监听和引起事件触发的动作则在相关模块。为了正常通信,相关模块需要共享同一个中间控制层实例。

6. 加强版

// Base封装组件的各个过程,并具有时间机制
var Base = Class.extend({
    init:function(config){
        //自动保存配置项
        this.__config = config
        this.bind()
        this.render()
    },
    //可以使用get来获取配置项
    get:function(key){
        return this.__config[key]
    },
    //可以使用set来设置配置项
    set:function(key,value){
        this.__config[key] = value
    },
    bind:function(){},
    render:function() {},
    //定义销毁的方法,一些收尾工作都应该在这里
    destroy:function(){}
});


/**
 * 加强版Base
 * 事件代理:不需要用户自己去找dom元素绑定监听,也不需要用户去关心什么时候销毁。
 * 模板渲染:用户不需要覆盖render方法,而是覆盖实现setUp方法。可以通过在setUp里面调用render来达到渲染对应html的目的。
 * 单向绑定:通过setChuckdata方法,更新数据,同时会更新html内容,不再需要dom操作。
 */
var RichBase = Base.extend({
    EVENTS: {},
    template: '',
    init: function(config){
        //存储配置项
        this.__config = config;
        //解析代理事件
        this._delegateEvent();
        this.setUp();
    },
    //循环遍历EVENTS,使用jQuery的delegate代理到parentNode
    _delegateEvent: function(){
        var self = this;
        var events = this.EVENTS || {};
        var eventObjs, fn, select, type;
        var parentNode = this.get('parentNode') || $(document.body);
        for (select in events) {
            eventObjs = events[select];
            for (type in eventObjs) {
                fn = eventObjs[type];
                parentNode.delegate(select,type,function(e){
                    fn.call(null,self,e);
                })
            }
        }
    },
    //支持underscore的极简模板语法
    //用来渲染模板,这边是抄的underscore的。非常简单的模板引擎,支持原生的js语法
    _parseTemplate: function(str,data){
        /**
         * http://ejohn.org/blog/javascript-micro-templating/
         * https://github.com/jashkenas/underscore/blob/0.1.0/underscore.js#L399
         */
        var fn = new Function('obj',
              'var p=[],print=function(){p.push.apply(p,arguments);};' +
              'with(obj){p.push(\'' + str.replace(/[\r\t\n]/g, " ")
                                        .split("<%").join("\t")
                                        .replace(/((^|%>)[^\t]*)'/g, "$1\r")
                                        .replace(/\t=(.*?)%>/g, "',$1,'")
                                        .split("\t").join("');")
                                        .split("%>").join("p.push('")
                                        .split("\r").join("\\'") +
              "');}return p.join('');");
        return data ? fn(data) : fn;
    },
    //提供给子类覆盖实现
    setUp: function(){
        this.render();
    },
    //用来实现刷新,只需要传入之前render时的数据里的key还有更新值,就可以自动刷新模板
    setChuckdata: function(key,value){
        var self = this;
        var data = self.get('__renderData');
        //更新对应的值
        data[key] = value;
        if (!this.template) return;
        //重新渲染
        var newHtmlNode = $(self._parseTemplate(this.template,data));
        //拿到存储的渲染后的节点
        var currentNode = self.get('__currentNode');
        if (!currentNode) return;
        //替换内容
        currentNode.replaceWith(newHtmlNode);
        self.set('__currentNode',newHtmlNode);
    },
    //使用data来渲染模板并且append到parentNode下面
    render: function(data){
        var self = this;
        //先存储起来渲染的data,方便后面setChuckdata获取使用
        self.set('__renderData', data);
        if (!this.template) return;
        //使用_parseTemplate解析渲染模板生成html
        //子类可以覆盖这个方法使用其他的模板引擎解析
        var html = self._parseTemplate(this.template,data);
        var parentNode = this.get('parentNode') || $(document.body);
        var currentNode = $(html);
        //保存下来留待后面的区域刷新
        //存储起来,方便后面setChuckdata获取使用
        self.set('__currentNode',currentNode);
        parentNode.append(currentNode);
    },
    destroy: function(){
        var self = this;
        //去掉自身的事件监听
        self.off();
        //删除渲染好的dom节点
        self.get('__currentNode').remove();
        //去掉绑定的代理事件
        var events = self.EVENTS || {};
        var eventObjs,fn,select,type;
        var parentNode = self.get('parentNode');
        for (select in events) {
            eventObjs = events[select];
            for (type in eventObjs) {
                fn = eventObjs[type];
                parentNode.undelegate(select,type,fn);
            }
        }
    },
    //可以使用get来获取配置项
    get: function(key){
        return this.__config[key]
    },
    //可以使用set来设置配置项
    set: function(key, value){
        this.__config[key] = value
    }
});

/**
 * (1)事件的解析跟代理,全部代理到parentNode上面。
 * (2)render抽出来,用户只需要实现setUp方法。如果需要模板支持就在setUp里面调用render来渲染模板
 * (3)可以通过setChuckdata来刷新模板,实现单向绑定。
 */
var TextCount = RichBase.extend({
    //事件直接在这里注册,会代理到parentNode节点,parentNode节点在下面指定
    EVENTS: {
        //选择器字符串,支持所有jQuery风格的选择器
        'input':{
            //注册keyup事件
            keyup:function(self,e){
                //单向绑定,修改数据直接更新对应模板
                self.setChuckdata('count',self._getNum());
            }
        }
    },
    //指定当前组件的模板
    template: '<span id="contentCount"><%= count %>个字</span>',
    //私有方法
    _getNum: function(){
        return this.get('input').val().length || 0
    },
    //覆盖实现setUp方法,所有逻辑写在这里。最后可以使用render来决定需不需要渲染模板
    //模板渲染后会append到parentNode节点下面,如果未指定,会append到document.body
    setUp: function(){
        var self = this;

        var input = this.get('parentNode').find('#content');
        self.set('input', input);

        var num = this._getNum();
        //赋值数据,渲染模板,选用。有的组件没有对应的模板就可以不调用这步。
        self.render({
            count: num
        });
    }
});

$(function() {
    //传入parentNode节点,组件会挂载到这个节点上。所有事件都会代理到这个上面。
    new TextCount({parentNode: $("#container")});
});

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 学习算法必须要了解的数据结构

    简而言之,数据结构是一个以特定形式存储数据的容器。这种“形式”允许数据结构在某些操作中更加高效。

    深度学习与Python
  • Android使用WebView开发常见的坑

    现在的App基本上都会使用Native+H5的方式来开发的,例如网易新闻详情页面,微信公号详情页面都会使用WebView开发。这样可以很容易实现图文排版的需求,...

    阳仔
  • MPEG 127th会议(2019.7)主要进展

    MPEG组织于2019年7月8日至12日举行了第127届会议,会议参与人数首次突破600大关。以下为会议上集中讨论取得的一些重要结果。

    用户1324186
  • 用python实现“桶排序”

    将待排序的数据分到几个有序的桶里,每个桶的数据单独排序,桶内排完序后,再按顺序依次取出,组成有序序列。

    小草AI
  • python 触发snort告警

    import optparse from scapy.all import * from random import randint

    用户5760343
  • wxPython+opencv 打造自己的画图板

    参数三: filetypes,比如我上面的设置过滤掉了其他非.jpg、.png文件

    月小水长
  • Android 内存泄漏总结

    内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实...

    阳仔
  • python 模拟syn攻击

    def synFlood(src, tgt): # TCP源端口不断自增一,而目标端口513不变 for sport in range(1024, 6553...

    用户5760343
  • Python 打造自由 DIY 群聊机器人

    这几天我的一个小伙伴问我能不能给 Ta 做一个配置灵活的微信群聊天机器人,之前了解过 itchat 库的使用,我就爽快的答应了,花了一个晚上,终于做出了雏形。

    月小水长
  • PHP算法——四大基础算法

    对于大多数业务开发来说,平时很少需要自己实现数据结构与算法,都是利用已经封装好的现成接口,类库来推测、翻译业务逻辑,但是,不需要自己实现,并不代表什么都不需要了...

    猿哥

扫码关注云+社区

领取腾讯云代金券