學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫

雖然如今基本不怎麼使用jQuery了,但jQuery流行10多年JS庫,仍是有必要學習它的源碼的。也能夠學着打造屬於本身的js類庫,求職面試時能夠增色很多。html

本文章學習的是v3.4.1 版本。
unpkg.com源碼地址:https://unpkg.com/jquery@3.4....前端

jQuery github倉庫node

自執行匿名函數

(function(global, factory){

})(typeof window !== "underfined" ? window: this, function(window, noGlobal){

});

外界訪問不到裏面的變量和函數,裏面能夠訪問到外界的變量,但裏面定義了本身的變量,則不會訪問外界的變量。
匿名函數將代碼包裹在裏面,防止與其餘代碼衝突和污染全局環境。
關於自執行函數不是很瞭解的讀者能夠參看這篇文章。
[[譯] JavaScript:當即執行函數表達式(IIFE)](https://segmentfault.com/a/11...jquery

瀏覽器環境下,最後把$jQuery函數掛載到window上,因此在外界就能夠訪問到$jQuery了。linux

if ( !noGlobal ) {
    window.jQuery = window.$ = jQuery;
}
// 其中`noGlobal`參數只有在這裏用到。

支持多種環境下使用 好比 commonjs、amd規範

commonjs 規範支持

commonjs實現 主要表明 nodejsgit

// global是全局變量,factory 是函數
( function( global, factory ) {

    //  使用嚴格模式
    "use strict";
    // Commonjs 或者 CommonJS-like  環境
    if ( typeof module === "object" && typeof module.exports === "object" ) {
        // 若是存在global.document 則返回factory(global, true);
        module.exports = global.document ?
            factory( global, true ) :
            function( w ) {
                if ( !w.document ) {
                    throw new Error( "jQuery requires a window with a document" );
                }
                return factory( w );
            };
    } else {
        factory( global );
    }

// Pass this if window is not defined yet
// 第一個參數判斷window,存在返回window,不存在返回this
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {});

amd 規範 主要表明 requirejs

if ( typeof define === "function" && define.amd ) {
    define( "jquery", [], function() {
        return jQuery;
    } );
}

cmd 規範 主要表明 seajs

很遺憾,jQuery源碼裏沒有暴露對seajs的支持。但網上也有一些方案。這裏就不具體提了。畢竟如今基本不用seajs了。github

無 new 構造

實際上也是能夠 new的,由於jQuery是函數。並且和不用new效果是同樣的。
new顯示返回對象,因此和直接調用jQuery函數做用效果是同樣的。
若是對new操做符具體作了什麼不明白。能夠參看我以前寫的文章。面試

面試官問:可否模擬實現JS的new操做符編程

源碼:ubuntu

var
    version = "3.4.1",

    // Define a local copy of jQuery
    jQuery = function( selector, context ) {
        // 返回new以後的對象
        return new jQuery.fn.init( selector, context );
    };
jQuery.fn = jQuery.prototype = {
    // jQuery當前版本
    jquery: version,
    // 修正構造器爲jQuery
    constructor: jQuery,
    length: 0,
};
init = jQuery.fn.init = function( selector, context, root ) {
    // ...
    if ( !selector ) {
        return this;
    }
    // ...
};
init.prototype = jQuery.fn;
jQuery.fn === jQuery.prototype;     // true
init = jQuery.fn.init;
init.prototype = jQuery.fn;
// 也就是
jQuery.fn.init.prototype === jQuery.fn;     // true
jQuery.fn.init.prototype === jQuery.prototype;     // true

關於這個筆者畫了一張jQuery原型關係圖,所謂一圖勝千言。
jQuery-v3.4.1原型關係圖

<sciprt src="https://unpkg.com/jquery@3.4.1/dist/jquery.js">
</script>
console.log({jQuery});
// 在谷歌瀏覽器控制檯,能夠看到jQuery函數下掛載了不少靜態屬性和方法,在jQuery.fn 上也掛着不少屬性和方法。

Vue源碼中,也跟jQuery相似,執行的是Vue.prototype._init方法。

function Vue (options) {
    if (!(this instanceof Vue)
    ) {
        warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
}
initMixin(Vue);
function initMixin (Vue) {
    Vue.prototype._init = function (options) {};
};

核心函數之一 extend

用法:

jQuery.extend( target [, object1 ] [, objectN ] )        Returns: Object

jQuery.extend( [deep ], target, object1 [, objectN ] )

jQuery.extend API
jQuery.fn.extend API

看幾個例子:
(例子能夠我放到在線編輯代碼的jQuery.extend例子codepen了,能夠直接運行)。

// 1. jQuery.extend( target)
var result1 = $.extend({
    job: '前端開發工程師',
});

console.log(result1, 'result1', result1.job); // $函數 加了一個屬性 job  // 前端開發工程師

// 2. jQuery.extend( target, object1)
var result2 = $.extend({
    name: '若川',
},
{
    job: '前端開發工程師',
});

console.log(result2, 'result2'); // { name: '若川', job: '前端開發工程師' }

// deep 深拷貝
// 3. jQuery.extend( [deep ], target, object1 [, objectN ] )
var result3 = $.extend(true,  {
    name: '若川',
    other: {
        mac: 0,
        ubuntu: 1,
        windows: 1,
    },
}, {
    job: '前端開發工程師',
    other: {
        mac: 1,
        linux: 1,
        windows: 0,
    }
});
console.log(result3, 'result3');
// deep true
// {
//     "name": "若川",
//     "other": {
//         "mac": 1,
//         "ubuntu": 1,
//         "windows": 0,
//         "linux": 1
//     },
//     "job": "前端開發工程師"
// }
// deep false
// {
//     "name": "若川",
//     "other": {
//         "mac": 1,
//         "linux": 1,
//         "windows": 0
//     },
//     "job": "前端開發工程師"
// }

結論:extend函數既能夠實現給jQuery函數能夠實現淺拷貝、也能夠實現深拷貝。能夠給jQuery上添加靜態方法和屬性,也能夠像jQuery.fn(也就是jQuery.prototype)上添加屬性和方法,這個功能歸功於thisjQuery.extend調用時this指向是jQueryjQuery.fn.extend調用時this指向則是jQuery.fn

淺拷貝實現

知道這些,其實實現淺拷貝仍是比較容易的:

// 淺拷貝實現
jQuery.extend = function(){
    // options 是擴展的對象object1,object2...
    var options,
    // object對象上的鍵
    name,
    // copy object對象上的值,也就是是須要拷貝的值
    copy,
    // 擴展目標對象,可能不是對象,因此或空對象
    target = arguments[0] || {},
    // 定義i爲1
    i = 1,
    // 定義實參個數length
    length = arguments.length;
    // 只有一個參數時
    if(i === length){
        target = this;
        i--;
    }
    for(; i < length; i++){
        // 不是underfined 也不是null
        if((options = arguments[i]) !=  null){
            for(name in options){
                copy = options[name];
                // 防止死循環,continue 跳出當前這次循環
                if ( name === "__proto__" || target === copy ) {
                    continue;
                }
                if ( copy !== undefined ) {
                    target[ name ] = copy;
                }
            }
        }

    }
    // 最後返回目標對象
    return target;
}

深拷貝則主要是在如下這段代碼作判斷。多是數組和對象引用類型的值,作判斷。

if ( copy !== undefined ) {
    target[ name ] = copy;
}

爲了方便讀者調試,代碼一樣放在jQuery.extend淺拷貝代碼實現codepen,可在線運行。

深拷貝實現

$.extend = function(){
    // options 是擴展的對象object1,object2...
    var options,
    // object對象上的鍵
    name,
    // copy object對象上的值,也就是是須要拷貝的值
    copy,
    // 深拷貝新增的四個變量 deep、src、copyIsArray、clone
    deep = false,
    // 源目標,須要往上面賦值的
    src,
    // 須要拷貝的值的類型是函數
    copyIsArray,
    //
    clone,
    // 擴展目標對象,可能不是對象,因此或空對象
    target = arguments[0] || {},
    // 定義i爲1
    i = 1,
    // 定義實參個數length
    length = arguments.length;

    // 處理深拷貝狀況
    if ( typeof target === "boolean" ) {
        deep = target;

        // Skip the boolean and the target
        // target目標對象開始後移
        target = arguments[ i ] || {};
        i++;
    }

    // Handle case when target is a string or something (possible in deep copy)
    // target不等於對象,且target不是函數的狀況下,強制將其賦值爲空對象。
    if ( typeof target !== "object" && !isFunction( target ) ) {
        target = {};
    }

    // 只有一個參數時
    if(i === length){
        target = this;
        i--;
    }
    for(; i < length; i++){
        // 不是underfined 也不是null
        if((options = arguments[i]) !=  null){
            for(name in options){
                copy = options[name];
                // 防止死循環,continue 跳出當前這次循環
                if ( name === "__proto__" || target === copy ) {
                    continue;
                }

                // Recurse if we're merging plain objects or arrays
                // 這裏deep爲true,而且須要拷貝的值有值,而且是純粹的對象
                // 或者需拷貝的值是數組
                if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
                    ( copyIsArray = Array.isArray( copy ) ) ) ) {

                    // 源目標,須要往上面賦值的
                    src = target[ name ];

                    // Ensure proper type for the source value
                    // 拷貝的值,而且src不是數組,clone對象改成空數組。
                    if ( copyIsArray && !Array.isArray( src ) ) {
                        clone = [];
                        // 拷貝的值不是數組,對象不是純粹的對象。
                    } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {
                        // clone 賦值爲空對象
                        clone = {};
                    } else {
                        // 不然 clone = src
                        clone = src;
                    }
                    // 把下一次循環時,copyIsArray 須要從新賦值爲false
                    copyIsArray = false;

                    // Never move original objects, clone them
                    // 遞歸調用本身
                    target[ name ] = jQuery.extend( deep, clone, copy );

                // Don't bring in undefined values
                }
                else if ( copy !== undefined ) {
                    target[ name ] = copy;
                }
            }
        }

    }
    // 最後返回目標對象
    return target;
};

爲了方便讀者調試,這段代碼一樣放在jQuery.extend深拷貝代碼實現codepen,可在線運行。

深拷貝衍生的函數 isFunction

判斷參數是不是函數。

var isFunction = function isFunction( obj ) {

    // Support: Chrome <=57, Firefox <=52
    // In some browsers, typeof returns "function" for HTML <object> elements
    // (i.e., `typeof document.createElement( "object" ) === "function"`).
    // We don't want to classify *any* DOM node as a function.
    return typeof obj === "function" && typeof obj.nodeType !== "number";
};

深拷貝衍生的函數 jQuery.isPlainObject

jQuery.isPlainObject(obj)
測試對象是不是純粹的對象(經過 "{}" 或者 "new Object" 建立的)。

jQuery.isPlainObject({}) // true
jQuery.isPlainObject("test") // false
var getProto = Object.getPrototypeOf;
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
var fnToString = hasOwn.toString;
var ObjectFunctionString = fnToString.call( Object );

jQuery.extend( {
    isPlainObject: function( obj ) {
        var proto, Ctor;

        // Detect obvious negatives
        // Use toString instead of jQuery.type to catch host objects
        // !obj 爲true或者 不爲[object Object]
        // 直接返回false
        if ( !obj || toString.call( obj ) !== "[object Object]" ) {
            return false;
        }

        proto = getProto( obj );

        // Objects with no prototype (e.g., `Object.create( null )`) are plain
        // 原型不存在 好比 Object.create(null) 直接返回 true;
        if ( !proto ) {
            return true;
        }

        // Objects with prototype are plain iff they were constructed by a global Object function
        Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
        // 構造器是函數,而且 fnToString.call( Ctor )  === fnToString.call( Object );
        return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
    },
});

extend函數,也能夠本身刪掉寫一寫,算是jQuery中一個比較核心的函數了。並且用途普遍,能夠內部使用也能夠,外部使用擴展 插件等。

鏈式調用

jQuery可以鏈式調用是由於一些函數執行結束後 return this
好比
jQuery 源碼中的addClassremoveClasstoggleClass

jQuery.fn.extend({
    addClass: function(){
        // ...
        return this;
    },
    removeClass: function(){
        // ...
        return this;
    },
    toggleClass: function(){
        // ...
        return this;
    },
});

jQuery.noConflict 不少js庫都會有的防衝突函數

jQuery.noConflict API

用法:

<script>
    var $ = '我是其餘的$,jQuery不要覆蓋我';
</script>
<script src="./jquery-3.4.1.js">
</script>
<script>
    $.noConflict();
    console.log($); // 我是其餘的$,jQuery不要覆蓋我
</script>

jQuery.noConflict 源碼

var

    // Map over jQuery in case of overwrite
    _jQuery = window.jQuery,

    // Map over the $ in case of overwrite
    _$ = window.$;

jQuery.noConflict = function( deep ) {
    // 若是已經存在$ === jQuery;
    // 把已存在的_$賦值給window.$;
    if ( window.$ === jQuery ) {
        window.$ = _$;
    }

    // 若是deep爲 true, 而且已經存在jQuery === jQuery;
    // 把已存在的_jQuery賦值給window.jQuery;
    if ( deep && window.jQuery === jQuery ) {
        window.jQuery = _jQuery;
    }

    // 最後返回jQuery
    return jQuery;
};

總結

全文主要經過淺析了jQuery總體結構,自執行匿名函數、無new構造、支持多種規範(如commonjs、amd規範)、核心函數之extend、鏈式調用、jQuery.noConflict等方面。

從新梳理下文中學習的源碼結構。

// 源碼結構
( function( global, factory )
    "use strict";
    if ( typeof module === "object" && typeof module.exports === "object" ) {
        module.exports = global.document ?
            factory( global, true ) :
            function( w ) {
                if ( !w.document ) {
                    throw new Error( "jQuery requires a window with a document" );
                }
                return factory( w );
            };
    } else {
        factory( global );
    }

} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
    var    version = "3.4.1",

        // Define a local copy of jQuery
        jQuery = function( selector, context ) {
            return new jQuery.fn.init( selector, context );
        };

    jQuery.fn = jQuery.prototype = {
        jquery: version,
        constructor: jQuery,
        length: 0,
        // ...
    };

    jQuery.extend = jQuery.fn.extend = function() {};

    jQuery.extend( {
        // ...
        isPlainObject: function( obj ) {},
        // ...
    });

    init = jQuery.fn.init = function( selector, context, root ) {};

    init.prototype = jQuery.fn;

    if ( typeof define === "function" && define.amd ) {
        define( "jquery", [], function() {
            return jQuery;
        } );
    }
    jQuery.noConflict = function( deep ) {};

    if ( !noGlobal ) {
        window.jQuery = window.$ = jQuery;
    }

    return jQuery;
});

能夠學習到jQuery巧妙的設計和架構,爲本身所用,打造屬於本身的js類庫。
相關代碼和資源放置在github blog中,須要的讀者能夠自取。

下一篇文章是學習underscorejs的源碼總體架構。
學習underscorejs總體架構,打造屬於本身的函數式編程類庫

讀者發現有不妥或可改善之處,歡迎評論指出。另外以爲寫得不錯,能夠點贊、評論、轉發,也是對筆者的一種支持。

筆者往期文章

面試官問:JS的繼承
面試官問:JS的this指向
面試官問:可否模擬實現JS的call和apply方法
面試官問:可否模擬實現JS的bind方法
面試官問:可否模擬實現JS的new操做符
前端使用puppeteer 爬蟲生成《React.js 小書》PDF併合並

擴展閱讀

chokcoco: jQuery- v1.10.2 源碼解讀
chokcoco:【深刻淺出jQuery】源碼淺析--總體架構
songjz :jQuery 源碼系列(一)整體架構

關於

做者:常以若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
我的博客 https://lxchuan12.github.io
github blog,相關源碼和資源都放在這裏,求個star^_^~

微信交流羣

加微信 lxchuan12,備註寫明來源。拉您進微信羣【前端視野交流羣】。

相關文章
相關標籤/搜索