jQuery源碼解讀----part 2

分離構造器

經過new操做符構建一個對象,通常通過四步:javascript

A.建立一個新對象css

B.將構造函數的做用域賦給新對象(因此this就指向了這個新對象)html

C.執行構造函數中的代碼java

D.返回這個新對象jquery

最後一點就說明了,咱們只要返回一個新對象便可。其實new操做符主要是把原型鏈跟實例的this關聯起來,這纔是最關鍵的一點,因此咱們若是須要原型鏈就必需要new操做符來進行處理。不然this則變成window對象了。算法

改造jQuery無new的格式,咱們能夠經過instanceof判斷this是否爲當前實例:設計模式

var $$ = ajQuery = function(selector) {
    if(!(this instanceof ajQuery)){ // 第二次看仍是以爲這一句很NB
        return new ajQuery(selector);
    }
    this.selector = selector;
    return this
}

但在jQuery實際上採起的手段是把原型上的一個init方法做爲構造器,這樣貌似更節省代碼空間?數組

var $$ = ajQuery = function(selector) {
    //把原型上的init做爲構造器
    return new ajQuery.fn.init( selector );
}

ajQuery.fn = ajQuery.prototype = {
    name: 'aaron',
    init: function() {
        console.log(this)
    },
    constructor: ajQuery
}

但這樣子還缺點東西,init是ajQuery原型上做爲構造器的一個方法,那麼其this就不是ajQuery了,因此this就徹底引用不到ajQuery的原型了,因此這裏經過new把init方法與ajQuery給分離成2個獨立的構造器。app


靜態與實例方法共享設計

接着上面分割出2個構造器的疑問,來看看jQuery的一個遍歷接口:框架

$(".aaron").each()   //做爲實例方法存在
$.each()             //做爲靜態方法存在

看似實例和靜態方法須要兩個函數來實現,但在jQuery源碼中是這樣的:

jQuery.prototype = {
    // 調用實例方法其實是將實例對象this做爲一個參數,調用對應的靜態方法,這樣就造成了共享
    each: function( callback, args ) {
        return jQuery.each( this, callback, args );
    }
}

實例方法取於靜態方法,換句話來講這是靜態與實例方法共享設計,靜態方法掛在jQuery構造器上,原型方法掛在哪裏呢?------jQuery經過new原型prototype上的init方法看成構造器,那麼init的原型鏈方法就是實例的方法了,因此jQuery經過2個構造器劃分2種不一樣的調用方式一種是靜態,一種是原型。

那若是要將2個構造器原型關聯起來,關鍵就是靠下面一句:

ajQuery.fn.init.prototype = ajQuery.fn

540905880001daac05540230

這樣init構造出來的實例對象也可以繼承jQuery原型上的方法了。


方法鏈式調用的實現

jQuery的核心理念是Write less,Do more(寫的更少,作的更多),那麼鏈式方法的設計與這個核心理念不謀而合。那麼從深層次考慮這種設計其實就是一種Internal DSL。

DSL是指Domain Specific Language,也就是用於描述和解決特定領域問題的語言。

jQuery的Internal DSL形式帶來的好處——編寫代碼時,讓代碼更貼近做者的思惟模式;閱讀代碼時,讓讀者更容易理解代碼的含義;應用DSL能夠有效的提升系統的可維護性(縮小了實現模型和領域模型的距離,提升了實現的可讀性)和靈活性,而且提供開發的效率。

jQuery的這種管道風格的DSL鏈式代碼,總的來講:

☑ 節約JS代碼;

☑ 所返回的都是同一個對象,能夠提升代碼的效率

實現鏈式操做的原理你們都懂的,就只須要在方法內返回當前的這個實例對象this就能夠了,由於返回當前實例的this,從而又能夠訪問本身的原型了,這樣的就節省代碼量,提升代碼的效率,代碼看起來更優雅。可是這種方法有一個問題是:全部對象的方法返回的都是對象自己,也就是說沒有返回值,因此這種方法不必定在任何環境下都適合。


插件接口的設計

jQuery插件的開發分爲兩種:

☑ 一種是掛在jQuery命名空間下的全局函數,也可稱爲靜態方法;

☑ 另外一種是jQuery對象級別的方法,即掛在jQuery原型下的方法,這樣經過選擇器獲取的jQuery對象實例也能共享該方法。

提供的接口:

$.extend(target, [object1], [objectN]);
$.fn.extend();

接口的使用:

// 拓展到jQuery上的靜態方法
jQuery.extend({
    data:function(){},
    removeData:function(){}
})

// 拓展到實例對象上的原型方法
jQuery.fn.extend({
    data:function(){},
    removeData:function(){}
})

而jQuery源碼中對於上面兩種擴展,實際上是同指向同一方法的不一樣引用(這裏有一個設計的重點,經過調用的上下文,咱們來肯定這個方法是做爲靜態仍是實例處理,在javascript的世界中一共有四種上下文調用方式:方法調用模式、函數調用模式、構造器調用模式、apply調用模式),而這一切都是依靠this來完成的。

☑  jQuery.extend調用的時候上下文指向的是jQuery構造器,this指向的是jQuery

☑  jQuery.fn.extend調用的時候上下文指向的是jQuery構造器的實例對象了,this指向實例對象

所以在源碼中是這樣的:

aAron.extend = aAron.fn.extend = function() {
    var options, src, copy,
        target = arguments[0] || {},
        i = 1,
        length = arguments.length;

    // 只有一個參數,就是對jQuery自身的擴展處理
    if (i === length) {
        target = this; // 調用的上下文對象,前一個方法對應jQuery,後一個方法對應實例
        i--;
    }
    for (; i < length; i++) {
        // 從i開始取參數,不爲空開始遍歷
        if ((options = arguments[i]) != null) {
            for (name in options) {
                copy = options[name];
                // 覆蓋拷貝
                target[name] = copy;
            }
        }
    }
    return target;
}

我來說解一下上面的代碼:由於extend的核心功能就是經過擴展收集功能(相似於mix混入),因此就會存在收集對象(target)與被收集的數據,由於jQuery.extend並無明確實參,並且是經過arguments來判斷的,因此這樣處理起來很靈活。arguments經過判斷傳遞參數的數量能夠實現函數重載。其中最重要的一段target = this,經過調用的方式咱們就能確實當前的this的指向,因此這時候就能肯定target了。最後就很簡單了,經過for循環遍歷把數據附加到這個target上了。固然在這個附加的過程當中咱們還能夠作數據過濾、深拷貝等一系列的操做了。


回溯處理的設計

經過jQuery處理後返回的不只僅只有DOM對象,而是一個包裝容器,返回jQuery對象。而這一個對象中有一個preObject的屬性。

1559366209776

要了解這個屬性是作什麼的,首先了解一下jQuery對象棧,jQuery內部維護着一個jQuery對象棧。每一個遍歷方法(在當前選中範圍內的DOM再進行篩選的操做,例如.find()方法)都會找到一組新元素(一個jQuery對象),而後jQuery會把這組元素推入到棧中。

而每一個jQuery對象都有三個屬性:context、selector和prevObject(用id選擇器的話這個屬性不必定有),其中的prevObject屬性就指向這個對象棧中的前一個對象,而經過這個屬性能夠回溯到最初的DOM元素集中。

能夠看下下面的例子:

$("div").find('.foo').find('.aaa') // 這裏的preObject屬性就會指向$("div").find('.foo')的DOM集合
$("div").find('.foo')  // 往前一級的preObect屬性就是指向$("div")的DOM集合

而這種能夠回溯到以前選擇的DOM集合的機制,是爲這兩個方法服務的:

.end() // 回溯到前一個jQuery對象,即prevObject屬性
.addBack() // 把當前位置和前一個位置的元素結合組合起來,而且將這個新的組合的元素集推入棧的上方

而利用這個回溯機制和對應的方法能夠進行以下的操做:

<ul class="first">
    <li class="foo">list item 1</li>
    <li>list item 2</li>
    <li class="bar">list item 3</li>
</ul>

<script>
    // foo類li標籤背景設置爲紅色, bar類li標籤背景設置爲綠色
    $("#test2").click(function(){
        //經過end連貫處理
        $('ul.first')
            .find('.foo')
            .css('background-color', 'red')
            .end()
            .find('.bar')
            .css('background-color', 'green');
    })
</scripts>

利用這個DOM元素棧能夠減小重複的查詢和遍歷的操做,而減小重複操做也正是優化jQuery代碼性能的關鍵所在。


end

end方法可以幫助咱們回溯到上一個DOM合集,所以該方法返回的就是一個jQuery對象,在源碼中的表現就是返回了prevObject對象:

end: function() {
    return this.prevObject || this.constructor(null);
}

那麼prevObject在什麼狀況下會產生?

在構建jQuery對象的時候,經過pushStack方法構建,以下代碼:

pushStack: function( elems ) {
    // Build a new jQuery matched element set
    // 這裏將傳進來的DOM元素,經過調用jQuery的方法構建成一個新的jQuery對象
    var ret = jQuery.merge( this.constructor(), elems );

    // Add the old object onto the stack (as a reference)
    // 在此對象上把前一個jQuery對象添加到prevObject屬性中
    ret.prevObject = this;
    ret.context = this.context;

    // Return the newly-formed element set
    // 最後返回這個jQuery對象
    return ret;
 }

那麼在find方法中,爲了將前一個jQuery對象推入棧中,就會調用這個pushStack方法來構建:

jQuery.fn.extend({
    find: function(selector) {

        //...........................省略................................

        //經過sizzle選擇器,返回結果集
        jQuery.find(selector, self[i], ret);

        // Needed because $( selector, context ) becomes $( context ).find( selector )
        ret = this.pushStack(len > 1 ? jQuery.unique(ret) : ret); // 這裏就是將實例對象推入棧中,而後返回新的jQuery對象
        ret.selector = this.selector ? this.selector + " " + selector : selector;
        return ret;
    }
}

仿棧與隊列的操做

jQuery既然是模仿的數組結構,那麼確定會實現一套類數組的處理方法,好比常見的棧與隊列操做push、pop、shift、unshift、求和、遍歷循環each、排序及篩選等一系的擴展方法。

jQuery提供了.get()、:index()、 :lt()、:gt()、:even()及 :odd()這類索引值相關的選擇器,他們的做用能夠過濾他們前面的匹配表達式的集合元素,篩選的依據就是這個元素在原先匹配集合中的順序。

先來看看get方法的實現源碼:

get: function(num) {
    return num != null ?  // 不傳參爲undefined, 走false線
    // Return just the one element from the set
    (num < 0 ? this[num + this.length] : this[num]) :
    // Return all the elements in a clean array
    slice.call(this); // 返回整個DOM元素數組
}

get與eq的區別

熟悉jQuery的童鞋都清楚,get返回的是DOM元素,而eq返回的是jQuery對象,這樣就能夠繼續執行鏈式操做。

eq實現的原理:

eq: function( i ) {
    var len = this.length,
        j = +i + ( i < 0 ? len : 0 );
    return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );

上面實現代碼的邏輯就是跟get是同樣的,區別就是經過了pushStack產生了一個新的jQuery對象。

若是須要的是一個合集對象要怎麼處理?所以jQuery便提供了一個slice方法,根據下標範圍取元素集合,並生成一個新的jQuery對象。

slice方法實現源碼:

slice: function() {
    return this.pushStack( slice.apply( this, arguments ) );
},

迭代器

迭代器是一個框架的重要設計。咱們常常須要提供一種方法順序用來處理聚合對象中各個元素,而又不暴露該對象的內部,這也是設計模式中的迭代器模式(Iterator)。

針對迭代器,這裏有幾個特色:

☑ 訪問一個聚合對象的內容而無需暴露它的內部。

☑ 爲遍歷不一樣的集合結構提供一個統一的接口,從而支持一樣的算法在不一樣的集合結構上進行操做。

☑ 遍歷的同時更改迭代器所在的集合結構可能會致使問題。

另外還要考慮這四點:

☑ 聚合對象,多是對象,字符串或者數組等類型

☑ 支持參數傳遞

☑ 支持上下文的傳遞

☑ 支持循環中退出(返回false的時候退出循環,節省性能)

簡單實現一個迭代器:

function each(obj, callback, context, arg) {
    var i = 0;
    var value;
    var length = obj.length;
    for (; i < length; i++) {
        value = callback.call(context || null, obj[i], arg);
        if (value === false) {
            break;
        }
    }

jQuery的each迭代器

$.each()函數和$(selector).each()是不同的,後者是專門用來遍歷一個jQuery對象的,是爲jQuery內部服務的。

jQuery的實例方法最終也是調用的靜態方法,咱們在以前就解釋過jQuery的實例與原型方法共享的設計。

$.each()實例方法以下:

// 內部是直接調用的靜態方法
each: function(callback, args) {
    return jQuery.each(this, callback, args);
},

jQuery.each靜態方法:

each: function(obj, callback, args) {
    var value,
        i = 0,
        length = obj.length,
        isArray = isArraylike(obj);

    if (args) {
        if (isArray) {
            for (; i < length; i++) {
                value = callback.apply(obj[i], args);

                if (value === false) {
                    break;
                }
            }
        } else {
            for (i in obj) {
                value = callback.apply(obj[i], args);

                if (value === false) {
                    break;
                }
            }
        }

實現原理幾乎一致,只是增長了對於參數的判斷。對象用for in遍歷,數組用for遍歷。

相關文章
相關標籤/搜索