jQuery源碼解析

jQuery 是一個很是優秀且經典的庫。怎麼形容它的優秀呢?即便近兩年流行了如 Vue 、 React 等衆多熱門的庫,但對於封裝方法、思想而言,這些庫都未曾超越jQuery。所以,對於前端工程師而言,閱讀 jQuery 源碼是一條提高自個人必經之路。那麼接下來,就讓咱們一塊兒走進 jQuery 內幕的世界。javascript

1、jQuery源碼目錄解析

1)目錄結構解析

首先,咱們從 jQuery 源碼的 github 上下載並使用 vscode 打開 jQuery 源碼。
圖片描述html

打開 jQuery 目錄,能夠很明顯的看見 package.json 和 gruntfile.js 兩個文件,熟悉 grunt 的小夥伴,看見 gruntfile.js 就很清楚,該目錄代碼使用的是 grunt 做爲其構建工具。前端

  • 咱們爲何要使用構建工具呢?
  • 一句話:自動化。對於須要反覆重複的任務,例如壓縮、編譯、單元測試、linting等,自動化工具能夠減輕你的勞動,簡化你的工做。
  • 爲何要使用 Grunt 呢?
  • Grunt 生態系統很是龐大,而且一直在增加。因爲擁有數量龐大的插件可供選擇,所以,你能夠利用 Grunt 自動完成任何事,而且花費最少的代價。

打開src文件夾,文件夾裏面就是 jQuery 的源碼目錄,咱們能夠從目錄清晰的看見jQuery的各個模塊:
圖片描述java

接下來,咱們打開src文件夾中的jquery.js,便可看到 jQuery 的代碼加載:
圖片描述
從圖片中,咱們能夠看見,採用的是AMD方式定義。咱們甚至能夠直接從該文件看出 jQuery 有哪些功能,可供咱們使用。jquery

2、jQuery經典細節解析

1)經典細節1——當即執行函數

首先,咱們能夠從jquery官網,使用grunt編譯一下 jQuery 源碼或下載編譯事後、未壓縮版本的 jQuery 。若使用grunt編譯,咱們能夠從dist/jquery.js中,看到以下代碼:
圖片描述git

(function(global, factory){
    ...
})(typeof window !== "undefined" ? window : this, function( window, noGlobal(){...});

咱們對其,進行一番簡化:github

(function(global,factory){
    ...
})(window,funciton(){});

這樣,就很是一目瞭然了,這是經典的當即執行函數(IIFE):json

(function(){ ... })()

Q:採用當即執行函數,這樣作,有什麼好處呢?
A:經過定義一個匿名函數,建立了一個新的函數做用域,至關於建立了一個「私有」的命名空間,該命名空間的變量和方法,不會破壞污染全局的命名空間。此時如果想訪問全局對象,將全局對象以參數形式傳進去便可。此外,新做用域內對象想訪問傳入的全局對象時,就不須要一步一步的往上找,可提升效率。segmentfault

2)經典細節2——init()

咱們看以下一段代碼:前端工程師

var s = new $('.test');
var p = $('.test');
console.log(s);
console.log(p);

咱們引入一下jQuery,並處理一下這段代碼,能夠看到效果以下:
圖片描述
使人驚訝的是,new出來的和直接調用的,竟然是如出一轍的。
這是爲何呢?
這就涉及到了jQuery的經典的init操做:

咱們打開jQuery目錄下的src/core.js文件,咱們能夠看見一段很是經典的代碼:
圖片描述

從上面這張圖,咱們能夠了解到:

  • 第一個紅框:調用 jQuery ,返回的是new jQuery.fn.init(selector,context);而init方法被掛在到了jQuery.fn上的。
  • 第二個紅框:jQuery.fn = jQuery.prototype = {...};

[注]咱們也能夠從src/core/init.js中,看init是如何具體實現初始化的。

爲了方便講解,咱們對其進行一些簡化:

//1
jQuery = function( selector, context ) {
    return new jQuery.fn.init( selector, context );
}
//2
jQuery.fn = jQuery.prototype = {
    init:function( selector, context ){
        ...
    }
}
//3
init = jQuery.fn.init = function( selector, context, root ){
    ...
}
init.prototype = jQuery.fn;
  • 步驟1:咱們從代碼塊2開始看,jQuery.prototype = jQuery.fn,且都掛載了init()函數。
  • 步驟2:再看代碼塊3,jQuery.fn.init.prototype = jQuery.fn,而咱們從步驟1中,瞭解到jQuery.prototype = jQuery.fn。所以,jQuery.fn.init.prototype = jQuery.fn = jQuery.prototype。
  • 步驟3:最後,再回過來看代碼塊1,function返回的是new jQuery.fn.init(..)。咱們再看步驟2,jQuery.fn.init.prototype = jQuery.prototype。那麼,new jQuery.fn.init(..)就至關於function返回了一個new jQuery()。

饒了一大圈,就至關於 jQuery = new JQuery();

Q:那麼,爲啥要繞那麼遠呢?
A:爲了獲得jQuery原型鏈上的方法。

[特別標註]若是你看了五遍,依舊看不懂這個過程,亦或對Q/A沒有看懂,你可能對js建立對象中的構造函數模式、原型模式、組合模式等的理解還不夠深入,你能夠戳我這篇博文學習一下,亦可翻閱《javascript 高級程序設計》第六章-面向對象的程序設計中的建立對象部份內容。

3)經典細節3————鏈式調用

接下來,咱們看一段官方給的jQuery鏈式對象的示例:

//html
<div class="grandparent">
    <div class="parent">
        <div class="child">
           <div class="subchild"></div>
        </div>
    </div>
    <div class="surrogateParent1"></div>
    <div class="surrogateParent2"></div>
</div>
//js
//return [div.surrogateParent1]
$("div,parent").nextAll().first();

//return [div.surrogateParent2]
$("div.parent").nextAll().last();

$("div,parent").nextAll().first()這是咱們使用jQuery時,常用的調用方法,鏈式調用。那它是如何作到的呢?咱們先看一眼這個代碼:

var test = {
    a:function(){
        console.log('a');
    },
    b:function(){
        console.log('b');
    },
    c:function(){
        console.log('c');
    }
}
test.a().b().c();

結果如何呢?
圖片描述
答案很明顯,b()和c()是沒法訪問的。jQuery是如何實現它的呢?很簡單,返回它自己便可。如:

var test = {
    a:function(){
        console.log('a');
        return this;
    },
    b:function(){
        console.log('b');
        return this;
    },
    c:function(){
        console.log('c');
    }
}
test.a().b().c();
//a 
//b 
//c
4)經典細節4————閉包下的重載
$('.test','td')
$(['.test','#id'])
$(function(){...})

$()就是一個函數,參數不一樣,就涉及到了函數的重載。參數個數不等,用傳統js實現起來很是困難。那麼jQuery到底是如何實現的呢?

咱們經過兩段代碼,領悟它的實現方式:
(1)首先咱們看一個普通的例子:

function addMethod( object, name, func ) {
    var old = object[name];
    object[name] = function(){
        if(func.length === arguments.length){
            return func.apply(this,arguments);
        }else{
            return old.apply(this,arguments);
        }
    }
}
var people = {
    name:["a","b","c"]
}
var find0 = function(){
    return this.name;
}
addMethod(people,'find',find0);
console.log(people.find());//["a", "b", "c"]

調用people.find,將find()方法加到了people中,調用people下的find()方法後,返回的是people.name,即:["a", "b", "c"]。

(2)咱們加上一些代碼,造成重載,再來看看這個例子:
添加一個addMethod(people,'find',find1):

function addMethod( object, name, func ) {
    var old = object[name];
    object[name] = function(){
        if(func.length === arguments.length){
            return func.apply(this,arguments);
        }else{
            return old.apply(this,arguments);
        }
    }
}
var people = {
    name:["a","b","c"]
}
var find0 = function(){
    return this.name;
}
//新增
var find1 = function(name){
    var arr = this.name;
    for(var i = 0;i <= arr.length;i++ ){
        if(arr[i]=name){
            return arr[i];
        }
    }
}
addMethod(people,'find',find0);
//新增
addMethod(people,'find',find1);
console.log(people.find());//["a", "b", "c"]
console.log(people.find("a"));//a

在第一次執行addMethod方法是,這個過程是:

一、object -> people,name -> find,func -> find0;
二、old -> people[find],爲undefined
三、people[find],關聯的是find0

在第二次執行addMethod方法是,這個過程是:

一、object -> people,name -> find,func -> find1;
二、old 爲 object[name],即上一次執行object[name]=function(){..}時的函數,這個函數關聯的是find0。
三、people[find],關聯的是find1

兩次調用後,此時,若調用people.find("a")的話,過程以下:

一、兩次addMethod()後,形式參數爲1個參數,調用people.find("a"),實際參數爲1個參數
二、形參長度與實參長度相等,調用return func.apply(this,arguments),即find1
三、運行find1,打印出「a」

你看到這,是否也和博主同樣,以爲這是無所必要的呢?接下來,就是令你興奮的時刻:
若調用people.find()的話,這個過程會以下:

一、兩次addMethod()後,形式參數爲1個參數,調用people.find(),實際參數爲0個參數
二、形參長度與實參長度不相等,先調用return old.apply(this,arguments),咱們在第二次調用addMethod中闡述了,它關聯的是find0,於是,此時的程序,會再次調用第一次addMethod中無參數的function(){...},即find0
三、此時的形式參數爲0個,實際參數爲0個
三、運行find0,打印出["a", "b", "c"]
相關文章
相關標籤/搜索