jQuery誕生記-原理與機制

1、看似偶然的東西實際是必然會發生的

我大學時候在圖書館翻過一本很破舊的書,講生物理論的,主要內容就是探討生命的產生是偶然仍是必然。裏面不少亞里士多德都看不懂的公式計算什麼的,還有模擬原始地球環境出現了有機物的實驗什麼的 。總之,書論述的觀點是:「在當時的地球環境下,生命的產生是必然的!」 無數次機會的偶然條件、無數次化合物的相遇反應等一定會產生有機物,再有N多偶然,有機物必然造成了有機體……javascript

這種理論相似於,你是個過馬路很是當心的人,且你萬壽無疆,除了怕被汽車撞。給你100萬年的壽命,你最後必然仍是被車撞死。css

若是以這種理論來看jQuery的出現,結論也應該是必然的html

2、需求、動力、發展、事物產生與jQuery的誕生

刀削麪機器人

一個成熟的東西顯然不是一口氣就出來的,所謂「一鏟子挖不了一口井」,我想jQuery的原做者再天才,也是按部就班過來的,如何個按部就班法,我想,頗有可能就是需求驅動而產生的,比如上圖刀削麪機器人,聽說如今已經第八代了!java

1. gelElementById太長了
頁面上有個按鈕,還有個圖片,我想點擊按鈕圖片隱藏,以下HTML:node

<button id="button">點擊我</button>
<img id="image" src="xxx.jpg">

因而,個人腳本可能就這樣:數組

var button = document.getElementById("button")
    , image = document.getElementById("image")

button.onclick = function() {
    image.style.display = "none";
};

有何問題?人幾乎都是天生的「懶惰者」,document.getElementById名稱長且重複出現,好像到了公司發現卡沒帶又回家從新拿卡的感受,我但願越簡單越好。恩, 我很喜歡錢,$這個符號我很喜歡,我打算改造一番,簡化個人工做:瀏覽器

var $ = function(id) {
    return document.getElementById(id);
};

$("button").onclick = function() {
    $("image").style.display = "none"; }; 

這裏的$()就是最簡單的包裝器,只是返回的是原生的DOM對象。框架

2. 我須要一個簡潔的暗號,就像「芝麻開門」
後來頁面複雜了,點擊一個按鈕,有2張圖片要隱藏。ide

$("button").onclick = function() {
    $("image1").style.display = "none";
    $("image2").style.display = "none";
};

好像又看見長長的重複的東西,xxx.style.display = "none", 爲何每次開門都要從包裏找到鑰匙、對準插口插進去、還要左扭扭右扭扭呢?一次還好,每天常常總是這樣怎受得了。設想,要是有個芝麻開門的暗號就行了,「open開」,聲音識別,門自動開了,多省心。函數

這裏每次隱藏都要xxx.style.display = "none", 比天天拿鑰匙開門還要煩,我但願有一個快捷的方式,例如,「hide隱」,語句識別,元素自動隱藏,多省心。

就是要變成下面的效果:

$("button").onclick = function() {
    $("image1").hide();
    $("image2").hide(); };

3. 如何識別「芝麻開門」的暗號
$("image1")本質是個DOM元素,$("image1").hide()也就是在DOM元素上擴展一個hide方法,調用即隱藏。

哦,擴展,立馬想到了JS中的prototype原型。//zxx: 老闆,如今滿大街什麼菜最便宜。老闆:原型啊,都氾濫了!

HTMLElement.prototype.hide = function() {
    this.style.display = "none";
};

上面代碼的demo地址應該不會被人看到吧……

雖然在身體上鑽了個窟窿插進入了一個方法,畢竟瀏覽器有有效果啊,切膚之痛就不算什麼了。可是,咱們是在泱泱天朝,不少IE6~IE8老頑固,這些老東西不認識HTMLElement,對於HTMLElement自殘擴展之法根本理解不了,而這些老傢伙掌管了半壁江山。唉,面對現實,元素直接擴展是行不通了。

所以,因爲兼容性,咱們須要想其餘擴展方法。

4. 條條大路通羅馬,此處不留爺,自有留爺處
雖IE6~IE8不識HTMLElement原型擴展,可是,Function的原型擴展其認識啊。管無論用,暫時不得而知,先隨便搞個簡單的試試唄~

var F = function() {};
F.prototype.hide = function() {
    this?.style.display = "none";
};

new F().hide();    // 這個實現隱藏?

本文至少還有一半的內容,可是,全文的最難點就在這裏的,對new F()的認識和理解。

上面的代碼,new F()您能夠看作是this?.style這裏的this. 您可能會跳起來搶答道:「那new F()return值 = DOM元素不就完事OK啦!—— this.style.hide = new F().style.hide = DOM.style.hide」!

很傻很天真

只要new表達式以後的constructor返回(return)一個引用對象(數組,對象,函數等),都將覆蓋new建立的匿名對象,若是返回(return)一個原始類型(無return時其實爲return原始類型undefined),那麼就返回new建立的匿名對象。

上面的引用來自這裏。什麼意思呢?說白了就是,new F()若是沒有返回值(Undefined類型),或返回值是5種基本型(Undefined類型、Null類型、Boolean類型、Number類型、String類型)之一,則new F()咱們能夠當作是原型擴展方法中的this; 若是返回是是數組啊、對象啊什麼的,則返回值就是這些對象自己,此時new F()this

舉例說明:

var F = function(id) {
    return document.getElementById(id);
};

new F("image1") == document.getElementById("image1");    // true 說明看上去返回DOM對象,實際確實就是DOM對象
var F = function(id) {
    return id;
};

new F("image1") == "image1";    // false 說明看上去返回字符串值,實際並非字符串

回到上面天真的想法。要想使用prototype.hide方法中的this, 偶們就不能讓F函數有亂七八糟的返回值。

所以,new F()直接返回DOM是不可取的,但咱們能夠藉助this間接調用。比方說:

var F = function(id) {
    this.element = document.getElementById(id);
};
F.prototype.hide = function() {
    this.element.style.display = "none"; }; new F("image").hide(); // 看你還不隱藏

上面代碼的demo地址應該不會被人看到吧……

5. 暴露與重用元素獲取方法
上面的方法,元素的獲取直接在F方法中,可是,實際狀況,考慮到兼容性實現,元素獲取可能會至關複雜,同時方法私有,不能重利用。所以,能夠把元素獲取方法放在原型上,便於管理和重用。代碼以下:

var F = function(id) {
    return this.getElementById(id);
};
F.prototype.getElementById = function(id) {
    this.element = document.getElementById(id);
    return this; }; F.prototype.hide = function() { this.element.style.display = "none"; }; new F("image").hide(); // 看你還不隱藏

元素獲取方法放在prototype上,經過F()執行。你可能會奇怪了,你剛明明說「new F()直接返回DOM是不可取的」,怎麼如今又有return呢?你們務必擦亮眼睛,F.prototype.getElementById的返回值是this,也就是new F()的返回值是this. 形象點就是new F("image")出了一拳,又反彈到本身臉上了。

上面代碼的demo地址應該不會被人看到吧……

6. 我不喜歡new, 我喜歡$
new F("image")這種寫法我好不喜歡,我喜歡$, 我就是喜歡$, 我要換掉。

好吧,把new什麼什麼藏在$方法中把~

var $ = function(id) {
    return new F(id);
};

因而,上面的圖片隱藏的直接執行代碼就是:

$("image").hide();

上面代碼的demo地址應該不會被人看到吧……

IE6瀏覽器也是支持的哦!是否是已經有些jQuery的樣子啦!

7. 你怎麼就一種姿式啊,人家都膩了誒
按部就班到如今,都是拿id來舉例的,實際應用,咱們可能要使用類名啊,標籤名啊什麼的,如今,爲了接下來的繼續,有必要支持多個「姿式」。

在IE8+瀏覽器中,咱們有選擇器API,document.querySelectordocument.querySelectorAll,前者返回惟一Node,後者爲NodeList集合。大統一塊兒見,咱們使用後者。因而,就有:

var F = function(selector, context) {
    return this.getNodeList(selector, context);
};
F.prototype.getNodeList = function(selector, context) {
    context = context || document;
    this.element = context.querySelectorAll(selector);
    return this;
};

var $ = function(selector, context) {
    return new F(selector, context);
};

此時,咱們就可使用各類選擇器了,例如,$("body #image"), this.element就是選擇的元素們。

8. IE6/IE7腫麼辦?
IE6/IE7不認識querySelectorAll,咋辦?
怎麼辦?

jQuery就使用了一個比較強大的選擇器框架-Sizzle. 知道就好,重在演示原理,所以,下面仍是使用原生的選擇器API示意,故demo效果須要IE8+瀏覽器下查看。

8. 遍歷是個麻煩事
this.element此時類型是NodeList, 所以,直接this.element.style.xxx的作法必定是報錯,看來有必要循環下:

F.prototype.hide = function() {
    var i=0, length = this.element.length;
    for (; i<length; i+=1) {
        this.element[i].style.display = "none";
    }    
};

因而乎:

$("img").hide();  // 頁面全部圖片都隱藏啦!

上面代碼的demo地址應該不會被人看到吧……

單純一個hide方法還能夠應付,再來個show方法,豈不是還要循環遍歷一次,豈不是要煩死~

所以,急需一個遍歷包裝器元素的方法,姑且叫作each吧~

因而有:

F.prototype.each = function(fn) {
    var i=0, length = this.element.length;
    for (; i<length; i+=1) {
        fn.call(this.element[i], i, this.element[i]);
    }
    return this;
};
F.prototype.hide = function() {
    this.each(function() { this.style.display = "none"; }); }; $("img").hide(); // 頁面全部圖片都隱藏啦!

上面代碼的demo地址應該不會被人看到吧……

9. 我不喜歡this.element, 能夠去掉嗎?
如今包裝器對象結構相似這樣:

F.prototype = {
    element: [NodeList],
    each: function() {},
    hide: function() {}
}

element看上去好礙眼,就不能去掉嗎?能夠啊,寶貝,NodeList是個類數組結構,咱們把它以數值索引形式分配到對象中就好啦!一來去除冗餘element屬性,二來讓原型對象成爲類數組結構,能夠有一些特殊的功能。

因而,F.prototype.getNodeList須要換一個名字了,比方說初始化init, 因而有:

F.prototype.init = function(selector, context) {
    var nodeList = (context || document).querySelectorAll(selector);
    this.length = nodeList.length; for (var i=0; i<this.length; i+=1) { this[i] = nodeList[i]; } return this; };

此時,each方法中,就沒有煩人礙眼的this.element[i]出現了,而是直接的this[i].

F.prototype.each = function(fn) {
    var i=0, length = this.length;
    for (; i<length; i+=1) {
        fn.call(this[i], i, this[i]); } return this; };

咱們也能夠直接使用索引訪問包裝器中的DOM元素。例如:$("img")[0]就是第一張圖片啦!

上面代碼的demo地址應該不會被人看到吧……

10. 我是完美主義者,我特不喜歡F名稱,能夠換掉嗎?
F這個名稱從頭至尾出現,我好不喜歡的來,我要換成$, 我就是要換成$符號……

這個……$已經用了啊,再用衝突的吧。再說,你又不是狐後,耍無賴也沒用啊……

狐後耍無賴

御姐發飆了

好吧,想一想其餘辦法吧。一步一步來,那我把全部的F換成$.fn.

就有:
全部的F換成$.fn以後~

上圖代碼的demo地址應該不會被人看到吧……

顯然,運行是OK的。彷佛也很是有jQuery的模樣了,可是,實際上,跟jQuery比仍是有差異的,有個較大的差異。若是是上圖代碼所示的JS結構,則包裝器對象要擴展新方法,每一個都須要再寫一個原型的。例如,擴展一個attr方法,則要寫成:

$.fn.prototype.attr = function() {
    // ...
};

又看到prototype了,高級的東西應該要隱藏住,不然會給人難以上手的感受。那該怎麼辦呢?御姐不是好惹的。

腦子動一下就知道了,把F.prototype換成$.fn不久好了。這樣,擴展新方法的時候,直接就是

$.fn.attr = function() {
    // ...
};
 

至此,就使用上講,與jQuery很是接近了。 可是,還有幾個F怎麼辦呢,總不能就像下面這樣放着吧:

var $ = function(selector, context) {
    return new F(selector, context);
};
var F = function(selector, context) {
    return this.init(selector, context);
};

$.fn = F.prototype;

$.fn.init = function(selector, context) {
    // ...
    return this;
};
$.fn.each = function(fn) {
   // ... }; $.fn.hide = function() { // ... };

數學中,咱們都學過合併同類項。仔細觀察上面的的代碼:
$()返回的是new F(),而new F()又是返回的對象的引用。擦,這返回來返回去的,參數又是同樣的,咱們是否是能夠一次性返回,而後再作些手腳,讓$.fn.init返回的this依然可以正確指向。

因而,一番調整有:

var $ = function(selector, context) {
    return new $.fn.init(selector, context);
};
var F = function() { };

$.fn = F.prototype;
$.fn.init = function(selector, context) {
    // ...
    return this;
};

// ...

上面代碼顯然是有問題的,new的是$.fn.init, $.fn.init的返回值是this. 也就是$()的返回值是$.fn.init的原型對象,尼瑪$.fn.initprototype原型如今就是個光桿司令啊,喲,正好,$.fn對應的原型方法,除了init沒用外,其餘hide(), each()就是咱們須要的。所以,咱們須要加上這麼一行:

$.fn.init.prototype = $.fn

因而,$()的返回值從$.fn.init.prototype一會兒變成$.fn,正好就是咱們一開始的擴展方法。

因而乎,大功告成。慢着……
慢着……

上面明明還有殘留的F呢!

哦,那個啊。F是任意函數,$自己就是函數,所以,直接使用$替換就能夠了:

var $ = function(selector, context) {
    return new $.fn.init(selector, context);
};
var F = function() { };   // 這個直接刪除
$.fn = $.prototype;
$.fn.init = function(selector, context) {
    // ... return this; }; // ...

上圖代碼的demo地址應該不會被人看到吧……

實際上,若是你不是非得一個$行便天下的話,到了上面進階第9步就足夠了。jQuery在第10步的處理是爲了彰顯其$用得如此的出神入化,代碼完美,使人驚歎!

至此,jQuery大核心已經一步一步走完了,能夠看到,全部的這些進階都是根據需求、實際開發須要來的,慢慢完善,慢慢擴充的!

11. 每一個擴展方法都要$.fn.xxxx, 好鬧心的來

$.fn.css = function() {}
$.fn.attr = function() {}
$.fn.data = function() {}
// ...

每一個擴展前面都有個$.fn, 好討厭的感受,就不能合併嗎?

因而,jQuery搞了個extend方法。

$.fn.extend({
    css: function() {},
    attr: function() {},
    data: function() {},
    // ...
});

12. $()不只能夠是選擇器字符串,還能夠是DOM
init方法中,判斷第一個參數,若是是節點,直接this[0] = this_node. over!

如下13~?都是完善啊,補充啊,兼容性處理啊什麼的,沒有價值,到此爲止!

3、排了很長隊的結束語

網上也有其餘一些介紹jQuery原理或機制的文章,可能當事人本身理解,而閱讀者原本就不懂,說來講去,越說越繞,可能更不懂了。

jQuery是很優秀,比如身爲靈長類的人類。可是,其誕生顯然是從簡單開始的。所以,要了解人類,能夠經過追溯其起源。若是你是上帝,要讓你造一我的,你會怎麼造,是一口氣出來?女媧造人還要捏泥人呢!不妨從單細胞生物開始,隨着天然進化,淘汰,天然而然,就會出現人類,上帝他就是這麼幹的。

jQuery的誕生也大體如此,要想了解jQuery,能夠試試踏着本文jQuery的成長足跡,一點一點逐步深刻,您就會了解爲什麼jQuery要這麼設計,它是如何設計的等。

雖然,內容由淺及深,可是,其中涉及的原型以及new構造函數的一些特性,對於新人而言,仍是有一些理解門檻的,但願個人描述與解釋可讓你有一絲豁然開朗,那就再好不過了。

感謝您的閱讀至此,歡迎指出文章可能書寫不許確的地方,再次感謝!

月底在百姓網有個小分享,演示文檔連個肉渣子還沒準備呢。所以,將來一週休文。

相關文章
相關標籤/搜索