JavaScript组件设计思想

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

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

JavaScript组件设计思想
文本框内输入内容,后面动态显示输入的字符长度。

<div id="container">
    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("");
        }
        $("#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("");
        }
        $("#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("");
        }
        $("#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('');
        }
        $('#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('');
    }
    $('#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: '<%= count %>个字',
    //私有方法
    _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")});
});

你可能感兴趣的:(JavaScript)