學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫

前言

你好,我是若川。這是學習源碼總體架構第一篇。總體架構這詞語好像有點大,姑且就算是源碼總體結構吧,主要就是學習是代碼總體結構,不深究其餘不是主線的具體函數的實現。本篇文章學習的是打包整合後的代碼,不是實際倉庫中的拆分的代碼。html

學習源碼總體架構系列文章以下:前端

1.學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫
2.學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫
3.學習 lodash 源碼總體架構,打造屬於本身的函數式編程類庫
4.學習 sentry 源碼總體架構,打造屬於本身的前端異常監控SDK
5.學習 vuex 源碼總體架構,打造屬於本身的狀態管理庫
6.學習 axios 源碼總體架構,打造屬於本身的請求庫
7.學習 koa 源碼的總體架構,淺析koa洋蔥模型原理和co原理
8.學習 redux 源碼總體架構,深刻理解 redux 及其中間件原理vue

感興趣的讀者能夠點擊閱讀。node

雖然如今基本不怎麼使用jQuery了,但jQuery流行10多年JS庫,仍是有必要學習它的源碼的。也能夠學着打造屬於本身的js類庫,求職面試時能夠增色很多。jquery

本文章學習的是v3.4.1版本。 unpkg.com源碼地址:https://unpkg.com/jquery@3.4.1/dist/jquery.jslinux

jQuery github倉庫ios

自執行匿名函數

(function(global, factory){
 })(typeof window !== "underfined" ? window: this, function(window, noGlobal){  }); 複製代碼

外界訪問不到裏面的變量和函數,裏面能夠訪問到外界的變量,但裏面定義了本身的變量,則不會訪問外界的變量。 匿名函數將代碼包裹在裏面,防止與其餘代碼衝突和污染全局環境。 關於自執行函數不是很瞭解的讀者能夠參看這篇文章。 [譯] JavaScript:當即執行函數表達式(IIFE)git

瀏覽器環境下,最後把$jQuery函數掛載到window上,因此在外界就能夠訪問到$jQuery了。github

if ( !noGlobal ) {
 window.jQuery = window.$ = jQuery; } // 其中`noGlobal`參數只有在這裏用到。 複製代碼

支持多種環境下使用 好比 commonjs、amd規範

commonjs 規範支持

commonjs實現 主要表明 nodejs面試

// global是全局變量,factory 是函數
( function( global, factory ) {   // 使用嚴格模式  "use strict";  // Commonjs 或者 CommonJS-like 環境  if ( typeof module === "object" && typeof module.exports === "object" ) {  // 若是存在global.document 則返回factory(global, true);  module.exports = global.document ?  factory( global, true ) :  function( w ) {  if ( !w.document ) {  throw new Error( "jQuery requires a window with a document" );  }  return factory( w );  };  } else {  factory( global );  }  // Pass this if window is not defined yet // 第一個參數判斷window,存在返回window,不存在返回this } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {}); 複製代碼

amd 規範 主要表明 requirejs

if ( typeof define === "function" && define.amd ) {
 define( "jquery", [], function() {  return jQuery;  } ); } 複製代碼

cmd 規範 主要表明 seajs

很遺憾,jQuery源碼裏沒有暴露對seajs的支持。但網上也有一些方案。這裏就不具體提了。畢竟如今基本不用seajs了。

無 new 構造

實際上也是能夠 new的,由於jQuery是函數。並且和不用new效果是同樣的。 new顯示返回對象,因此和直接調用jQuery函數做用效果是同樣的。 若是對new操做符具體作了什麼不明白。能夠參看我以前寫的文章。

面試官問:可否模擬實現JS的new操做符

源碼:

var
 version = "3.4.1",   // Define a local copy of jQuery  jQuery = function( selector, context ) {  // 返回new以後的對象  return new jQuery.fn.init( selector, context );  }; jQuery.fn = jQuery.prototype = {  // jQuery當前版本  jquery: version,  // 修正構造器爲jQuery  constructor: jQuery,  length: 0, }; init = jQuery.fn.init = function( selector, context, root ) {  // ...  if ( !selector ) {  return this;  }  // ... }; init.prototype = jQuery.fn; 複製代碼
jQuery.fn === jQuery.prototype;  // true
init = jQuery.fn.init; init.prototype = jQuery.fn; // 也就是 jQuery.fn.init.prototype === jQuery.fn; // true jQuery.fn.init.prototype === jQuery.prototype; // true 複製代碼

關於這個筆者畫了一張jQuery原型關係圖,所謂一圖勝千言。 jQuery-v3.4.1原型關係圖

<sciprt src="https://unpkg.com/jquery@3.4.1/dist/jquery.js">
</script> console.log({jQuery}); // 在谷歌瀏覽器控制檯,能夠看到jQuery函數下掛載了不少靜態屬性和方法,在jQuery.fn 上也掛着不少屬性和方法。 複製代碼

Vue源碼中,也跟jQuery相似,執行的是Vue.prototype._init方法。

function Vue (options) {
 if (!(this instanceof Vue)  ) {  warn('Vue is a constructor and should be called with the `new` keyword');  }  this._init(options); } initMixin(Vue); function initMixin (Vue) {  Vue.prototype._init = function (options) {}; }; 複製代碼

核心函數之一 extend

用法:

jQuery.extend( target [, object1 ] [, objectN ] )        Returns: Object
 jQuery.extend( [deep ], target, object1 [, objectN ] ) 複製代碼

jQuery.extend API jQuery.fn.extend API

看幾個例子: (例子能夠我放到在線編輯代碼的jQuery.extend例子codepen了,能夠直接運行)。

// 1. jQuery.extend( target)
var result1 = $.extend({  job: '前端開發工程師', });  console.log(result1, 'result1', result1.job); // $函數 加了一個屬性 job // 前端開發工程師  // 2. jQuery.extend( target, object1) var result2 = $.extend({  name: '若川', }, {  job: '前端開發工程師', });  console.log(result2, 'result2'); // { name: '若川', job: '前端開發工程師' }  // deep 深拷貝 // 3. jQuery.extend( [deep ], target, object1 [, objectN ] ) var result3 = $.extend(true, {  name: '若川',  other: {  mac: 0,  ubuntu: 1,  windows: 1,  }, }, {  job: '前端開發工程師',  other: {  mac: 1,  linux: 1,  windows: 0,  } }); console.log(result3, 'result3'); // deep true // { // "name": "若川", // "other": { // "mac": 1, // "ubuntu": 1, // "windows": 0, // "linux": 1 // }, // "job": "前端開發工程師" // } // deep false // { // "name": "若川", // "other": { // "mac": 1, // "linux": 1, // "windows": 0 // }, // "job": "前端開發工程師" // } 複製代碼

結論:extend函數既能夠實現給jQuery函數能夠實現淺拷貝、也能夠實現深拷貝。能夠給jQuery上添加靜態方法和屬性,也能夠像jQuery.fn(也就是jQuery.prototype)上添加屬性和方法,這個功能歸功於thisjQuery.extend調用時this指向是jQueryjQuery.fn.extend調用時this指向則是jQuery.fn

淺拷貝實現

知道這些,其實實現淺拷貝仍是比較容易的:

// 淺拷貝實現
jQuery.extend = function(){  // options 是擴展的對象object1,object2...  var options,  // object對象上的鍵  name,  // copy object對象上的值,也就是是須要拷貝的值  copy,  // 擴展目標對象,可能不是對象,因此或空對象  target = arguments[0] || {},  // 定義i爲1  i = 1,  // 定義實參個數length  length = arguments.length;  // 只有一個參數時  if(i === length){  target = this;  i--;  }  for(; i < length; i++){  // 不是underfined 也不是null  if((options = arguments[i]) != null){  for(name in options){  copy = options[name];  // 防止死循環,continue 跳出當前這次循環  if ( name === "__proto__" || target === copy ) {  continue;  }  if ( copy !== undefined ) {  target[ name ] = copy;  }  }  }   }  // 最後返回目標對象  return target; } 複製代碼

深拷貝則主要是在如下這段代碼作判斷。多是數組和對象引用類型的值,作判斷。

if ( copy !== undefined ) {
 target[ name ] = copy; } 複製代碼

爲了方便讀者調試,代碼一樣放在jQuery.extend淺拷貝代碼實現codepen,可在線運行。

深拷貝實現

$.extend = function(){
 // options 是擴展的對象object1,object2...  var options,  // object對象上的鍵  name,  // copy object對象上的值,也就是是須要拷貝的值  copy,  // 深拷貝新增的四個變量 deep、src、copyIsArray、clone  deep = false,  // 源目標,須要往上面賦值的  src,  // 須要拷貝的值的類型是函數  copyIsArray,  //  clone,  // 擴展目標對象,可能不是對象,因此或空對象  target = arguments[0] || {},  // 定義i爲1  i = 1,  // 定義實參個數length  length = arguments.length;   // 處理深拷貝狀況  if ( typeof target === "boolean" ) {  deep = target;   // Skip the boolean and the target  // target目標對象開始後移  target = arguments[ i ] || {};  i++;  }   // Handle case when target is a string or something (possible in deep copy)  // target不等於對象,且target不是函數的狀況下,強制將其賦值爲空對象。  if ( typeof target !== "object" && !isFunction( target ) ) {  target = {};  }   // 只有一個參數時  if(i === length){  target = this;  i--;  }  for(; i < length; i++){  // 不是underfined 也不是null  if((options = arguments[i]) != null){  for(name in options){  copy = options[name];  // 防止死循環,continue 跳出當前這次循環  if ( name === "__proto__" || target === copy ) {  continue;  }   // Recurse if we're merging plain objects or arrays  // 這裏deep爲true,而且須要拷貝的值有值,而且是純粹的對象  // 或者需拷貝的值是數組  if ( deep && copy && ( jQuery.isPlainObject( copy ) ||  ( copyIsArray = Array.isArray( copy ) ) ) ) {   // 源目標,須要往上面賦值的  src = target[ name ];   // Ensure proper type for the source value  // 拷貝的值,而且src不是數組,clone對象改成空數組。  if ( copyIsArray && !Array.isArray( src ) ) {  clone = [];  // 拷貝的值不是數組,對象不是純粹的對象。  } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {  // clone 賦值爲空對象  clone = {};  } else {  // 不然 clone = src  clone = src;  }  // 把下一次循環時,copyIsArray 須要從新賦值爲false  copyIsArray = false;   // Never move original objects, clone them  // 遞歸調用本身  target[ name ] = jQuery.extend( deep, clone, copy );   // Don't bring in undefined values  }  else if ( copy !== undefined ) {  target[ name ] = copy;  }  }  }   }  // 最後返回目標對象  return target; }; 複製代碼

爲了方便讀者調試,這段代碼一樣放在jQuery.extend深拷貝代碼實現codepen,可在線運行。

深拷貝衍生的函數 isFunction

判斷參數是不是函數。

var isFunction = function isFunction( obj ) {
  // Support: Chrome <=57, Firefox <=52  // In some browsers, typeof returns "function" for HTML <object> elements  // (i.e., `typeof document.createElement( "object" ) === "function"`).  // We don't want to classify *any* DOM node as a function.  return typeof obj === "function" && typeof obj.nodeType !== "number"; }; 複製代碼

深拷貝衍生的函數 jQuery.isPlainObject

jQuery.isPlainObject(obj) 測試對象是不是純粹的對象(經過 "{}" 或者 "new Object" 建立的)。

jQuery.isPlainObject({}) // true
jQuery.isPlainObject("test") // false 複製代碼
var getProto = Object.getPrototypeOf;
var class2type = {}; var toString = class2type.toString; var hasOwn = class2type.hasOwnProperty; var fnToString = hasOwn.toString; var ObjectFunctionString = fnToString.call( Object );  jQuery.extend( {  isPlainObject: function( obj ) {  var proto, Ctor;   // Detect obvious negatives  // Use toString instead of jQuery.type to catch host objects  // !obj 爲true或者 不爲[object Object]  // 直接返回false  if ( !obj || toString.call( obj ) !== "[object Object]" ) {  return false;  }   proto = getProto( obj );   // Objects with no prototype (e.g., `Object.create( null )`) are plain  // 原型不存在 好比 Object.create(null) 直接返回 true;  if ( !proto ) {  return true;  }   // Objects with prototype are plain iff they were constructed by a global Object function  Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;  // 構造器是函數,而且 fnToString.call( Ctor ) === fnToString.call( Object );  return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;  }, }); 複製代碼

extend函數,也能夠本身刪掉寫一寫,算是jQuery中一個比較核心的函數了。並且用途普遍,能夠內部使用也能夠,外部使用擴展 插件等。

鏈式調用

jQuery可以鏈式調用是由於一些函數執行結束後 return this。 好比 jQuery 源碼中的addClassremoveClasstoggleClass

jQuery.fn.extend({
 addClass: function(){  // ...  return this;  },  removeClass: function(){  // ...  return this;  },  toggleClass: function(){  // ...  return this;  }, }); 複製代碼

jQuery.noConflict 不少js庫都會有的防衝突函數

jQuery.noConflict API

用法:

<script>
 var $ = '我是其餘的$,jQuery不要覆蓋我'; </script> <script src="./jquery-3.4.1.js"> </script> <script>  $.noConflict();  console.log($); // 我是其餘的$,jQuery不要覆蓋我 </script> 複製代碼

jQuery.noConflict 源碼

var
  // Map over jQuery in case of overwrite  _jQuery = window.jQuery,   // Map over the $ in case of overwrite  _$ = window.$;  jQuery.noConflict = function( deep ) {  // 若是已經存在$ === jQuery;  // 把已存在的_$賦值給window.$;  if ( window.$ === jQuery ) {  window.$ = _$;  }   // 若是deep爲 true, 而且已經存在jQuery === jQuery;  // 把已存在的_jQuery賦值給window.jQuery;  if ( deep && window.jQuery === jQuery ) {  window.jQuery = _jQuery;  }   // 最後返回jQuery  return jQuery; }; 複製代碼

總結

全文主要經過淺析了jQuery總體結構,自執行匿名函數、無new構造、支持多種規範(如commonjs、amd規範)、核心函數之extend、鏈式調用、jQuery.noConflict等方面。

從新梳理下文中學習的源碼結構。

// 源碼結構
( function( global, factory )  "use strict";  if ( typeof module === "object" && typeof module.exports === "object" ) {  module.exports = global.document ?  factory( global, true ) :  function( w ) {  if ( !w.document ) {  throw new Error( "jQuery requires a window with a document" );  }  return factory( w );  };  } else {  factory( global );  }  } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {  var version = "3.4.1",   // Define a local copy of jQuery  jQuery = function( selector, context ) {  return new jQuery.fn.init( selector, context );  };   jQuery.fn = jQuery.prototype = {  jquery: version,  constructor: jQuery,  length: 0,  // ...  };   jQuery.extend = jQuery.fn.extend = function() {};   jQuery.extend( {  // ...  isPlainObject: function( obj ) {},  // ...  });   init = jQuery.fn.init = function( selector, context, root ) {};   init.prototype = jQuery.fn;   if ( typeof define === "function" && define.amd ) {  define( "jquery", [], function() {  return jQuery;  } );  }  jQuery.noConflict = function( deep ) {};   if ( !noGlobal ) {  window.jQuery = window.$ = jQuery;  }   return jQuery; }); 複製代碼

能夠學習到jQuery巧妙的設計和架構,爲本身所用,打造屬於本身的js類庫。 相關代碼和資源放置在github blog中,須要的讀者能夠自取。

下一篇文章是學習underscorejs的源碼總體架構。學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫

讀者發現有不妥或可改善之處,歡迎評論指出。另外以爲寫得不錯,能夠點贊、評論、轉發,也是對筆者的一種支持。

筆者往期文章

面試官問:JS的繼承
面試官問:JS的this指向
面試官問:可否模擬實現JS的call和apply方法
面試官問:可否模擬實現JS的bind方法
面試官問:可否模擬實現JS的new操做符
前端使用puppeteer 爬蟲生成《React.js 小書》PDF併合並

擴展閱讀

chokcoco: jQuery- v1.10.2 源碼解讀
chokcoco:【深刻淺出jQuery】源碼淺析--總體架構
songjz :jQuery 源碼系列(一)整體架構

筆者另外一個系列

面試官問:JS的繼承
面試官問:JS的this指向
面試官問:可否模擬實現JS的call和apply方法
面試官問:可否模擬實現JS的bind方法
面試官問:可否模擬實現JS的new操做符

關於

做者:常以若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
若川的博客,使用vuepress重構了,閱讀體驗可能更好些
掘金專欄,歡迎關注~
segmentfault前端視野專欄,歡迎關注~
語雀前端視野專欄,新增語雀專欄,歡迎關注~
知乎前端視野專欄,歡迎關注~
github blog,相關源碼和資源都放在這裏,求個star^_^~

歡迎加微信交流 微信公衆號

可能比較有趣的微信公衆號,長按掃碼關注。歡迎加筆者微信ruochuan12(註明來源,基原本者不拒),拉您進【前端視野交流羣】,長期交流學習~

若川視野
若川視野

本文使用 mdnice 排版

相關文章
相關標籤/搜索