欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaScript组件设计思想

程序员文章站 2022-03-27 12:29:27
...
“当你学会了用‘分层思想’去看待事情,任何的问题都不是问题,都可以实现”。当然,这里说的是在程序设计方面。自己觉的很有道理,但是体会不是很深。 

紧跟着,这个周期盼已久的“重构版热图”上线了,“低bug率、高速度”等在各方面指标瞬间秒杀“旧版热图”,

大致思想如下:将每个功能点最小颗粒化、然后将其封装成模块;创建数据中心,使各个模块不在互相调用嵌套,所有的依赖和调用全部通过数据中心(这里使用自定义事件实现的观察者模式);所有的网状的需求点,划点成线,最终形成操作流。 
这不就是“分层思想”的一种体现吗?

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

<div id="container">
    <input id="content" />
</div>
  • 1
  • 2
  • 3

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();
    });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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

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();
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

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

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();
 });    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

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"});
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

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

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));
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85

说明:由于功能比较单一,所以不能很好的体会到上述“观察者模式”的好处。试想,将上述抽离为两个业务模块,即当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")});
});