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

在前兩章,爲了方便調試,咱們寫了一個很是簡單的 jQuery.fn.init 方法:node

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

所以咱們在 demo 裏執行 $('div') 時能夠取得這麼一個類數組對象:jquery

在完整的 jQuery 中經過 $(selector) 的形式獲取的對象也基本如此 —— 它是一個對象而非數組,但能夠經過下標(如 $div[index] )或 .get(index) 接口來獲取到相應的 DOM 對象,也能夠直接經過 .length 來獲取匹配到的 DOM 對象總數。git

這麼實現的緣由是 —— 方便,該對象畢竟是 jQuery 實例,繼承了全部的實例方法,同時又直接是所檢索到的DOM集合(而不須要經過 $div.getDOMList() 之類的方法來獲取),簡直一石二鳥。github

以下圖所示即是一個很尋常的 JQ 類數組對象(初始化執行的代碼是 $('div')數組

1. Sizzle 引入瀏覽器

在 jQuery 中,檢索DOM的能力來自於 Sizzle 引擎,它是 JQ 最核心也是最複雜的部分,在後續有機會咱們再對其做詳細介紹,當前階段,咱們只須要直接「獲取」並「使用」它便可。dom

Sizzle 是開源的選擇器引擎,其官網是 http://sizzlejs.com/ ,直接在首頁便能下載到最新版本。工具

咱們在 src 目錄下新增一個 /sizzle 文件夾,並把下載到的 sizzle.js 放進去(即存放爲 src/sizzle/sizzle.js ),接着得對其作點小修改,使其得以適應咱們 rollup 的打包模式。性能

其原先代碼爲:優化

(function( window ) {

var i,
    support,

//...省略一大堆有的沒的
Sizzle.noConflict = function() { if ( window.Sizzle === Sizzle ) { window.Sizzle = _sizzle; } return Sizzle; }; if ( typeof define === "function" && define.amd ) { define(function() { return Sizzle; }); // Sizzle requires that there be a global window in Common-JS like environments } else if ( typeof module !== "undefined" && module.exports ) { module.exports = Sizzle; } else { window.Sizzle = Sizzle; } // EXPOSE })( window );

將這段代碼的頭和尾替換爲:

var i,
    support,

//...省略

Sizzle.noConflict = function() {
    if ( window.Sizzle === Sizzle ) {
        window.Sizzle = _sizzle;
    }

    return Sizzle;
};

export default Sizzle;

同時新增一個初始化文件 src/sizzle/init.js ,用於把 Sizzle 賦予靜態接口 jQuery.find:

import Sizzle from './sizzle.js';

var selectorInit = function(jQuery){
    jQuery.find = Sizzle;
};



export default selectorInit;

別忘了在打包的入口文件裏引入該模塊並執行:

import jQuery from './core';
import global from './global';
import init from './init';
import sizzleInit from './sizzle/init';  //新增

global(jQuery);
init(jQuery);
sizzleInit(jQuery);  //新增

export default jQuery;

打包後咱們就能愉快地經過 jQuery.find 接口來使用 Sizzle 的各類能力了(使用方式能夠參考 Sizzle 的API文檔

留意 $.find(XXX) 返回的是一個匹配到的 DOM 集合的數組(注意類型直接就是Array,不是 document.querySelectorAll 那樣返回的 nodeList )

咱們須要多作一點處理,來將這個數組轉換爲前頭提到的類數組JQ對象。

另外,雖然如今 JQ 的工具方法有了檢索DOM的能力,但其實例方法是木有的,鑑於構造器的靜態屬性不會繼承給實例,會致使咱們無法鏈式地來支持 find,好比:

$('div').find('p').find('span')

很明顯,這能夠在 jQuery.fn.extend 裏多加一個 find 接口來實現,不過不着急,我們一步一步來。

2. $.merge 方法

針對上述的第一個需求點,咱們修改下 src/core.js ,往 jQuery.extend 裏新增一個 jQuery.merge 靜態方法,方便把檢索到的 DOM 集合數組轉換爲類數組對象:

jQuery.fn = jQuery.prototype = {
    jquery: version,
    length: 0,  // 修改點1,JQ實例.length 默認爲0
    //...
}

jQuery.extend( {
    merge: function( first, second ) {  //修改點2,新增 merge 工具接口
        var len = +second.length,
            j = 0,
            i = first.length;

        for ( ; j < len; j++ ) {
            first[ i++ ] = second[ j ];
        }

        first.length = i;

        return first;
    },
    //...
});

merge 的代碼段太好理解了,其實現的能力爲:

<div>hello</div>
<div>world</div>

<script>
    var divs = $.find('div'); //純數組
    var $div1 = $.merge( ['hi'], divs); //右邊的數組合併到左邊的數組,造成一個新數組
    var $div2 = $.merge( {0: 'hi', length: 1}, divs); //右邊的數組合併到左邊的對象,造成一個新的類數組對象

    console.log($div1);
    console.log($div2);
</script>

運行輸出:

所以,若是咱們在 jQuery.fn.init 中,把 this 傳入爲 $.merge 的 first 參數(留意這裏this爲JQ實例對象自身,默認 length 實例屬性爲0),再把檢索到的 DOM 集合數組做爲 second 參數傳入,那麼就能愉快地獲得咱們想要的 JQ 類數組對象了。

咱們簡單地修改下 src/init.js

    jQuery.fn.init = function (selector, context, root) {
        if (!selector) {
            return this;
        } else {
            var elemList = jQuery.find(selector);
            if (elemList.length) {
                jQuery.merge( this, elemList );  //this是JQ實例,默認實例屬性 .length 爲0
            }
            return this;
        }
    };

咱們打包後執行:

<div>hello</div>
<div>world</div>

<script>
    var $div = $('div');
    console.log($div);
</script>

輸出正是咱們所想要的類數組對象:

3. 擴展 $.fn.find

針對第二個需求點 —— 鏈式支持 find 接口,咱們須要給 $.fn 擴展一個 find 方法:

jQuery.fn.extend({
    find: function( selector ) {  //鏈式支持find
        var i, ret,
            len = this.length,
            self = this;

        ret = [];

        for ( i = 0; i < len; i++ ) {  //遍歷
            jQuery.find( selector, self[ i ], ret );  //直接利用 Sizzle 接口,把結果注入到 ret 數組中去
        }

        return ret;
    }
});

這裏咱們依舊直接使用了 Sizzle 接口 —— 當帶上了第三個參數(數組類型)時,Sizzle 會把檢索到的 DOM 集合注入到該參數中去API文檔

咱們打包後執行下方代碼:

<div><span>hi</span><b>hello</b></div>
<div><span>你好</span></div>

<script>
    var $span = $('div').find('span');
    console.log($span);
</script>

效果以下:

能夠看到,咱們要的子元素是出來了,不過呢,這裏獲取到的是純數組,而非 JQ 對象,處理方法很簡單 —— 直接調用前面剛加上的 $.merge 方法便可。

另外也有個問題,一旦我們獲取到了子孫元素(如上方代碼中的span),那麼若是咱們須要從新取到其祖先元素(如上方代碼中的div),就又得從新去走 $('div') 來檢索了,這樣麻煩且效率不高。

而咱們知道,在 jQuery 中是有一個 $.fn.end 方法能夠返回上一次檢索到的 JQ 對象的:

$('div').find('span').end()  //返回$('div')對象

處理方法也很簡單,參考瀏覽器的歷史記錄棧,咱們也來寫一個遵循後進先出的棧操做方法。

可能你在第一時間會想到,是否使用一個數組,經過 push 和 pop 來實現入棧和出棧的功能。

事實上咱們有更簡單的形式 —— 給新的 JQ 對象新增一個 .prevObject 屬性並指向舊 JQ 對象,這樣一來,咱們想獲取當前 JQ 對象以前的一次 JQ 對象,經過該屬性就能直接取到了:

jQuery.fn = jQuery.prototype = {
    jquery: version,
    length: 0, 
    constructor: jQuery,
    /**
     * 入棧操做
     * @param elems {Array}
     * @returns {*}
     */
    pushStack: function( elems ) {  //elems是數組

        // 將檢索到的DOM集合轉換爲JQ類數組對象
        var ret = jQuery.merge( this.constructor(), elems );  //this.constructor() 返回了一個 length 爲0的JQ對象

        // 添加關係鏈,新JQ對象的prevObject屬性指向舊JQ對象
        ret.prevObject = this;

        return ret;
    }
    //省略...
}

這樣經過 pushStack 接口包裝下,就解決了上面說的兩個問題,咱們改下 $.fn.find 代碼:

jQuery.fn.extend({
    find: function( selector ) {  //鏈式支持find
        var i, ret,
            len = this.length,
            self = this;

        ret = [];

        for ( i = 0; i < len; i++ ) {  //遍歷
            jQuery.find( selector, self[ i ], ret );  //直接利用 Sizzle 接口,把結果注入到 ret 數組中去
        }

        return this.pushStack( ret );  //轉爲JQ對象
    }
});

從性能上考慮,咱們這樣寫會更好一些(減小一些merge裏的遍歷)

jQuery.fn.extend({
    find: function( selector ) {  //鏈式支持find
        var i, ret,
            len = this.length,
            self = this;

        ret = this.pushStack( [] ); //轉爲JQ對象

        for ( i = 0; i < len; i++ ) {  //遍歷
            jQuery.find( selector, self[ i ], ret );  //直接利用 Sizzle 接口,把結果注入到 ret 數組中去
        }

        return ret
    }
}); 

4. $.fn.end、$.fn.eq 和 $.fn.get

鑑於咱們在 pushStack 中加上了 oldJQ.prevObject 的關係鏈,那麼 $.fn.end 接口的實現就太簡單了:

jQuery.fn.extend({
    end: function() {
        return this.prevObject || this.constructor();
    }
});

直接返回上一次檢索到的JQ對象(若是木有,則返回一個空的JQ對象)

這裏順便再多添加兩個你們熟悉的不能再熟悉的 $.fn.eq 和 $.fn.get 工具方法,代碼很是的簡單:

jQuery.fn.extend({
    end: function() {
        return this.prevObject || this.constructor();
    },
    eq: function( i ) {
        var len = this.length,
            j = +i + ( i < 0 ? len : 0 );  //支持倒序搜索,i能夠是負數
        return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); //容錯處理,若i過大或太小,返回空數組
    },
    get: function( num ) {
        return num != null ?

            // 支持倒序搜索,num能夠是負數
            ( num < 0 ? this[ num + this.length ] : this[ num ] ) :

            // 克隆一個新數組,避免指向相同
            [].slice.call( this );  //建議把 [].slice 封裝到 var.js 中去複用
    }
});

經過 eq 接口咱們能夠知道,後續任何方法,若是要返回一個 JQ 對象,基本都須要裹一層 pushStack 作處理,來確保 prevObject 的正確引用。

固然,這也輕鬆衍生了 $.fn.first 和 $.fn.last 兩個工具方法:

jQuery.fn.extend({
    first: function() {
        return this.eq( 0 );
    },
    last: function() {
        return this.eq( -1 );
    }
});

本章就先寫到這裏,避免太多內容難消化。事實上,咱們的 $.fn.init 、$.find 和 $.fn.find 都還有一些不完善的地方:

1. $.fn.init 方法沒有兼顧到各類參數類型的狀況,也尚未加上第二個參數 context 來作上下文預設;

2. 同上,$.fn.find 也未對兼顧到各類參數類型的狀況;

3. $.fn.find 返回結果有可能帶有重複的 DOM,例如:

<div><div><span>hi</span></div></div>

<script>
    var $span = $('div').find('span');
    console.log($span);  //重複了
</script>

這些存在的問題咱們都會在後面的篇章作進一步的優化。

另外提幾個點:

1. 部分讀者是從公衆號上閱讀本系列文章的,建議也要同時關注本人博客好一些 —— 有時我會對文章作一些更改,讓其更易讀懂;
2. 對於前兩篇文章,部分基礎較差的讀者貌似不太好理解,我其實有考慮寫個番外篇來幫大家梳理這塊(特別是原型鏈的)知識點,若是以爲有須要的話能夠留言給我,要求的人多的話我就動筆了;
3. 工做較忙,發文頻率大約是1到2週一篇文章。近期其實蠻多讀者催我更文的,但爲了保持文章質量,須要多點時間,不但願數量上來了質量卻下去了。

本文的代碼掛在個人github上,有須要的同窗能夠自行下載調試。共勉~

相關文章
相關標籤/搜索