從零開始,DIY一個jQuery(2)

在上篇文章咱們簡單實現了一個 jQuery 的基礎結構,不過爲了順應潮流,此次咱把它改成模塊化的寫法,此舉得以有效提高項目的可維護性,所以在後續也將以模塊化形式進行持續開發。html

模塊化開發和編譯須要用上 ES6 和 rollup,具體緣由和使用方法請參照我以前的《冗餘代碼都走開——前端模塊打包利器 Rollup.js 入門》一文。前端

本期代碼均掛在個人github上,有須要的童鞋自行下載。node

1. 基本配置jquery

爲了讓 rollup 得以靜態解析模塊,從而減小可能存在的冗餘代碼,咱們得用上 ES6 的解構賦值語法,所以得配合 babel 輔助開發。git

在目錄下咱們新建一個 babel 配置「.babelrc」:github

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

以及 rollup 配置「rollup.comfig.js」:npm

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 文件夾下。api

別忘了確保已安裝了三大套:數組

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;
    }

});

它返回傳入參數的類型(小寫)。該方法在咱們下一章也會直接在模塊中使用到。

本章先這樣吧,得感謝這颱風天賞賜了一天的假期,纔有了時間寫文章,共勉~

相關文章
相關標籤/搜索