• 从零开始,DIY一个jQuery(2)


    在上篇文章我们简单实现了一个 jQuery 的基础结构,不过为了顺应潮流,这次咱把它改为模块化的写法,此举得以有效提升项目的可维护性,因此在后续也将以模块化形式进行持续开发。

    模块化开发和编译需要用上 ES6 和 rollup,具体原因和使用方法请参照我之前的《冗余代码都走开——前端模块打包利器 Rollup.js 入门》一文。

    本期代码均挂在我的github上,有需要的童鞋自行下载。

    1. 基本配置

    为了让 rollup 得以静态解析模块,从而减少可能存在的冗余代码,我们得用上 ES6 的解构赋值语法,因此得配合 babel 辅助开发。

    在目录下我们新建一个 babel 配置“.babelrc”:

    {
      "presets": ["es2015-rollup"]
    }

    以及 rollup 配置“rollup.comfig.js”:

    var rollup = require( 'rollup' );
    var babel = require('rollup-plugin-babel');
    
    rollup.rollup({
        entry: 'src/jquery.js',
        plugins: [ babel() ]
    }).then( function ( bundle ) {
        bundle.write({
            format: 'umd',
            moduleName: 'jQuery',
            dest: 'rel/jquery.js'
        });
    });

    其中入口文件为“src/jquery.js”,并将以 umd 模式输出到 rel 文件夹下。

    别忘了确保已安装了三大套:

    npm i babel-preset-es2015-rollup rollup rollup-plugin-babel

    后续咱们直接执行:

    node rollup.config.js

    即可实现打包。

    2. 模块拆分

    从模块功能性入手,我们暂时先简单地把上次的整个 IIFE 代码段拆分为:

    src/jquery.js  //出口模块
    src/core.js  //jQuery核心模块
    src/global.js  //全局变量处理模块
    src/init.js  //初始化模块

    它们的内容分别如下:

    jquery.js:

    import jQuery from './core';
    import global from './global';
    import init from './init';
    
    global(jQuery);
    init(jQuery);
    
    export default jQuery;

    core.js:

    var version = "0.0.1",
          jQuery = function (selector, context) {
    
              return new jQuery.fn.init(selector, context);
          };
    
    
    
    jQuery.fn = jQuery.prototype = {
        jquery: version,
        constructor: jQuery,
        setBackground: function(){
            this[0].style.background = 'yellow';
            return this
        },
        setColor: function(){
            this[0].style.color = 'blue';
            return this
        }
    };
    
    
    export default jQuery;

    init.js:

    var init = function(jQuery){
        jQuery.fn.init = function (selector, context, root) {
            if (!selector) {
                return this;
            } else {
                var elem = document.querySelector(selector);
                if (elem) {
                    this[0] = elem;
                    this.length = 1;
                }
                return this;
            }
        };
    
        jQuery.fn.init.prototype = jQuery.fn;
    };
    
    
    
    export default init;

    global.js:

    var global = function(jQuery){
        //走模块化形式的直接绕过
        if(typeof module === 'object' && typeof module.exports !== 'undefined') return;
    
        var _jQuery = window.jQuery,
            _$ = window.$;
    
        jQuery.noConflict = function( deep ) {
            //确保window.$没有再次被改写
            if ( window.$ === jQuery ) {
                window.$ = _$;
            }
    
            //确保window.jQuery没有再次被改写
            if ( deep && window.jQuery === jQuery ) {
                window.jQuery = _jQuery;
            }
    
            return jQuery;  //返回 jQuery 接口引用
        };
    
        window.jQuery = window.$ = jQuery;
    };
    
    export default global;

    留意在 global.js 中我们先加了一层判断,如果使用者走的模块化形式,那是无须考虑全局变量冲突处理的,直接绕过该模块即可。

    执行打包后效果如下(rel/jquery.js)

    (function (global, factory) {
      typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
      typeof define === 'function' && define.amd ? define(factory) :
      (global.jQuery = factory());
    }(this, function () { 'use strict';
    
      /**
       * Created by vajoy on 2016/8/1.
       */
    
      var version = "0.0.1";
      var jQuery = function jQuery(selector, context) {
    
          return new jQuery.fn.init(selector, context);
      };
      jQuery.fn = jQuery.prototype = {
          jquery: version,
          constructor: jQuery,
          setBackground: function setBackground() {
              this[0].style.background = 'yellow';
              return this;
          },
          setColor: function setColor() {
              this[0].style.color = 'blue';
              return this;
          }
      };
    
      var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
        return typeof obj;
      } : function (obj) {
        return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj;
      };
    
      /**
       * Created by vajoy on 2016/8/2.
       */
      var global$1 = function global(jQuery) {
          //走模块化形式的直接绕过
          if ((typeof exports === 'undefined' ? 'undefined' : _typeof(exports)) === 'object' && typeof module !== 'undefined') return;
    
          var _jQuery = window.jQuery,
              _$ = window.$;
    
          jQuery.noConflict = function (deep) {
              //确保window.$没有再次被改写
              if (window.$ === jQuery) {
                  window.$ = _$;
              }
    
              //确保window.jQuery没有再次被改写
              if (deep && window.jQuery === jQuery) {
                  window.jQuery = _jQuery;
              }
    
              return jQuery; //返回 jQuery 接口引用
          };
    
          window.jQuery = window.$ = jQuery;
      };
    
      /**
       * Created by vajoy on 2016/8/1.
       */
    
      var init = function init(jQuery) {
          jQuery.fn.init = function (selector, context, root) {
              if (!selector) {
                  return this;
              } else {
                  var elem = document.querySelector(selector);
                  if (elem) {
                      this[0] = elem;
                      this.length = 1;
                  }
                  return this;
              }
          };
    
          jQuery.fn.init.prototype = jQuery.fn;
      };
    
      global$1(jQuery);
      init(jQuery);
    
      return jQuery;
    
    }));
    View Code

    3. extend 完善

    如上章所说,我们可以通过 $.extend / $.fn.extend 接口来扩展 JQ 的静态方法/实例方法,也可以简单地实现对象的合并和深/浅拷贝。这是非常重要且实用的功能,在这里我们得完善它。

    core.js 中我们新增如下代码段:

    jQuery.extend = jQuery.fn.extend = function() {
        var options, 
            target = arguments[ 0 ] || {},  //target为要被合并的目标对象
            i = 1,
            length = arguments.length,
            deep = false; //默认为浅拷贝
    
        // 若第一个参数为Boolean,表示其为决定是否要深拷贝的参数
        if ( typeof target === "boolean" ) {
            deep = target;
    
            // 那么 target 参数就得往后挪一位了
            target = arguments[ i ] || {};
            i++;
        }
    
        // 若 target 类型不是对象的处理
        if ( typeof target !== "object" && typeof target !== "function" ) {
            target = {};
        }
    
        // 若 target 后没有其它参数(要被拷贝的对象)了,则直接扩展jQuery自身(把target合并入jQuery)
        if ( i === length ) {
            target = this;
            i--;  //减1是为了方便取原target(它反过来变成被拷贝的源对象了)
        }
    
        for ( ; i < length; i++ ) {
    
            // 只处理源对象值不为 null/undefined 的情况
            if ( ( options = arguments[ i ] ) != null ) {
    
                // TODO - 完善Extend
            }
        }
    
        // 返回修改后的目标对象
        return target;
    };

    该段代码可以判断如下写法并做对应处理:

    $.extend( targetObj, copyObj1[, copyObj2...] )
    $.extend( true, targetObj, copyObj1[, copyObj2...]  )
    $.extend( copyObj )
    $.extend( true, copyObj )

    其它情况会被绕过(返回空对象)

    我们继续完善内部的遍历:

        var isObject = function(obj){
            return Object.prototype.toString.call(obj) === "[object Object]"
        };
        var isArray = function(obj){
            return Object.prototype.toString.call(obj) === "[object Array]"
        };
    
        for ( ; i < length; i++ ) { //遍历被拷贝的源对象
    
            // 只处理源对象值不为 null/undefined 的情况
            if ( ( options = arguments[ i ] ) != null ) {
    
                var name, clone, copy;
                // 遍历源对象属性
                for ( name in options ) {
                    src = target[ name ];
                    copy = options[ name ];
    
                    // 避免自己合自己,导致无限循环
                    if ( target === copy ) {
                        continue;
                    }
    
                    // 深拷贝,且确保被拷贝属性值为对象/数组
                    if ( deep && copy && ( isObject( copy ) ||
                        ( copyIsArray = isArray( copy ) ) ) ) {
    
                        //被拷贝属性值为数组
                        if ( copyIsArray ) {
                            copyIsArray = false;
                            //若被合并属性不是数组,则设为[]
                            clone = src && isArray( src ) ? src : [];
    
                        } else {  //被拷贝属性值为对象
                            //若被合并属性不是数组,则设为{}
                            clone = src && isObject( src ) ? src : {};
                        }
    
                        // 右侧递归直到最内层属性值非对象,再把返回值赋给 target 对应属性
                        target[ name ] = jQuery.extend( deep, clone, copy );
    
                        // 非对象/数组,或者浅拷贝情况(注意排除 undefined 类型)
                    } else if ( copy !== undefined ) {
                        target[ name ] = copy;
                    }
                }
            }
        }
    
        // 返回被修改后的目标对象
        return target;

    这里需要留意的有,我们会通过 

    jQuery.extend( deep, clone, copy )

    来递归生成被合并的 target 属性值,这是为了避免扩展后的 target 属性和被扩展的 copyObj 属性引用了同一个对象,导致互相影响。

    通过 extend 递归解剖 copyObj 源对象的属性直到最内层,最内层属性的值(上方代码里的 copy)大致有这么两种情况:

    1. copy 为空对象/空数组:

        for ( ; i < length; i++ ) { //遍历被拷贝对象
    
            // 只处理源对象值不为 null/undefined 的情况
            if ( ( options = arguments[ i ] ) != null ) {
    
                //空数组/空对象没有可枚举的元素/属性,这里会忽略
            }
        }
    
        // 返回被修改后的目标对象
        return target;    //直接返回空数组/空对象

    2. copy 为非对象(如“vajoy”):

                    if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
                        ( copyIsArray = jQuery.isArray( copy ) ) ) ) {
                                    //不会执行这里
    
                    
                    } else if ( copy !== undefined ) {// 执行这里
                        target[ name ] = copy;
                    }
                }
            }
        }
    
        // 返回如 ['vajoy'] 或者 {'name' : 'vajoy'}
        return target;

    从而确保 target 所扩展的每一层属性都跟 copyObj 的是互不关联的。

    P.S. jQuery 里的深拷贝实现其实比较简单,如果希望能做到更全面的兼容,可以参考 lodash 中的实现。

    4. 建立基础工具模块

    在上方的 extend 代码块中其实存在两个不合理的地方:

    1. 仅通过 Object.toString.call(obj) === "[object Object]" 作为对象判断条件在我们扩展对象的逻辑中有些片面,适合扩展的对象应当是“纯粹/简单”(plain)的 js Object 对象,但在某些浏览器中,像 document 在 Object.toSting 调用时也会返回和 Object 相同结果;
    2. 像 Object.hasOwnProperty 和 Object.prototype.toString.call 等方法在我们后续开发中会经常使用上,如果能把它们写到一个模块中封装起来复用就更好了。

    关于 plainObject 的概念可以点这里了解。

    基于上述两点,我们新增一个 var.js 来封装这些常用的输出:

    export var class2type = {};  //在core.js中会被赋予各类型属性值
    
    export const toString = class2type.toString; //等同于 Object.prototype.toString
    
    export const getProto = Object.getPrototypeOf;
    
    export const hasOwn = class2type.hasOwnProperty;
    
    export const fnToString = hasOwn.toString; //等同于 Object.toString/Function.toString
    
    export const ObjectFunctionString = fnToString.call( Object ); //顶层Object构造函数字符串"function Object() { [native code] }",用于判断 plainObj

    然后在 core.js 导入所需接口即可:

    import { class2type, toString, getProto, hasOwn, fnToString, ObjectFunctionString } from './var.js';

    我们进一步修改 extend 接口代码为:

    jQuery.extend = jQuery.fn.extend = function() {
        var options, name, src, copy, copyIsArray, clone,
            target = arguments[ 0 ] || {},
            i = 1,
            length = arguments.length,
            deep = false;
    
        if ( typeof target === "boolean" ) {
            deep = target;
    
            target = arguments[ i ] || {};
            i++;
        }
    
        if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {  //修改点1
            target = {};
        }
    
        if ( i === length ) {
            target = this;
            i--;
        }
    
        for ( ; i < length; i++ ) {
    
            if ( ( options = arguments[ i ] ) != null ) {
    
                for ( name in options ) {
                    src = target[ name ];
                    copy = options[ name ];
    
                    if ( target === copy ) {
                        continue;
                    }
    
                    // Recurse if we're merging plain objects or arrays
                    if ( deep && copy && ( jQuery.isPlainObject( copy ) ||  //修改点2
                        ( copyIsArray = jQuery.isArray( copy ) ) ) ) {
    
                        if ( copyIsArray ) {
                            copyIsArray = false;
                            clone = src && jQuery.isArray( src ) ? src : [];  //修改点3
    
                        } else {
                            clone = src && jQuery.isPlainObject( src ) ? src : {};
                        }
    
                        target[ name ] = jQuery.extend( deep, clone, copy );
    
                    } else if ( copy !== undefined ) {
                        target[ name ] = copy;
                    }
                }
            }
        }
    
        return target;
    };
    
    //新增修改点1,class2type注入各JS类型键值对,配合 jQuery.type 使用,后面会用上
    "Boolean Number String Function Array Date RegExp Object Error Symbol".split(" ").forEach(function(name){
        class2type[ "[object " + name + "]" ] = name.toLowerCase();
    });
    
    //新增修改点2
    jQuery.extend( {
        isArray: Array.isArray,
        isPlainObject: function( obj ) {
            var proto, Ctor;
    
            // 明显的非对象判断,直接返回false
            if ( !obj || toString.call( obj ) !== "[object Object]" ) {
                return false;
            }
    
            proto = getProto( obj );  //获取 prototype
    
            // 通过 Object.create( null ) 形式创建的 {} 是没有prototype的
            if ( !proto ) {
                return true;
            }
    
            // 简单对象的构造函数等于最顶层 Object 构造函数
            Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
            return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
        },
        isFunction: function( obj ) {
            return jQuery.type( obj ) === "function";
        },
        //获取类型(如'function')
        type: function( obj ) {  
            if ( obj == null ) {
                return obj + ""; //'undefined' 或 'null'
            }
    
            return typeof obj === "object" || typeof obj === "function" ?
            class2type[ toString.call( obj ) ] || "object" :
                typeof obj;
        }
    
    });

    这里我们新增了isArray、isPlainObject、isFunction、type 四个 jQuery 静态方法,其中 isPlainObject 比较有趣,为了过滤某些浏览器中的 document 等特殊类型,会对 obj.prototype 及其构造函数进行判断:

    1. 通过Object.create( null ) 形式创建的 {} ,或者实例对象都是没有 prototype 的,直接返回 true2. 判断其构造函数合法性(存在且等于原生的对象构造器 function Object(){ [native code] })

    关于第二点,实际是直接判断两个构造器字符串化后是否相同:

    Function.toString.call(constructor) === Function.toString.call(Object)

    另外,需要留意的是,通过这段代码:

    //新增修改点1,class2type注入各JS类型键值对,配合 jQuery.type 使用,后面会用上
    "Boolean Number String Function Array Date RegExp Object Error Symbol".split(" ").forEach(function(name){
        class2type[ "[object " + name + "]" ] = name.toLowerCase();
    });

    class2type 对象是变成了这样的:

    {
    "[object Boolean]":"boolean",
    "[object Number]":"number",
    "[object String]":"string",
    "[object Function]":"function",
    "[object Array]":"array",
    "[object Date]":"date",
    "[object RegExp]":"regexp",
    "[object Object]":"object",
    "[object Error]":"error",
    "[object Symbol]":"symbol"
    }

    所以后续只需要通过 

    class2type[ Object.prototype.toString(obj) ]

    就能获取 obj 的类型名称。isFunction 接口便是利用这种钩子模式判断传入参数是否函数类型的:

        isFunction: function( obj ) {
            return jQuery.type( obj ) === "function";
        }

    最后。我们执行打包处理:

    node rollup.config.js

    在 HTML 页面运行下述代码:

        var $div = $('div');
        $div.setBackground().setColor();
    
        var arr = [1, 2, 3];
        console.log($.type(arr))

    效果如下:

    留意 $.type 静态方法是我们上方通过 jQuery.extend 扩展进去的:

    //新增修改点1,class2type注入各JS类型键值对,配合 jQuery.type 使用
    "Boolean Number String Function Array Date RegExp Object Error Symbol".split(" ").forEach(function(name){
        class2type[ "[object " + name + "]" ] = name.toLowerCase();
    });
    
    jQuery.extend( {
        type: function( obj ) {  
            if ( obj == null ) {
                return obj + ""; //'undefined' 或 'null'
            }
    
            return typeof obj === "object" || typeof obj === "function" ?
            //兼容安卓2.3- 函数表达式类型不正确情况
            class2type[ toString.call( obj ) ] || "object" :
                typeof obj;
        }
    
    });

    它返回传入参数的类型(小写)。该方法在我们下一章也会直接在模块中使用到。

    本章先这样吧,得感谢这台风天赏赐了一天的假期,才有了时间写文章,共勉~

  • 相关阅读:
    如何透过上层div点击下层的元素解决方法
    学习javscript函数笔记(二)
    js栈内存和堆内存的区别
    学习javscript对象笔记(一)
    小程序 给最外层view设置百分之百高度不起作用
    小程序 开发阶段请求网络报 不在以下 request 合法域名列表中
    git安装以及webstorm配置git
    failed to push some refs to 'git@github.com:xxx/xxx.git' 解决方法
    Jquery EasyUI Treegrid按需加载子集
    js时间戳与日期格式的相互转换
  • 原文地址:https://www.cnblogs.com/vajoy/p/5728755.html
Copyright © 2020-2023  润新知