js组件

作者: 209bd3bc6844 | 来源:发表于2018-02-25 17:43 被阅读0次

    比如我们要实现这样一个组件,就是一个输入框里面字数的计数。

    为了更清楚的演示,下面全部使用jQuery作为基础语言库。

    最简陋的写法

    <!DOCTYPE html>
    <html>
      <head>
        <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
      </head>
      <body>
        <input type="text" id="J_input">
        <script>
          $(function(){
            var input = $('#J_input');
            //用来获取字数
            function getNum(){
              return input.val().length;
            }
            function render() {
              var num = getNum();
    
              //没有字数的容器就新建一个
              if ($('#J_input_count').length == 0) {
                input.after('<span id="J_input_count"></span>');
              };
    
              $('#J_input_count').html(num+'个字');
            }
              //监听事件
            input.on('keyup',function(){
              render();
            });
    
            //初始化,第一次渲染
            render();
          })
        </script>
      </body>
    </html>
    

    以上是一串面向过程的写法。各种变量混乱,没有很好的隔离作用域,当页面变的复杂的时候,会很难去维护。
    于是我们会想到命名空间。就是把所以东西放在一个对象里。我们要调用的时候通过这个对象调用。用来实现全局变量只有一个。其他的变量都是这个对象内部的。

    var textCount = {
      input:null,
      init:function(config){
        this.input = $(config.id); //  textCount.init({id:'#J_input'})的调用方式使得this指向textCount。
        this.bind();
        //这边范围对应的对象,可以实现链式调用
        return this;
      },
      bind:function(){
        var self = this;
        this.input.on('keyup',function(){
     //这里的this作为inputCount对象内bind方法的内部函数,在这个内部函数里this是
     //指向全局,故将之前的this先存下来,便于在函数内部使用。
     //就像setTimeout函数的参数执行this也指向全局一样
          self.render();
        });
      },
      getNum:function(){
        return this.input.val().length;
      },
      //渲染元素
      render:function(){
        var num = this.getNum();
    
        if ($('#J_input_count').length == 0) {
          this.input.after('<span id="J_input_count"></span>');
        };
    
        $('#J_input_count').html(num+'个字');
      }
    }
    
    $(function() {
      //在domready后调用
      textCount.init({id:'#J_input'}).render();  //链式调用,先初始化胡渲染。
    })
    
    

    上面的代码getNum,bind虽说没有污染全局变量。但是其他代码可以很随意的改动这些。本应该内部私有的就不要被外面访问到。当代码量特别特别多的时候,很容易出现变量重复,或被修改的问题。我们接着改动
    利用匿名函数自执行返回值的形式形成的闭包

    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(config){
    
      }
    
      TextCountFun.prototype.init = function(config) {
        this.input = $(config.id);
        _bind(this);
    
        return this;
      };
    
      TextCountFun.prototype.render = function() {
        var num = _getNum(this);
    
        if ($('#J_input_count').length == 0) {
          this.input.after('<span id="J_input_count"></span>');
        };
    
        $('#J_input_count').html(num+'个字');
      };
      //返回构造函数
      return TextCountFun;
    
    })();
    
    $(function() {
      new TextCount().init({id:'#J_input'}).render();
    })
    

    这种写法,把所有的东西都包在了一个自动执行的闭包里面,所以不会受到外面的影响,并且只对外公开了TextCountFun构造函数,生成的对象只能访问到init,render方法。但是为什么要返回一个构造函数。直接返回render函数不可以吗。render函数里进行init初始化操作。这样的做法把init融合在render的做法就不推荐。函数的功能单一性被破坏。造成代码耦合。

    面向对象

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

    在编程的圈子里,面向对象一直是被认为最佳的编写代码方式。比如java,就是因为把面向对象发挥到了极致,所以多个人写出来的代码都很接近,维护也很方便。js中没有类Class。实现继承是靠原型链。我们可以用原型链封装出一个类似的Class。真正的oo语言,实现继承是靠复制。传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。js通过混入mixin来模仿类。并且实现多继承。
    mixin

    (function(){
     //initializing是为了解决我们之前说的继承导致原型有多余参数的问题。
     //当我们直接将父类的实例赋值给子类原型时。是会调用一次父类的构造函数的。所以这边会把真正的构造流程放到init函数里面,通过initializing来表示当前是不是处于构造原型阶段,
     //为true的话就不会调用init。
     //fnTest用来匹配代码里面有没有使用super关键字。对于一些浏览器`function(){xyz;}`会生成个字符串,并且会把里面的代码弄出来,有的浏览器就不会。
     //`/xyz/.test(function(){xyz;})`为true代表浏览器支持看到函数的内部代码,所以用`/\b_super\b/`来匹配。如果不行,就不管三七二十一。
     //所有的函数都算有super关键字,于是就是个必定匹配的正则。
     var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
    
     // The base Class implementation (does nothing)
     // 超级父类
     this.Class = function(){};
    
     // Create a new Class that inherits from this class
     // 生成一个类,这个类会具有extend方法用于继续继承下去
     Class.extend = function(prop) {// 这里的extend很像mixin
       //保留当前类,一般是父类的原型
       //this指向父类。初次时指向Class超级父类
       var _super = this.prototype;
      
       // Instantiate a base class (but only create the instance,
       // don't run the init constructor)
       //开关 用来使原型赋值时不调用真正的构成流程。就是执行var p = Class.extend()时候不执行init。而是执行 new p()和Person.extend()时候执行init。
       initializing = true;
       var prototype = new this();
       initializing = false;
      
       // Copy the properties over onto the new prototype
       for (var name in prop) {
         // Check if we're overwriting an existing function
         //这边其实就是很简单的将prop的属性混入到子类的原型上。如果是函数我们就要做一些特殊处理
         prototype[name] = typeof prop[name] == "function" &&
           typeof _super[name] == "function" && fnTest.test(prop[name]) ?
           (function(name, fn){
             //通过闭包,返回一个新的操作函数.在外面包一层,这样我们可以做些额外的处理
             return function() {
               var tmp = this._super;// 这个this是实例化对象的。var p = new Person(true);就是p的
              
               // Add a new ._super() method that is the same method
               // but on the super-class
               // 调用一个函数时,会给this注入一个_super方法用来调用父类的同名方法
               this._super = _super[name];
              
               // The method only need to be bound temporarily, so we
               // remove it when we're done executing
               //因为上面的赋值,是的这边的fn里面可以通过_super调用到父类同名方法
               var ret = fn.apply(this, arguments);  
               //离开时 保存现场环境,恢复值。
               this._super = tmp;
              
               return ret;
             };
           })(name, prop[name]) :
           prop[name];
       }
      
       // 这边是返回的类,其实就是我们返回的子类
       function Class1() {
         // All construction is actually done in the init method
         if ( !initializing && this.init )
           this.init.apply(this, arguments);
       }
      
       // 赋值原型链,完成继承
       Class1.prototype = prototype;
      
       // 改变constructor引用
       Class1.prototype.constructor = Class1;
    
       // 为子类也添加extend方法
       Class1.extend = arguments.callee;
      
       return Class1;
     };
    })();
    

    使用方法

    var Person = Class.extend({
      init: function(isDancing){
        this.dancing = isDancing;
      },
      dance: function(){
        return this.dancing;
      }
    });
     
    var Ninja = Person.extend({
      init: function(){
        this._super( false );
      },
      dance: function(){
        // Call the inherited version of dance()
        return this._super();
      },
      swingSword: function(){
        return true;
      }
    });
     
    var p = new Person(true);
    p.dance(); // => true
     
    var n = new Ninja();
    n.dance(); // => false
    n.swingSword(); // => true
     
    // Should all be true
    p instanceof Person && p instanceof Class &&
    n instanceof Ninja && n instanceof Person && n instanceof Class
    
    

    javascript oo实现
    有了这个类的扩展,我们可以这么编写代码了:

    var TextCount = Class.extend({
      init:function(config){
        this.input = $(config.id);
        this._bind();
        this.render();
      },
      render:function() {
        var num = this._getNum();
    
        if ($('#J_input_count').length == 0) {
          this.input.after('<span id="J_input_count"></span>');
        };
    
        $('#J_input_count').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:"#J_input"
      });
    })
    

    抽象出base

    可以看到,我们的组件有些方法,是大部分组件都会有的。

    • 比如init用来初始化属性。
    • 比如render用来处理渲染的逻辑。
    • 比如bind用来处理事件的绑定。
      当然这也是一种约定俗成的规范了。如果大家全部按照这种风格来写代码,开发大规模组件库就变得更加规范,相互之间配合也更容易。
      这个时候面向对象的好处就来了,我们抽象出一个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类主要把组件的一般性内容都提取了出来,这样我们编写组件时可以直接继承base类,覆盖里面的bind和render方法。

    于是我们可以这么写代码:

    var TextCount = Base.extend({
      _getNum:function(){
        return this.get('input').val().length;
      },
      bind:function(){
        var self = this;
        self.get('input').on('keyup',function(){
          self.render();
        });
      },
      render:function() {
        var num = this._getNum();
    
        if ($('#J_input_count').length == 0) {
          this.get('input').after('<span id="J_input_count"></span>');
        };
    
        $('#J_input_count').html(num+'个字');
    
      }
    })
    
    $(function() {
      new TextCount({
      //这边直接传input的节点了,因为属性的赋值都是自动的。
        input:$("#J_input")
      });
    })
    

    javascript组件化

    相关文章

      网友评论

          本文标题:js组件

          本文链接:https://www.haomeiwen.com/subject/ivvthxtx.html