jQuery原始碼分析系列 : 整體架構

BothEyes1993發表於2018-12-10

query這麼多年了分析都寫爛了,老早以前就拜讀過, 不過這幾年都是做移動端,一直御用zepto, 最近抽出點時間把jquery又給掃一遍 我也不會照本宣科的翻譯原始碼,結合自己的實際經驗一起拜讀吧! github上最新是jquery-master,加入了AMD規範了,我就以官方最新2.0.3為準

整體架構

jQuery框架的核心就是從HTML文件中匹配元素並對其執行操作、 例如:

$().find().css()
$().hide().html('....').hide().
複製程式碼

從上面的寫法上至少可以發現2個問題

  1. jQuery物件的構建方式
  2. jQuery方法的呼叫方式jQuery方法的呼叫方式

分析一:jQuery的無new構建

JavaScript是函式式語言,函式可以實現類,類就是物件導向程式設計中最基本的概念

var aQuery = function(selector, context) {
        //建構函式
}
aQuery.prototype = {
    //原型
    name:function(){},
    age:function(){}
}

var a = new aQuery();

a.name();
複製程式碼

這是常規的使用方法,顯而易見jQuery不是這樣玩的 jQuery沒有使用new執行符將jQuery顯示的例項化,還是直接呼叫其函式 按照jQuery的抒寫方式

$().ready() 
$().noConflict()
複製程式碼

要實現這樣,那麼jQuery就要看成一個類,那麼$()應該是返回類的例項才對 所以把程式碼改一下:

var aQuery = function(selector, context) {
       returnnew aQuery();
}
aQuery.prototype = {
    name:function(){},
    age:function(){}
}
複製程式碼

通過new aQuery(),雖然返回的是一個例項,但是也能看出很明顯的問題,死迴圈了!


那麼如何返回一個正確的例項?

在javascript中例項this只跟原型有關係 那麼可以把jQuery類當作一個工廠方法來建立例項,把這個方法放到jQuery.prototye原型中

var aQuery = function(selector, context) {
       return  aQuery.prototype.init();
}
aQuery.prototype = {
    init:function(){
        returnthis;
    }
    name:function(){},
    age:function(){}
}
複製程式碼

當執行aQuery() 返回的例項:

在這裡插入圖片描述
很明顯aQuery()返回的是aQuery類的例項,那麼在init中的this其實也是指向的aQuery類的例項 問題來了init的this指向的是aQuery類,如果把init函式也當作一個構造器,那麼內部的this要如何處理?

var aQuery = function(selector, context) {
       return  aQuery.prototype.init();
}
aQuery.prototype = {
    init: function() {
        this.age = 18
        returnthis;
    },
    name: function() {},
    age: 20
}

aQuery().age  //18
複製程式碼

這樣的情況下就出錯了,因為this只是指向aQuery類的,所以需要設計出獨立的作用域才行


jQuery框架分隔作用域的處理

jQuery = function( selector, context ) {
        // The jQuery object is actually just the init constructor 'enhanced'returnnew jQuery.fn.init( selector, context, rootjQuery );
    },
複製程式碼

很明顯通過例項init函式,每次都構建新的init例項物件,來分隔this,避免互動混淆 那麼既然都不是同一個物件那麼肯定又出現一個新的問題 例如:

var aQuery = function(selector, context) {
       returnnew aQuery.prototype.init();
}
aQuery.prototype = {
    init: function() {
        this.age = 18
        returnthis;
    },
    name: function() {},
    age: 20
}

//Uncaught TypeError: Object [object Object] has no method 'name' 
console.log(aQuery().name())
複製程式碼

丟擲錯誤,無法找到這個方法,所以很明顯new的init跟jquery類的this分離了


怎麼訪問jQuery類原型上的屬性與方法?

做到既能隔離作用域還能使用jQuery原型物件的作用域呢,還能在返回例項中訪問jQuery的原型物件? 實現的關鍵點

// Give the init function the jQuery prototype for later instantiation
jQuery.fn.init.prototype = jQuery.fn;
複製程式碼

通過原型傳遞解決問題,把jQuery的原型傳遞給jQuery.prototype.init.prototype 換句話說jQuery的原型物件覆蓋了init構造器的原型物件 因為是引用傳遞所以不需要擔心這個迴圈引用的效能問題

var aQuery = function(selector, context) {
       returnnew aQuery.prototype.init();
}
aQuery.prototype = {
    init: function() {
        returnthis;
    },
    name: function() {
        returnthis.age
    },
    age: 20
}

aQuery.prototype.init.prototype = aQuery.prototype;

console.log(aQuery().name()) //20
複製程式碼

百度借網友的一張圖,方便直接理解: fn解釋下,其實這個fn沒有什麼特殊意思,只是jQuery.prototype的引用

在這裡插入圖片描述


分析二:鏈式呼叫

DOM鏈式呼叫的處理: 1.節約JS程式碼. 2.所返回的都是同一個物件,可以提高程式碼的效率

通過簡單擴充套件原型方法並通過return this的形式來實現跨瀏覽器的鏈式呼叫。 利用JS下的簡單工廠模式,來將所有對於同一個DOM物件的操作指定同一個例項。 這個原理就超簡單了

aQuery().init().name()

分解
a = aQuery();
a.init()
a.name()
複製程式碼

把程式碼分解一下,很明顯實現鏈式的基本條件就是例項this的存在,並且是同一個

aQuery.prototype = {
    init: function() {
        returnthis;
    },
    name: function() {
        returnthis
    }
}
複製程式碼

所以我們在需要鏈式的方法訪問this就可以了,因為返回當前例項的this,從而又可以訪問自己的原型了

aQuery.init().name()
複製程式碼

優點:節省程式碼量,提高程式碼的效率,程式碼看起來更優雅 最糟糕的是所有物件的方法返回的都是物件本身,也就是說沒有返回值,這不一定在任何環境下都適合。

Javascript是無阻塞語言,所以他不是沒阻塞,而是不能阻塞,所以他需要通過事件來驅動,非同步來完成一些本需要阻塞程式的操作,這樣處理只是同步鏈式,非同步鏈式jquery從1.5開始就引入了Promise,jQuery.Deferred後期在討論。


分析三:外掛介面

jQuery的主體框架就是這樣,但是根據一般設計者的習慣,如果要為jQuery或者jQuery prototype新增屬性方法,同樣如果要提供給開發者對方法的擴充套件,從封裝的角度講是不是應該提供一個介面才對,字面就能看懂是對函式擴充套件,而不是看上去直接修改prototype.友好的使用者介面,

jQuery支援自己擴充套件屬性,這個對外提供了一個介面,jQuery.fn.extend()來對物件增加方法 從jQuery的原始碼中可以看到,jQuery.extend和jQuery.fn.extend其實是同指向同一方法的不同引用

jQuery.extend = jQuery.fn.extend = function() { jQuery.extend 對jQuery本身的屬性和方法進行了擴充套件

jQuery.fn.extend 對jQuery.fn的屬性和方法進行了擴充套件 通過extend()函式可以方便快速的擴充套件功能,不會破壞jQuery的原型結構

jQuery.extend = jQuery.fn.extend = function(){...}; 這個是連等,也就是2個指向同一個函式,怎麼會實現不同的功能呢?這就是this 力量了!

針對fn與jQuery其實是2個不同的物件,在之前有講述: ● jQuery.extend 呼叫的時候,this是指向jQuery物件的(jQuery是函式,也是物件!),所以這裡擴充套件在jQuery上。 ● 而jQuery.fn.extend 呼叫的時候,this指向fn物件,jQuery.fn 和jQuery.prototype指向同一物件,擴充套件fn就是擴充套件jQuery.prototype原型物件。 ● 這裡增加的是原型方法,也就是物件方法了。所以jQuery的api中提供了以上2中擴充套件函式。 extend的實現

jQuery.extend = jQuery.fn.extend = function() {
    var src, copyIsArray, copy, name, options, clone,
        target = arguments[0] || {},    // 常見用法 jQuery.extend( obj1, obj2 ),此時,target為arguments[0]
        i = 1,
        length = arguments.length,
        deep = false;

    // Handle a deep copy situationif ( typeof target === "boolean" ) {    // 如果第一個引數為true,即 jQuery.extend( true, obj1, obj2 ); 的情況
        deep = target;  // 此時target是true
        target = arguments[1] || {};    // target改為 obj1// skip the boolean and the target
        i = 2;
    }

    // Handle case when target is a string or something (possible in deep copy)if ( typeof target !== "object" && !jQuery.isFunction(target) ) {  // 處理奇怪的情況,比如 jQuery.extend( 'hello' , {nick: 'casper})~~
        target = {};
    }

    // extend jQuery itself if only one argument is passedif ( length === i ) {   // 處理這種情況 jQuery.extend(obj),或 jQuery.fn.extend( obj )
        target = this;  // jQuery.extend時,this指的是jQuery;jQuery.fn.extend時,this指的是jQuery.fn
        --i;
    }

    for ( ; i < length; i++ ) {
        // Only deal with non-null/undefined valuesif ( (options = arguments[ i ]) != null ) { // 比如 jQuery.extend( obj1, obj2, obj3, ojb4 ),options則為 obj2、obj3...// Extend the base objectfor ( name in options ) {
                src = target[ name ];
                copy = options[ name ];

                // Prevent never-ending loopif ( target === copy ) {    // 防止自引用,不贅述continue;
                }

                // Recurse if we're merging plain objects or arrays// 如果是深拷貝,且被拷貝的屬性值本身是個物件if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
                    if ( copyIsArray ) {    // 被拷貝的屬性值是個陣列
                        copyIsArray = false;
                        clone = src && jQuery.isArray(src) ? src : [];

                    } else {    被拷貝的屬性值是個plainObject,比如{ nick: 'casper' }
                        clone = src && jQuery.isPlainObject(src) ? src : {};
                    }

                    // Never move original objects, clone them
                    target[ name ] = jQuery.extend( deep, clone, copy );  // 遞迴~// Don't bring in undefined values
                } elseif ( copy !== undefined ) {  // 淺拷貝,且屬性值不為undefined
                    target[ name ] = copy;
                }
            }
        }
    }

    // Return the modified objectreturn target;
複製程式碼

總結:

● 通過new jQuery.fn.init() 構建一個新的物件,擁有init構造器的prototype原型物件的方法

● 通過改變prorotype指標的指向,讓這個新的物件也指向了jQuery類的原型prototype

● 所以這樣構建出來的物件就繼續了jQuery.fn原型定義的所有方法了

相關文章