寫在前面
前面陸陸續續寫了jQuery源碼的一些分析,盡可能地想要cover里面的源碼細節,結果導致進度有些緩慢。jQuery的源碼本來就比較晦澀,里面還有很多為了解決兼容問題很引入的神代碼,如果不google的話壓根不知道那一段段代碼為什么會存在於人世。
於是就一直在重復坐着這么件事情,到處谷歌或者請教別人,這段兼容代碼是為解決神馬問題引入的。好不容易把所有的源碼細節搞清楚,喝着咖啡對着電腦欣賞自己的勞動成果,內心卻閃過一絲奇怪的感覺:我花了這么長的時間究竟做了什么?就為了搞清楚這段常理無法解釋的代碼?而在這之前已經有無數仁人志士在這上面浪費了自己多少寶貴的時間。
當然,學習jQuery源碼對於我這種老菜鳥還是很有助益的,只不過需要換種方式,不再去摳一些無謂的細節了,有些比較難理解的東西就直接扔出來看下園里的朋友們幫忙解答下了。社會化寫作似乎是更好的方式,之前也想過把系列文章扔github讓別人來幫忙完善,不過顯然對於大部分人來說這種方式成本還是太高,而自己寫的東西暫時也沒有說讓人家去fork然后pull request的價值,就作罷了。
技術無關的內容就此打住,還是老老實實開始我的源碼分析。里面標疑惑的地方,是還沒來得及去摳的細節(一般就是一些正則神馬的),圍觀的群眾如果能夠幫忙解答下那是真真的好~~
簡單例子
jQuery.fn.html()同樣屬於使用頻率比較高的接口,從它的接口文檔http://api.jquery.com/html/,可知有如下幾種用法,假設有如下HTML片段
<div id="casper"> <span>name:</span> <span>casper</span> </div>
讀取:$(selector).html()
運行下面代碼
console.log( $('#casper').html() );
輸出:
<span>name:</span>
<span>casper</span>
設置一:$(selector).html(value)
還是上面的HTML,運行下面腳本
$('#casper').html('<p>大家好,我是第一段文本</p><p>大家好,我是第二段文本</p>');
原本的HTML變成
<div id="casper"><p>大家好,我是第一段文本</p><p>大家好,我是第二段文本</p></div>
設置二:$(selector).html(callback)
在上面例子中,HTML變成如下
<div id="casper"><p>大家好,我是第一段文本</p><p>大家好,我是第二段文本</p></div>
運行如下代碼,參數 index、html 分別代碼什么,見下面輸出即可,不贅述
$('#casper p').html(function(index, html){ return index + '、原本的內容:'+ html; });
結果原來的HTML變成(為方便查看進行了適當格式化)
<div id="casper"> <p>0、原本的內容:大家好,我是第一段文本</p> <p>1、原本的內容:大家好,我是第二段文本</p> </div>
源碼分析
下面濕jQuery.fn.html 的源碼,就直接貼上來了,一點都不意外,又見到了全知全能的jQuery.access方法。。。這里我們先不立即展開,下文慢慢分析

html: function( value ) { return jQuery.access( this, function( value ) { var elem = this[0] || {}, i = 0, l = this.length; if ( value === undefined ) { return elem.nodeType === 1 ? elem.innerHTML.replace( rinlinejQuery, "" ) : undefined; } // See if we can take a shortcut and just use innerHTML if ( typeof value === "string" && !rnoInnerhtml.test( value ) && ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { value = value.replace( rxhtmlTag, "<$1></$2>" ); try { for (; i < l; i++ ) { // Remove element nodes and prevent memory leaks elem = this[i] || {}; if ( elem.nodeType === 1 ) { jQuery.cleanData( getAll( elem, false ) ); elem.innerHTML = value; } } elem = 0; // If using innerHTML throws an exception, use the fallback method } catch(e) {} } if ( elem ) { this.empty().append( value ); } }, null, value, arguments.length ); }
把傳入jQuery.access的會掉方法的方法體去掉,我們看到如下代碼,就是調用了下jQuery.access而已(這里我們還不知道jQuery.access那噩夢般的參數究竟是干嘛的):
html: function( value ) { return jQuery.access( this, function( value ) { // 先隱藏掉方法體內的細節 }, null, value, arguments.length ); },
實在不知道如何下手分析jQuery.access,就直接跳過jQuery.access運行的內部細節,給出各種情況下的運行分支
源碼分析之:fn被調用的各種情況
假設在jQuery.fn.html方法中,第二個傳入的回調方法為fn
讀取:$(selector).html()
html: function( value ) { return fn.call(this); },
設置一:$(selector).html(value)
html: function( value ) { fn.call( this, value ); return this; },
設置二:$(selector).html(callback)
這里可能比較費解一點,得結合設置一的代碼來看(其實結合了業不容易看懂),可以看到,內部比較曲折,最終將$(selector).html(callback)轉成了$(selector).html(value)來實現,這種轉換手法在jQuery源碼里很常見。
html: function( callback ) { var elems = this, i = 0, length = elems.length; var bulk = fn; // elem為dom元素,value為該dom元素最初的innerHTML fn = function( elem, value ) { return bulk.call( jQuery( elem ), value ); }; for ( ; i < length; i++ ) { // 第一步:fn( elems[i], key ) ) 返回elem[i]的innerHTML,相當於 $(elem[i]).html() // 第二步:callback.call( elems[i], i, fn( elems[i], key ) ),就是將 i、$(elem[i]).html()當參數傳給 callback // 第三步:fn( elems[i], XXXX ) 到了這一步,其實就是 $(elem[i]).html(value)的等價形式了,因為第三步中已經返回了一段html文本 fn( elems[i], callback.call( elems[i], i, fn( elems[i], key ) ) ); } return this; }
源碼分析之:fn各種情況下內部的分支邏輯
接下來到大頭,上面說的fn的源碼了,下面列出各種情況下,fn內部的處理邏輯
讀取:$(selector).html()
fn里面的處理邏輯,很簡單,將$(selector)選中的第一個dom元素的innerHTML屬性返回。
疑問:rinlinejQuery這個正則是干嘛?為什么還要將elem.innerHTML先替換一下再返回?
var elem = this[0] || {}, // this為選中的jQuery對象 i = 0, l = this.length; if ( value === undefined ) { return elem.nodeType === 1 ? elem.innerHTML.replace( rinlinejQuery, "" ) : undefined; }
設置一:$(selector).html(value)
代碼也不算復雜,就是遍歷下$(selector)選中的元素,將它們的innerHTML分別設置成value。這里麻煩點在於,value存在,還得符合一堆條件才能走進這個if,原諒我偷懶了
疑問:
!rnoInnerhtml.test( value ):判斷神馬的
( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ):判斷神馬的
( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ):判斷神馬的
!wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ]:判斷神馬的
value = value.replace( rxhtmlTag, "<$1></$2>" ); 作用是神馬
// See if we can take a shortcut and just use innerHTML if ( typeof value === "string" && !rnoInnerhtml.test( value ) && ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { value = value.replace( rxhtmlTag, "<$1></$2>" ); try { for (; i < l; i++ ) { // Remove element nodes and prevent memory leaks elem = this[i] || {}; if ( elem.nodeType === 1 ) { jQuery.cleanData( getAll( elem, false ) ); elem.innerHTML = value; } } elem = 0; // If using innerHTML throws an exception, use the fallback method } catch(e) {} }
設置二:$(selector).html(callback)
這里直接略過,因為fn內部的邏輯是跟$(selector).html(value)一樣的
寫在后面
本文有些偷懶,湊合着看吧。。里面提到的疑惑,如果能夠幫忙解答下就更好了。。。