即日起在codingBlog上分享您的技术经验即可获得积分,积分可兑换现金哦。

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

微信 前端大全 10℃ 0评论

(点击上方公众号,可快速关注我们)


来源:VaJoy Larn

链接:www.cnblogs.com/vajoy/p/5728755.html


上篇文章我们简单实现了一个 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 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;

};

 

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;

 

}));

3. extend 完善

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

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

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

$.extend( targetObj, copyObj1[, copyObj2…] )

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

$.extend( copyObj )

$.extend( true, copyObj )

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

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

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

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 的是互不关联的。

4. 建立基础工具模块

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

1. 仅通过 Object.toString.call(obj) === “[object Object]” 作为对象判断条件在我们扩展对象的逻辑中有些片面,适合扩展的对象应当是“纯粹/简单”(plain)的 js Object 对象,但在某些浏览器中,像 document 在 Object.toSting 调用时也会返回和 Object 相同结果;

2. 像 Object.hasOwnProperty 和 Object.prototype.toString.call 等方法在我们后续开发中会经常使用上,如果能把它们写到一个模块中封装起来复用就更好了。

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

基于上述两点,我们新增一个 util.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 ‘./util.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;

    },

    //获取类型(如’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 两个 jQuery 静态方法,其中 isPlainObject 比较有趣,为了过滤某些浏览器中的 document 等特殊类型,会对 obj.prototype 及其构造函数进行判断:

1. 通过Object.create( null ) 形式创建的 {} ,或者实例对象都是没有 prototype 的,直接返回 true;

2. 判断其构造函数合法性(存在且等于原生的对象构造器 function Object(){ [native code] })

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

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

 

我们执行打包处理:

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;

    }

 

});

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

【今日微信公号推荐↓】

更多推荐请看值得关注的技术和设计公众号


其中推荐了包括技术设计极客 和 IT相亲相关的热门公众号。技术涵盖:Python、Web前端、Java、安卓、iOS、PHP、C/C++、.NET、Linux、数据库、运维、大数据、算法、IT职场等。点击《值得关注的技术和设计公众号》,发现精彩!


转载请注明:CodingBlog » 从零开始,DIY一个jQuery(2)

喜欢 (0)or分享 (0)
发表我的评论
取消评论

*

表情
(2)个小伙伴在吐槽
  1. 难度一下加大了,让我如何是好
    2016-08-10 13:42 回复
  2. 看的我都蒙逼了/微笑
    简单、2016-08-10 14:52 回复