查看原文
其他

学习 jQuery 源码整体架构,打造属于自己的 js 类库

若川视野 若川视野 2022-05-01

虽然现在基本不怎么使用 jQuery了,但 jQuery流行 10多年JS库,还是有必要学习它的源码的。也可以学着打造属于自己的 js类库,求职面试时可以增色不少。

本文章学习的是 v3.4.1版本。unpkg.com源码地址:https://unpkg.com/jquery@3.4.1/dist/jquery.js

jQuery github仓库

自执行匿名函数

  1. (function(global, factory){


  2. })(typeof window !== "underfined" ? window: this, function(window, noGlobal){


  3. });

外界访问不到里面的变量和函数,里面可以访问到外界的变量,但里面定义了自己的变量,则不会访问外界的变量。匿名函数将代码包裹在里面,防止与其他代码冲突和污染全局环境。关于自执行函数不是很了解的读者可以参看这篇文章。[译] JavaScript:立即执行函数表达式(IIFE)

浏览器环境下,最后把 $jQuery函数挂载到 window上,所以在外界就可以访问到 $jQuery了。

  1. if ( !noGlobal ) {

  2. window.jQuery = window.$ = jQuery;

  3. }

  4. // 其中`noGlobal`参数只有在这里用到。

支持多种环境下使用 比如 commonjs、amd规范

commonjs 规范支持

commonjs实现 主要代表 nodejs

  1. // global是全局变量,factory 是函数

  2. ( function( global, factory ) {


  3. // 使用严格模式

  4. "use strict";

  5. // Commonjs 或者 CommonJS-like 环境

  6. if ( typeof module === "object" && typeof module.exports === "object" ) {

  7. // 如果存在global.document 则返回factory(global, true);

  8. module.exports = global.document ?

  9. factory( global, true ) :

  10. function( w ) {

  11. if ( !w.document ) {

  12. throw new Error( "jQuery requires a window with a document" );

  13. }

  14. return factory( w );

  15. };

  16. } else {

  17. factory( global );

  18. }


  19. // Pass this if window is not defined yet

  20. // 第一个参数判断window,存在返回window,不存在返回this

  21. } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {});

amd 规范 主要代表 requirejs

  1. if ( typeof define === "function" && define.amd ) {

  2. define( "jquery", [], function() {

  3. return jQuery;

  4. } );

  5. }

cmd 规范 主要代表 seajs

很遗憾, jQuery源码里没有暴露对 seajs的支持。但网上也有一些方案。这里就不具体提了。毕竟现在基本不用 seajs了。

无 new 构造

实际上也是可以 new的,因为 jQuery是函数。而且和不用 new效果是一样的。new显示返回对象,所以和直接调用 jQuery函数作用效果是一样的。如果对 new操作符具体做了什么不明白。可以参看我之前写的文章。

面试官问:能否模拟实现JS的new操作符

源码:

  1. var

  2. version = "3.4.1",


  3. // Define a local copy of jQuery

  4. jQuery = function( selector, context ) {

  5. // 返回new之后的对象

  6. return new jQuery.fn.init( selector, context );

  7. };

  8. jQuery.fn = jQuery.prototype = {

  9. // jQuery当前版本

  10. jquery: version,

  11. // 修正构造器为jQuery

  12. constructor: jQuery,

  13. length: 0,

  14. };

  15. init = jQuery.fn.init = function( selector, context, root ) {

  16. // ...

  17. if ( !selector ) {

  18. return this;

  19. }

  20. // ...

  21. };

  22. init.prototype = jQuery.fn;

  1. jQuery.fn === jQuery.prototype; // true

  2. init = jQuery.fn.init;

  3. init.prototype = jQuery.fn;

  4. // 也就是

  5. jQuery.fn.init.prototype === jQuery.fn; // true

  6. jQuery.fn.init.prototype === jQuery.prototype; // true

关于这个笔者画了一张 jQuery原型关系图,所谓一图胜千言。

jquery-v3.4.1 原型关系图

  1. <sciprt src="https://unpkg.com/jquery@3.4.1/dist/jquery.js">

  2. </script>

  3. console.log({jQuery});

  4. // 在谷歌浏览器控制台,可以看到jQuery函数下挂载了很多静态属性和方法,在jQuery.fn 上也挂着很多属性和方法。


Vue源码中,也跟 jQuery类似,执行的是 Vue.prototype._init方法。

  1. function Vue (options) {

  2. if (!(this instanceof Vue)

  3. ) {

  4. warn('Vue is a constructor and should be called with the `new` keyword');

  5. }

  6. this._init(options);

  7. }

  8. initMixin(Vue);

  9. function initMixin (Vue) {

  10. Vue.prototype._init = function (options) {};

  11. };

核心函数之一 extend

用法:

  1. jQuery.extend( target [, object1 ] [, objectN ] ) Returns: Object


  2. jQuery.extend( [deep ], target, object1 [, objectN ] )

jQuery.extend APIjQuery.fn.extend API

看几个例子:(例子可以我放到在线编辑代码的jQuery.extend例子codepen了,可以直接运行)。

  1. // 1. jQuery.extend( target)

  2. var result1 = $.extend({

  3. job: '前端开发工程师',

  4. });


  5. console.log(result1, 'result1', result1.job); // $函数 加了一个属性 job // 前端开发工程师


  6. // 2. jQuery.extend( target, object1)

  7. var result2 = $.extend({

  8. name: '若川',

  9. },

  10. {

  11. job: '前端开发工程师',

  12. });


  13. console.log(result2, 'result2'); // { name: '若川', job: '前端开发工程师' }


  14. // deep 深拷贝

  15. // 3. jQuery.extend( [deep ], target, object1 [, objectN ] )

  16. var result3 = $.extend(true, {

  17. name: '若川',

  18. other: {

  19. mac: 0,

  20. ubuntu: 1,

  21. windows: 1,

  22. },

  23. }, {

  24. job: '前端开发工程师',

  25. other: {

  26. mac: 1,

  27. linux: 1,

  28. windows: 0,

  29. }

  30. });

  31. console.log(result3, 'result3');

  32. // deep true

  33. // {

  34. // "name": "若川",

  35. // "other": {

  36. // "mac": 1,

  37. // "ubuntu": 1,

  38. // "windows": 0,

  39. // "linux": 1

  40. // },

  41. // "job": "前端开发工程师"

  42. // }

  43. // deep false

  44. // {

  45. // "name": "若川",

  46. // "other": {

  47. // "mac": 1,

  48. // "linux": 1,

  49. // "windows": 0

  50. // },

  51. // "job": "前端开发工程师"

  52. // }

结论:extend函数既可以实现给 jQuery函数可以实现浅拷贝、也可以实现深拷贝。可以给jQuery上添加静态方法和属性,也可以像 jQuery.fn(也就是 jQuery.prototype)上添加属性和方法,这个功能归功于 thisjQuery.extend调用时 this指向是 jQueryjQuery.fn.extend调用时 this指向则是 jQuery.fn

浅拷贝实现

知道这些,其实实现浅拷贝还是比较容易的:

  1. // 浅拷贝实现

  2. jQuery.extend = function(){

  3. // options 是扩展的对象object1,object2...

  4. var options,

  5. // object对象上的键

  6. name,

  7. // copy object对象上的值,也就是是需要拷贝的值

  8. copy,

  9. // 扩展目标对象,可能不是对象,所以或空对象

  10. target = arguments[0] || {},

  11. // 定义i为1

  12. i = 1,

  13. // 定义实参个数length

  14. length = arguments.length;

  15. // 只有一个参数时

  16. if(i === length){

  17. target = this;

  18. i--;

  19. }

  20. for(; i < length; i++){

  21. // 不是underfined 也不是null

  22. if((options = arguments[i]) != null){

  23. for(name in options){

  24. copy = options[name];

  25. // 防止死循环,continue 跳出当前此次循环

  26. if ( name === "__proto__" || target === copy ) {

  27. continue;

  28. }

  29. if ( copy !== undefined ) {

  30. target[ name ] = copy;

  31. }

  32. }

  33. }


  34. }

  35. // 最后返回目标对象

  36. return target;

  37. }

深拷贝则主要是在以下这段代码做判断。可能是数组和对象引用类型的值,做判断。

  1. if ( copy !== undefined ) {

  2. target[ name ] = copy;

  3. }

为了方便读者调试,代码同样放在jQuery.extend浅拷贝代码实现codepen,可在线运行。

深拷贝实现

  1. $.extend = function(){

  2. // options 是扩展的对象object1,object2...

  3. var options,

  4. // object对象上的键

  5. name,

  6. // copy object对象上的值,也就是是需要拷贝的值

  7. copy,

  8. // 深拷贝新增的四个变量 deep、src、copyIsArray、clone

  9. deep = false,

  10. // 源目标,需要往上面赋值的

  11. src,

  12. // 需要拷贝的值的类型是函数

  13. copyIsArray,

  14. //

  15. clone,

  16. // 扩展目标对象,可能不是对象,所以或空对象

  17. target = arguments[0] || {},

  18. // 定义i为1

  19. i = 1,

  20. // 定义实参个数length

  21. length = arguments.length;


  22. // 处理深拷贝情况

  23. if ( typeof target === "boolean" ) {

  24. deep = target;


  25. // Skip the boolean and the target

  26. // target目标对象开始后移

  27. target = arguments[ i ] || {};

  28. i++;

  29. }


  30. // Handle case when target is a string or something (possible in deep copy)

  31. // target不等于对象,且target不是函数的情况下,强制将其赋值为空对象。

  32. if ( typeof target !== "object" && !isFunction( target ) ) {

  33. target = {};

  34. }


  35. // 只有一个参数时

  36. if(i === length){

  37. target = this;

  38. i--;

  39. }

  40. for(; i < length; i++){

  41. // 不是underfined 也不是null

  42. if((options = arguments[i]) != null){

  43. for(name in options){

  44. copy = options[name];

  45. // 防止死循环,continue 跳出当前此次循环

  46. if ( name === "__proto__" || target === copy ) {

  47. continue;

  48. }


  49. // Recurse if we're merging plain objects or arrays

  50. // 这里deep为true,并且需要拷贝的值有值,并且是纯粹的对象

  51. // 或者需拷贝的值是数组

  52. if ( deep && copy && ( jQuery.isPlainObject( copy ) ||

  53. ( copyIsArray = Array.isArray( copy ) ) ) ) {


  54. // 源目标,需要往上面赋值的

  55. src = target[ name ];


  56. // Ensure proper type for the source value

  57. // 拷贝的值,并且src不是数组,clone对象改为空数组。

  58. if ( copyIsArray && !Array.isArray( src ) ) {

  59. clone = [];

  60. // 拷贝的值不是数组,对象不是纯粹的对象。

  61. } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {

  62. // clone 赋值为空对象

  63. clone = {};

  64. } else {

  65. // 否则 clone = src

  66. clone = src;

  67. }

  68. // 把下一次循环时,copyIsArray 需要重新赋值为false

  69. copyIsArray = false;


  70. // Never move original objects, clone them

  71. // 递归调用自己

  72. target[ name ] = jQuery.extend( deep, clone, copy );


  73. // Don't bring in undefined values

  74. }

  75. else if ( copy !== undefined ) {

  76. target[ name ] = copy;

  77. }

  78. }

  79. }


  80. }

  81. // 最后返回目标对象

  82. return target;

  83. };

为了方便读者调试,这段代码同样放在jQuery.extend深拷贝代码实现codepen,可在线运行。

深拷贝衍生的函数 isFunction

判断参数是否是函数。

  1. var isFunction = function isFunction( obj ) {


  2. // Support: Chrome <=57, Firefox <=52

  3. // In some browsers, typeof returns "function" for HTML <object> elements

  4. // (i.e., `typeof document.createElement( "object" ) === "function"`).

  5. // We don't want to classify *any* DOM node as a function.

  6. return typeof obj === "function" && typeof obj.nodeType !== "number";

  7. };

深拷贝衍生的函数 jQuery.isPlainObject

jQuery.isPlainObject(obj)测试对象是否是纯粹的对象(通过 "{}" 或者 "new Object" 创建的)。

  1. jQuery.isPlainObject({}) // true

  2. jQuery.isPlainObject("test") // false

  1. var getProto = Object.getPrototypeOf;

  2. var class2type = {};

  3. var toString = class2type.toString;

  4. var hasOwn = class2type.hasOwnProperty;

  5. var fnToString = hasOwn.toString;

  6. var ObjectFunctionString = fnToString.call( Object );


  7. jQuery.extend( {

  8. isPlainObject: function( obj ) {

  9. var proto, Ctor;


  10. // Detect obvious negatives

  11. // Use toString instead of jQuery.type to catch host objects

  12. // !obj 为true或者 不为[object Object]

  13. // 直接返回false

  14. if ( !obj || toString.call( obj ) !== "[object Object]" ) {

  15. return false;

  16. }


  17. proto = getProto( obj );


  18. // Objects with no prototype (e.g., `Object.create( null )`) are plain

  19. // 原型不存在 比如 Object.create(null) 直接返回 true;

  20. if ( !proto ) {

  21. return true;

  22. }


  23. // Objects with prototype are plain iff they were constructed by a global Object function

  24. Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;

  25. // 构造器是函数,并且 fnToString.call( Ctor ) === fnToString.call( Object );

  26. return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;

  27. },

  28. });

extend函数,也可以自己删掉写一写,算是 jQuery中一个比较核心的函数了。而且用途广泛,可以内部使用也可以,外部使用扩展 插件等。

链式调用

jQuery能够链式调用是因为一些函数执行结束后 returnthis。比如 jQuery 源码中的 addClassremoveClasstoggleClass

  1. jQuery.fn.extend({

  2. addClass: function(){

  3. // ...

  4. return this;

  5. },

  6. removeClass: function(){

  7. // ...

  8. return this;

  9. },

  10. toggleClass: function(){

  11. // ...

  12. return this;

  13. },

  14. });

jQuery.noConflict 很多 js库都会有的防冲突函数

jQuery.noConflict API

用法:

  1. <script>

  2. var $ = '我是其他的$,jQuery不要覆盖我';

  3. </script>

  4. <script src="./jquery-3.4.1.js">

  5. </script>

  6. <script>

  7. $.noConflict();

  8. console.log($); // 我是其他的$,jQuery不要覆盖我

  9. </script>

jQuery.noConflict 源码

  1. var


  2. // Map over jQuery in case of overwrite

  3. _jQuery = window.jQuery,


  4. // Map over the $ in case of overwrite

  5. _$ = window.$;


  6. jQuery.noConflict = function( deep ) {

  7. // 如果已经存在$ === jQuery;

  8. // 把已存在的_$赋值给window.$;

  9. if ( window.$ === jQuery ) {

  10. window.$ = _$;

  11. }


  12. // 如果deep为 true, 并且已经存在jQuery === jQuery;

  13. // 把已存在的_jQuery赋值给window.jQuery;

  14. if ( deep && window.jQuery === jQuery ) {

  15. window.jQuery = _jQuery;

  16. }


  17. // 最后返回jQuery

  18. return jQuery;

  19. };

总结

全文主要通过浅析了 jQuery整体结构,自执行匿名函数、无 new构造、支持多种规范(如commonjs、amd规范)、核心函数之 extend、链式调用、 jQuery.noConflict等方面。

重新梳理下文中学习的源码结构。

  1. // 源码结构

  2. ( function( global, factory )

  3. "use strict";

  4. if ( typeof module === "object" && typeof module.exports === "object" ) {

  5. module.exports = global.document ?

  6. factory( global, true ) :

  7. function( w ) {

  8. if ( !w.document ) {

  9. throw new Error( "jQuery requires a window with a document" );

  10. }

  11. return factory( w );

  12. };

  13. } else {

  14. factory( global );

  15. }


  16. } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {

  17. var version = "3.4.1",


  18. // Define a local copy of jQuery

  19. jQuery = function( selector, context ) {

  20. return new jQuery.fn.init( selector, context );

  21. };


  22. jQuery.fn = jQuery.prototype = {

  23. jquery: version,

  24. constructor: jQuery,

  25. length: 0,

  26. // ...

  27. };


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


  29. jQuery.extend( {

  30. // ...

  31. isPlainObject: function( obj ) {},

  32. // ...

  33. });


  34. init = jQuery.fn.init = function( selector, context, root ) {};


  35. init.prototype = jQuery.fn;


  36. if ( typeof define === "function" && define.amd ) {

  37. define( "jquery", [], function() {

  38. return jQuery;

  39. } );

  40. }

  41. jQuery.noConflict = function( deep ) {};


  42. if ( !noGlobal ) {

  43. window.jQuery = window.$ = jQuery;

  44. }


  45. return jQuery;

  46. });

可以学习到 jQuery巧妙的设计和架构,为自己所用,打造属于自己的 js类库。相关代码和资源放置在github blog中,需要的读者可以自取。

下一篇文章可能是学习 underscorejs的源码整体架构。

读者发现有不妥或可改善之处,欢迎评论指出。另外觉得写得不错,可以点赞、评论、转发,也是对笔者的一种支持。

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客 http://lxchuan12.cn
https://github.com/lxchuan12/blog,相关源码和资源都放在这里,求个 star^_^~

微信交流群,加我微信lxchuan12,注明来源,拉您进前端视野交流群

下图是公众号二维码:若川视野,一个可能比较有趣的前端开发类公众号

往期文章

工作一年后,我有些感悟(写于2017年)

高考七年后、工作三年后的感悟


点击阅读原文,或许阅读体验更佳

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存