【筆記】jQuery原始碼(文件處理3)

MOCHIKO發表於2018-03-31

一覽

今天是第四天啦!筆記的內容主要是跟著慕課網上的jQuery原始碼解析系列課程以及自己的理解+實踐來寫的(也比較偏向於自己的梳理,所以可能會有點亂),可能會有錯誤,歡迎指出。

jQuery對於DOM操作常用的方法大概有以下幾種:

  • append
  • prepend
  • before
  • after
  • replaceWith
  • appendTo
  • prependTo
  • insertBefore
  • insertAfter
  • replaceAll

其中這裡有些方法是對應的,但是它們的目標元素和被瞄準元素位置不一樣。
對於DOM的移除有以下四種常用的方法:

  • detach
  • empty
  • remove
  • unwrap

插入篇

insertAfter()

比如after()insertAfter()方法:

$(`.inner`).after(`<p>Test</p>`);
$(`<p>Test</p>`).insertAfter(`.inner`);

這部分課程講的不是特別清楚,所以直接搬出原始碼來看:

jQuery.each( 
//可以看到其實insertAfter和After其實本質是一樣的
{
    appendTo: "append",
    prependTo: "prepend",
    insertBefore: "before",
    insertAfter: "after",
    replaceAll: "replaceWith"
}, function( name, original ) {
    jQuery.fn[ name ] = function( selector ) {
        var elems,
            ret = [],
            //轉為選擇器
            insert = jQuery( selector ),
            last = insert.length - 1,
            i = 0;
        
        //給每個選擇器去新增對應的元素
        for ( ; i <= last; i++ ) {
            //如果不是最後一次操作,就把元素進行克隆,因為是p呼叫了這個方法,所以這裡的this是加入的p的jQuery物件
            elems = i === last ? this : this.clone( true );
            /* 再進行對應方法操作
            * 舉例:如果是insertAfter則呼叫了After()方法         
            */
            jQuery( insert[ i ] )[ original ]( elems );
            //把元素新增到ret陣列裡
            push.apply( ret, elems.get() );
        }
        //構建一個新jQuery物件,以便實現鏈式
        return this.pushStack( ret );
    };
} );

append()

append():在匹配的元素末尾新增

append: function() {
    return this.domManip( arguments, function( elem ) {
        if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
            var target = manipulationTarget( this, elem );
            target.appendChild( elem );
        }
    });
}

這裡用到了一個manipulationTarget函式,用於處理向table元素加入tr但沒有tbody的情況,以下是它的原始碼:

function manipulationTarget( elem, content ) {
    return jQuery.nodeName( elem, "table" ) &&
        //如果加入內容文件碎片,就用它的孩子來判斷是不是tr
        jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ?
        //是tr的話如果存在tbody直接返回,否則建立所屬文件的tbody給elem(body)後返回
        elem.getElementsByTagName( "tbody" )[ 0 ] ||
            elem.appendChild( elem.ownerDocument.createElement( "tbody" ) ) :
        elem;
}

替換篇

replaceWith

replaceWidth方法對元素進行替換,呼叫的是原生的replaceChild方法,因為涉及到“刪除”這個元素,所以會呼叫cleanData這個方法:

replaceWith: function() {
    var arg = arguments[0];
    this.domManip(arguments, function(elem) {
        arg = this.parentNode;
        jQuery.cleanData(getAll(this));
        if (arg) {
            arg.replaceChild(elem, this);
        }
    });
    return arg && (arg.length || arg.nodeType) ? this : this.remove();
}   

最後返回的指向也從原來的元素換成了替換後的元素。
然後再來瞅瞅這個cleanData所做的事情(水很深,就先大概瞭解一下):

cleanData: function( elems ) {
    var data, elem, type,
        special = jQuery.event.special, //自定義事件
        i = 0;

    for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {
        if ( acceptData( elem ) ) {
            if ( ( data = elem[ dataPriv.expando ] ) ) {
              //如果data被繫結了事件,就一一移除
                if ( data.events ) {
                    for ( type in data.events ) {
                        if ( special[ type ] ) {
                            //用於移除special中的事件
                            jQuery.event.remove( elem, type );
                        } else {

                            jQuery.removeEvent( elem, type, data.handle );
                        }
                    }
                }
                elem[ dataPriv.expando ] = undefined;
            }
            if ( elem[ dataUser.expando ] ) {
                elem[ dataUser.expando ] = undefined;
            }
        }
    }
}

jQuery.event.special中有load事件,focus事件,blur事件,click事件和beforeunload事件等等…裡面做了一些處理去彌補原本的不足。

移除篇

innerText是常用的文字清理方法,火狐不相容,但提供了類似的textContent方法。
兩者的區別

IE中的innerText是需要對innerHTML的值進行:
1.HTML轉義2.HTML解釋和CSS樣式解釋3.剔除格式資訊4.留下的純文字
而textContent沒有2、3步,在經過了HTML轉義之後直接剔除所有html標籤後得到的純文字。

.empty()

為了避免佔有本該釋放的資源,所以jQuery進行刪除前必須要先移除子元素的資料事件處理函式。。

empty: function() {
    var elem,
        i = 0;
    for (;
        (elem = this[i]) != null; i++) {
        if (elem.nodeType === 1) {
            //進行刪前準備工作
            jQuery.cleanData(getAll(elem, false));
            //元素內容清空
            elem.textContent = "";
        }
    }
    return this;
}

jQuery.cleanData方法:
通過元素判斷上繫結的expando的這個uuid在與之對應的cache中找到資料與事件控制程式碼加以刪除。

remove()

.remove() 將元素移出DOM,可以移除自身元素。

/*
* keepData僅供jQuery內部使用
*/
remove: function(selector, keepData ) {
    var elem,
        //如果存在選擇器,就先過濾自己,不存在則就是自己
        elems = selector ? jQuery.filter(selector, this) : this,
        i = 0;
    for (;
        (elem = elems[i]) != null; i++) {
        //如果不保留(keepData==false),則先進行事件移除
        if (!keepData && elem.nodeType === 1) {
            jQuery.cleanData(getAll(elem));
        }
        
        //如果元素存在父親節點
        if (elem.parentNode) {
            //如果保留事件且元素所屬文件存在這個元素
            if (keepData && jQuery.contains(elem.ownerDocument, elem)) {
                //在全域性指令碼中進行記錄
                setGlobalEval(getAll(elem, "script"));
            }
            
            //移除元素 elem.parentNode.removeChild(elem);
        }
    }
    return this;
}

因為remove可能是移除自身,所以需要用父親元素來進行移除。

附加getAll方法:

function getAll( context, tag ) {
   //如果context下存在這個標籤元素就獲取,如果沒有結果就用querySelectorAll去獲取,再沒結果就是空組
    var ret = context.getElementsByTagName ? context.getElementsByTagName( tag || "*" ) :
            context.querySelectorAll ? context.querySelectorAll( tag || "*" ) :
            [];
    
    return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
        jQuery.merge( [ context ], ret ) :
        ret;
}

detach()

detach()方法移除被選元素,包括所有文字和子節點。但它保留jQuery物件中的匹配的元素,因而可以在將來再使用這些匹配的元素,它也保留所有繫結的事件、附加的資料,這一點與remove() 不同。
所以它和remove方法不一樣的就是keepData這個屬性,只要傳入true就可以了:

detach: function(selector) {
      return this.remove(selector, true);
}

這個方法可以用於刪除後又將會被新增進來的元素。

相關文章