初步學習 jQuery 核心 API

背景

不造輪子的程序員不是好程序員,因此咱們今天嘗試造一下輪子。今天的主角是 jQuery ,雖然如今市面上已被 ReactAngularVue 等擠的容不下它的位置,可是它的簡單 API 設計依然優秀,值得學習和體會。html

任務jquery

今天造輪子的目標不是實現功能,而是專一在 API 和架構。你須要完成的東西支持如下功能:git

一、$(selector) 根據選擇器構造一個jQuery 對象程序員

二、jQuery 對象是一個類數組,須要支持如下方法:es6

var a = $(selector);
a[0]                                   訪問元素
a.length                               元素個數
a.each(function(){ console.log(this)}) 迭代操做

三、鏈式調用github

var a = $(selector);
a.addClass('hello').click(function(){...});

四、擴展實例方法數組

$.fn.tabs = function(){
  console.log(this);
};

以後就能夠這樣使用瀏覽器

$(selector).tabs();

好,開始咱們的任務。架構

我在 jQuery 的官網下載的開發版(沒有壓縮)代碼,版本 3.2.1我記的上一次用的時候好像才 1.8左右 ?。只有一個 js 文件,打開一看,個人天,一萬多行代碼。。。模塊化

代碼有點多,咱們先梳理一下結構,找個入口開始看。

jQuery 的總體架構

( function( global, factory ) {
  //省略...
} )( typeof window !== "undefined" ? window : this,
  function( window, noGlobal ) {
    jQuery = function( selector, context ) {
      return new jQuery.fn.init( selector, context );
      //這裏用new,省去了構造函數 jQuery() 前面的運算符new,所以咱們能夠直接寫 jQuery()
    };
    jQuery.fn = jQuery.prototype = {
      jquery: version,
      constructor: jQuery,
      ...
    };
    // 經過覆蓋原型的方式,把 jQuery.prototype 覆蓋到 jQuery.fn.init.prototype 上
    jQuery.fn.init.prototype = jQuery.fn;
      //...
    jQuery.extend = jQuery.fn.extend = function(){
    ....//
    };
    jQuery.extend( {
      isFunction,
      type,
      isWindow,
      ...
    })
    //jQuery.extend()和jQuery.fn.extend()
    //用於合併多個對象的屬性到第一個對象,相似於 es6 的 Object.assign(),不過仍是有區別的
      if ( !noGlobal ) {
        window.jQuery = window.$ = jQuery;
      }
      return jQuery;
  }));

源碼分析

當即調用表達式

jQuery 當即調用表達式簡化版

(function(window, factory) {
    factory(window)
}(this, function() {
    return function() {
       //jQuery的調用
    }
}))

一上來,是個 當即調用表達式。 解決命名空間與變量污染的問題,全局變量是魔鬼, 匿名函數能夠有效的保證在頁面上寫入 JavaScript,而不會形成全局變量的污染,經過小括號,讓其加載的時候當即初始化,這樣就造成了一個單例模式的效果從而只會執行一次。

jQuery 的這個當即調用表達式的具體講解能夠參考這裏

jQuery = function( selector, context ) {
   return new jQuery.fn.init( selector, context );
}

//...

window.jQuery = window.$ = jQuery;

jQuery 賦值給了 window.jQuerywindow.$ 因此咱們在使用 jQuery 的時候 $jQuery 是等價的。

類數組對象

可是 jQuery() 返回了 new jQuery.fn.init(),爲何這樣寫?一臉懵逼。。。。

圖片描述

悲傷先放一邊,咱們先看一下這個函數 jQuery.fn.init(selector, context)

init = jQuery.fn.init = function( selector, context, root ) {
        // HANDLE: $(""), $(null), $(undefined), $(false)
        // Handle HTML strings
        // HANDLE: $(html) -> $(array)
        // HANDLE: $(html, props)
        // HANDLE: $(#id)
        // HANDLE: $(expr, $(...))
        // HANDLE: $(expr, context)
        // HANDLE: $(DOMElement)
        // HANDLE: $(function)
        return jQuery.makeArray( selector, this );
    };
init.prototype = jQuery.fn;

這個函數就是對參數 selector 對應的 htmlidclass 等不一樣選擇器的處理方式,並返回一個類數組對象。

看到這咱們就能實現咱們今天任務第一個目標以及第二個目標的 1/2 了。? 上代碼!

var jQuery = function(selector) {
  return new jQuery.fn.init(selector);
}

init = jQuery.fn.init = function( selector ) {
  var elem = document.querySelectorAll(selector);
  this.length = elem.length;
  this[0] = elem[0];
  for (i = 0; i < elem.length; i++) {
    this[i] = elem[i];
  }
  this.context = document;
  this.selector = selector;
  return this;
}

這裏有一個 jQuery 的特色 類數組對象結構。

所謂的類數組對象:

擁有一個 length 屬性和若干索引屬性的對象

舉個例子:

var array = ['name', 'age', 'sex'];

var arrayLike = {
    0: 'name',
    1: 'age',
    2: 'sex',
    length: 3
}

jQuery 能像數組同樣操做,經過對象 get 方法或者直接經過下標 0 索引就能轉成 DOM 對象。同時還擁有各類自定義方法,自定義屬性,看 jquery 對象的優雅的訪問方式便可知是如此美妙的對象。

進一步瞭解類數組對象能夠看這篇文章

對象的構建和分離構造器

而後咱們回來看看,讓咱們悲傷的代碼。。。

jQuery = function( selector, context ) {
   return new jQuery.fn.init( selector, context );
}

之因此這樣寫,演變過程是這樣的:

一、出於實例化 jQuery 對象性能的考慮 jQuery 採用了原型式的結構構建對象 
(jQuery.prototype)

                                  ⬇

二、jQuery 爲了初始化對象實例更方便,採用了無 new 化,初始化對象時,能夠不寫 new 操做符
(return new jQuery...)

                                  ⬇

三、jQuery 爲了不出現 return jQuery 無限遞歸本身,這種死循環的問題,採起的手段是把原型上的一個 init 方法做爲構造器

                                  ⬇

四、最後,就成了這樣了。
return new jQuery.fn.init()

這樣確實解決了循環遞歸的問題,可是又問題來了,init 是 jQuery 原型上做爲構造器的一個方法,那麼其 this 就不是 jQuery了,因此 this 就徹底引用不到 jQuery 的原型了,因此這裏經過 new 把 init 方法與 jQuery 給分離成2個獨立的構造器。

而後 jQuery 又經過下面的語句,將兩個獨立的構造器關聯起來了。

jQuery.fn = jQuery.prototype;
jQuery.fn.init.prototype = jQuery.fn;

這樣整個結構就串起來了,不得不佩服做者的設計思路,別具匠心。

上面說的若是沒看懂,能夠參考這兩篇文章:

jQuery 源碼解析 - 對象的構建

jQuery 源碼解析 - 分離構造器

靜態與實例方法共享設計

咱們要實現目標2中的 each 迭代操做,就要說一下 jQuery 的另外一個特性 靜態與實例方法共享

$(".box").each()   //做爲實例方法存在 遍歷一個jQuery對象的,是爲jQuery內部服務的
$.each()           //做爲靜態方法存在 能夠迭代任何集合

咱們要寫兩個方法嘛?看看 jQuery 怎麼作的?

jQuery.prototype = {
    each: function( callback, args ) {
        return jQuery.each( this, callback, args );
    }
}

實例方法取於靜態方法,這裏是靜態與實例方法共享設計,靜態方法掛在jQuery構造器上,原型方法通過下面的兩句代碼就掛載到 init 的原型上了,也就是對象的實例方法上了。

jQuery.fn = jQuery.prototype;
jQuery.fn.init.prototype = jQuery.fn;

那麼剩下的問題就是怎麼實現靜態方法 jQuery.each

這個靜態方法是在

jQuery.extend({
   each: function( obj, callback ) {
     var length, i = 0;
        if ( isArrayLike( obj ) ) {
            length = obj.length;
            for ( ; i < length; i++ ) {
                if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
                    break;
                }
            }
        } else {
            for ( i in obj ) {
                if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
                    break;
                }
            }
        }
        return obj;
    }
})

咱們實現 each,代碼以下:

var jQuery = function(selector) {
  return new jQuery.fn.init(selector);
}

jQuery.fn = jQuery.prototype = {
  constructor: jQuery,
  length:0,
  get: function( num ) {
    return this[ num ];
  },
  each: function( callback ) {
    return jQuery.each( this, callback );
  }
}

init = jQuery.fn.init = function( selector ) {
  var elem = document.querySelectorAll(selector);
  this.length = elem.length;
  this[0] = elem[0];
  for (i = 0; i < elem.length; i++) {
    this[i] = elem[i];
  }
  this.context = document;
  this.selector = selector;
  return this;
}

init.prototype = jQuery.fn;


jQuery.extend = jQuery.fn.extend = function() {
  var options, copy,
      target = arguments[0] || {},
      i = 1,
      length = arguments.length;

  //只有一個參數,就是對jQuery自身的擴展處理
  //extend,fn.extend
  if (i === length) {
      target = this; //調用的上下文對象jQuery/或者實例
      i--;
  }
  for (; i < length; i++) {
      //從i開始取參數,不爲空開始遍歷
      if ((options = arguments[i]) != null) {
          for (name in options) {
              copy = options[name];
              //覆蓋拷貝
              target[name] = copy;
          }
      }
  }
  return target;
}

jQuery.extend( {
  each: function( obj, callback ) {
    var length, i = 0;
    for ( i in obj ) {
      if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
        break;
      }
    }
    return obj;
  }
});

插件接口的設計

既然出現 extend 了,咱們就先實現第四個小目標 擴展實例方法 tabs

jQuery 中

jQuery.extend = jQuery.fn.extend = function() {
}

雖然指向了同一個函數,可是它們的 this 指向是不一樣。

fn 與 jQuery 實際上是2個不一樣的對象,在以前有講解:jQuery.extend 調用的時候,this是指向 jQuery 對象的( jQuery 是函數,也是對象!),因此這裏擴展在 jQuery 上。而jQuery.fn.extend 調用的時候,this 指向 fn 對象,jQuery.fn 和 jQuery.prototype指向同一對象,擴展 fn 就是擴展 jQuery.prototype 原型對象。這裏增長的是原型方法,也就是對象方法了。因此jQuery的API中提供了以上2個擴展函數。

咱們這樣擴展實例方法便可。

jQuery.fn.extend({
  tabs: function() {
    console.log('擴展實例方法:tabs');
  }
});

jQuery 抽出了全部可複用的特性,分離出單一模塊,經過組合的用法,無論在設計思路與實現手法上 jQuery 都是很是高明的。由於 jQuery 的設計中最喜歡的作的一件事,就是抽出共同的特性使之模塊化,固然也是更貼近 S.O.L.I.D 五大原則的單一職責SRP了,遵照單一職責的好處是可讓咱們很容易地來維護這個對象,好比,當一個對象封裝了不少職責的時候,一旦一個職責須要修改,勢必會影響該對象的其它職責代碼。經過解耦可讓每一個職責更加有彈性地變化。

方法鏈式調用的實現

經過簡單擴展原型方法並經過 return this 的形式來實現跨瀏覽器的鏈式調用。
因此咱們若是須要鏈式的處理,只須要在方法內部返回當前的這個實例對象 this 就能夠了,由於返回當前實例的 this,從而又能夠訪問本身的原型了,這樣的就節省代碼量,提升代碼的效率,代碼看起來更優雅。

詳細解說請點擊 方法鏈式調用的實現

最終的代碼演示

參考

文章的不少內容參考的慕課網jQuery源碼解析 系列,感興趣的小夥伴,能夠看一下整個系列。

相關文章
相關標籤/搜索