摘要:在前兩篇,為了方便調(diào)試,我們寫了一個非常簡單的 jQuery.fn.init 方法:jQuery.fn.init = function (selector, context, root) { if (!selector)&n
在前兩篇,為了方便調(diào)試,我們寫了一個非常簡單的 jQuery.fn.init 方法:
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 里執(zhí)行 $('div') 時可以取得這么一個類數(shù)組對象:
在完整的 jQuery 中通過 $(selector) 的形式獲取的對象也基本如此 —— 它是一個對象而非數(shù)組,但可以通過下標(biāo)(如 $div[index] )或 .get(index) 接口來獲取到相應(yīng)的 DOM 對象,也可以直接通過 .length 來獲取匹配到的 DOM 對象總數(shù)。
這么實現(xiàn)的原因是 —— 方便,該對象畢竟是 jQuery 實例,繼承了所有的實例方法,同時又直接是所檢索到的DOM集合(而不需要通過 $div.getDOMList() 之類的方法來獲?。喼币皇B。
如下圖所示便是一個很尋常的 JQ 類數(shù)組對象(初始化執(zhí)行的代碼是 $('div') ):
1. Sizzle 引入
在 jQuery 中,檢索DOM的能力來自于 Sizzle 引擎,它是 JQ 最核心也是最復(fù)雜的部分,在后續(xù)有機會我們再對其作詳細(xì)介紹,當(dāng)前階段,我們只需要直接“獲取”并“使用”它即可。
Sizzle 是開源的選擇器引擎,其官網(wǎng)是 http://sizzlejs.com/ ,直接在首頁便能下載到最新版本。
我們在 src 目錄下新增一個 /sizzle 文件夾,并把下載到的 sizzle.js 放進(jìn)去(即存放為 src/sizzle/sizzle.js ),接著得對其做點小修改,使其得以適應(yīng)我們 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 賦予靜態(tài)接口 jQuery.find:
import Sizzle from './sizzle.js'; var selectorInit = function(jQuery){ jQuery.find = Sizzle; }; export default selectorInit;
別忘了在打包的入口文件里引入該模塊并執(zhí)行:
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 集合的數(shù)組(注意類型直接就是Array,不是 document.querySelectorAll 那樣返回的 nodeList )。
我們需要多做一點處理,來將這個數(shù)組轉(zhuǎn)換為前頭提到的類數(shù)組JQ對象。
另外,雖然現(xiàn)在 JQ 的工具方法有了檢索DOM的能力,但其實例方法是木有的,鑒于構(gòu)造器的靜態(tài)屬性不會繼承給實例,會導(dǎo)致我們沒法鏈?zhǔn)降貋碇С?find,比如:
$('div').find('p').find('span')
很明顯,這可以在 jQuery.fn.extend 里多加一個 find 接口來實現(xiàn),不過不著急,咱們一步一步來。
2. $.merge 方法
針對上述的第一個需求點,我們修改下 src/core.js ,往 jQuery.extend 里新增一個 jQuery.merge 靜態(tài)方法,方便把檢索到的 DOM 集合數(shù)組轉(zhuǎn)換為類數(shù)組對象:
jQuery.fn = jQuery.prototype = { jquery: version, length: 0, // 修改點1,JQ實例.length 默認(rèn)為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 的代碼段太好理解了,其實現(xiàn)的能力為:
<div>hello</div> <div>world</div> <script> var divs = $.find('div'); //純數(shù)組 var $div1 = $.merge( ['hi'], divs); //右邊的數(shù)組合并到左邊的數(shù)組,形成一個新數(shù)組 var $div2 = $.merge( {0: 'hi', length: 1}, divs); //右邊的數(shù)組合并到左邊的對象,形成一個新的類數(shù)組對象 console.log($div1); console.log($div2); </script>
運行輸出:
因此,如果我們在 jQuery.fn.init 中,把 this 傳入為 $.merge 的 first 參數(shù)(留意這里this為JQ實例對象自身,默認(rèn) length 實例屬性為0),再把檢索到的 DOM 集合數(shù)組作為 second 參數(shù)傳入,那么就能愉快地得到我們想要的 JQ 類數(shù)組對象了。
我們簡單地修改下 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實例,默認(rèn)實例屬性 .length 為0 } return this; } };
我們打包后執(zhí)行:
<div>hello</div> <div>world</div> <script> var $div = $('div'); console.log($div); </script>
輸出正是我們所想要的類數(shù)組對象:
3. 擴展 $.fn.find
針對第二個需求點 —— 鏈?zhǔn)街С?find 接口,我們需要給 $.fn 擴展一個 find 方法:
jQuery.fn.extend({ find: function( selector ) { //鏈?zhǔn)街С謋ind var i, ret, len = this.length, self = this; ret = []; for ( i = 0; i < len; i++ ) { //遍歷 jQuery.find( selector, self[ i ], ret ); //直接利用 Sizzle 接口,把結(jié)果注入到 ret 數(shù)組中去 } return ret; } });
這里我們依舊直接使用了 Sizzle 接口 —— 當(dāng)帶上了第三個參數(shù)(數(shù)組類型)時,Sizzle 會把檢索到的 DOM 集合注入到該參數(shù)中去(API文檔)。
我們打包后執(zhí)行下方代碼:
<div><span>hi</span><b>hello</b></div> <div><span>你好</span></div> <script> var $span = $('div').find('span'); console.log($span); </script>
效果如下:
可以看到,我們要的子元素是出來了,不過呢,這里獲取到的是純數(shù)組,而非 JQ 對象,處理方法很簡單 —— 直接調(diào)用前面剛加上的 $.merge 方法即可。
另外也有個問題,一旦咱們獲取到了子孫元素(如上方代碼中的span),那么如果我們需要重新取到其祖先元素(如上方代碼中的div),就又得重新去走 $('div') 來檢索了,這樣麻煩且效率不高。
而我們知道,在 jQuery 中是有一個 $.fn.end 方法可以返回上一次檢索到的 JQ 對象的:
$('div').find('span').end() //返回$('div')對象
處理方法也很簡單,參考瀏覽器的歷史記錄棧,我們也來寫一個遵循后進(jìn)先出的棧操作方法。
可能你在第一時間會想到,是否使用一個數(shù)組,通過 push 和 pop 來實現(xiàn)入棧和出棧的功能。
事實上我們有更簡單的形式 —— 給新的 JQ 對象新增一個 .prevObject 屬性并指向舊 JQ 對象,這樣一來,我們想獲取當(dāng)前 JQ 對象之前的一次 JQ 對象,通過該屬性就能直接取到了:
jQuery.fn = jQuery.prototype = { jquery: version, length: 0, constructor: jQuery, /** * 入棧操作 * @param elems {Array} * @returns {*} */ pushStack: function( elems ) { //elems是數(shù)組 // 將檢索到的DOM集合轉(zhuǎn)換為JQ類數(shù)組對象 var ret = jQuery.merge( this.constructor(), elems ); //this.constructor() 返回了一個 length 為0的JQ對象 // 添加關(guān)系鏈,新JQ對象的prevObject屬性指向舊JQ對象 ret.prevObject = this; return ret; } //省略... }
這樣通過 pushStack 接口包裝下,就解決了上面說的兩個問題,我們改下 $.fn.find 代碼:
jQuery.fn.extend({ find: function( selector ) { //鏈?zhǔn)街С謋ind var i, ret, len = this.length, self = this; ret = []; for ( i = 0; i < len; i++ ) { //遍歷 jQuery.find( selector, self[ i ], ret ); //直接利用 Sizzle 接口,把結(jié)果注入到 ret 數(shù)組中去 } return this.pushStack( ret ); //轉(zhuǎn)為JQ對象 } });
從性能上考慮,我們這樣寫會更好一些(減少一些merge里的遍歷):
jQuery.fn.extend({ find: function( selector ) { //鏈?zhǔn)街С謋ind var i, ret, len = this.length, self = this; ret = this.pushStack( [] ); //轉(zhuǎn)為JQ對象 for ( i = 0; i < len; i++ ) { //遍歷 jQuery.find( selector, self[ i ], ret ); //直接利用 Sizzle 接口,把結(jié)果注入到 ret 數(shù)組中去 } return ret } });
4. $.fn.end、$.fn.eq 和 $.fn.get
鑒于我們在 pushStack 中加上了 oldJQ.prevObject 的關(guān)系鏈,那么 $.fn.end 接口的實現(xiàn)就太簡單了:
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可以是負(fù)數(shù) return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); //容錯處理,若i過大或過小,返回空數(shù)組 }, get: function( num ) { return num != null ? // 支持倒序搜索,num可以是負(fù)數(shù) ( num < 0 ? this[ num + this.length ] : this[ num ] ) : // 克隆一個新數(shù)組,避免指向相同 [].slice.call( this ); //建議把 [].slice 封裝到 var.js 中去復(fù)用 } });
通過 eq 接口我們可以知道,后續(xù)任何方法,如果要返回一個 JQ 對象,基本都需要裹一層 pushStack 做處理,來確保 prevObject 的正確引用。
當(dāng)然,這也輕松衍生了 $.fn.first 和 $.fn.last 兩個工具方法:
jQuery.fn.extend({ first: function() { return this.eq( 0 ); }, last: function() { return this.eq( -1 ); } });
本手記就先寫到這里,避免太多內(nèi)容難消化。事實上,我們的 $.fn.init 、$.find 和 $.fn.find 都還有一些不完善的地方:
1. $.fn.init 方法沒有兼顧到各種參數(shù)類型的情況,也還沒有加上第二個參數(shù) context 來做上下文預(yù)設(shè);
2. 同上,$.fn.find 也未對兼顧到各種參數(shù)類型的情況;
3. $.fn.find 返回結(jié)果有可能帶有重復(fù)的 DOM
例如:
<div><div><span>hi</span></div></div> <script> var $span = $('div').find('span'); console.log($span); //重復(fù)了 </script>