jQuery 源码学习笔记
在学习之前,先问问自己,有必要学吗,学的目的是什么?现在前端开发中,使用就 jQuery 的地方很少,但是还是有必要学,为什么?因为这个库写的好,我们学习这个库,不仅为了面试,更理想的设定是为以后写出这样的库先学习
笔者带以下几个问题来看 jQuery 的源码
jQuery 的无 new 构造函数
jQuery 有哪些设计模式
jQuery 是如何实现 「重载」的?
jQuery 缓存系统如何实现?
jQuery 的插件系统如何设计的?
jQuery 如何实现链式操作的?
jQuery 是如何做浏览器特性检测的?
为什么有人认为 jQuery 的 API 设计很优雅?
jQuery 的无 new 构造函数
先看看使用 jQuery 时的场景:
// 无 new 构造
$(".p").text("hello, world")
// 使用 new 构造
var p = new $(".p")
p.text("hello, world")
一般情况下,我们都使用无 new 构造的方式使用 jQuery,在看 jQuery 源码之前,我们先尝试下,如果我们要做个库应该会先怎么做?
传统思路:
function jQuery(name, age) {
// 实例属性
this.name = name;
this.age = age;
// 实例方法
this.sayName = function () {
console.log('My name is', this.name)
}
}
// 静态方法
jQuery.sayGoodBye = function() {
console.log('Good Bye')
}
jQuery.prototype = {
// 构造器指向构造函数
constructor: jQuery,
// 原型方法
sayHello: function () {
console.log('say Hello')
}
}
测试用例:
let johnny = new jQuery("Johnny", 29)
let elaine = new jQuery("elaine", 29)
那有没有其他模式来创建实例呢?
这里有个小思考:为什么不使用 new 来创建 jQuery;而硬要用 无 new 构造呢?原因是提高使用者的开发体验,如果你用new 构造 jQuery,每拿一个 DOM 节点对象,就要 new,那代码中的 new 太多了,难看
有,工厂模式。在 《JavaScript 高级编程》的第八章介绍如何创建对象中,除了我们常见的原型+构造函数模式,还有工厂模式可创建对象:
function createPerson(name, age) {
let o = new Object();
o.name = name;
o.age = age;
o.sayName = function() {
console.log(this.name)
}
return o;
}
let johnny = createPerson("Johnny", 29)
let elaine = createPerson("elaine", 29)
如果用工厂模式模拟 jQuery,就成了:
function jQuery(name, age) {
function init(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log("My name is", this.name);
};
}
+ this.sayHello = function () {
+ console.log("hello");
+ };
return new init(name, age);
}
测试用例:
let johnny = jQuery("Johnny", 29)
johnny.sayName() // My name is johnny
johnny.sayHello() // johnny.sayHello is not a function
但这样会有个问题,jQuery 构造函数返回的是一个内部 init 构造函数,也就是说实例对象是一个 init 对象,那就不能调用 jQuery 上的方法了
那,那,那,那该如何是好呢?
将内部 init 构造函数转移至原型上,原型能继承啊
function jQuery(name, age) {
return new jQuery.prototype.init(name, age);
}
jQuery.prototype = {
constructor: jQuery,
init: function (name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log("My name is", this.name);
};
},
sayHello: function () {
console.log("hello");
},
};
jQuery.prototype.init.prototype = jQuery.prototype;
测试用例:
let johnny = jQuery("Johnny", 29)
johnny.sayName() // My name is johnny
johnny.sayHello() // Hello
这里需要分步骤理解:
function jQuery(name, age) {
// 返回原型上的构造函数init
return new jQuery.prototype.init(name, age);
}
jQuery.prototype = {
// 保持 jQuery 的原型指针正常
constructor: jQuery,
// init 为构造函数
init: function (name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log("My name is", this.name);
};
},
// jQuery 上的原型,可供实例对象调用
sayHello: function () {
console.log("hello");
},
};
// 最重要的是这句话,将 jQuery.prototype 赋值给 jQuery.prototype.init.prototype
jQuery.prototype.init.prototype = jQuery.prototype;
笔者不知道理解的对不对,看到这一源码,脑子里出现的第一场景是——“代孕”,后来想想,用养父母的角度理解或许更为恰达
我们一开始以为 jQuery
和 jQuery.prototype
是实例的"父母",其实不是。实例对象由 jQuery.prototype.init
new 而来,而 jQuery.prototype.init.prototype
则是 jQuery.prototype
赋值,所以实例对象才能调用 jQuery 原型上的方法
换句话说,我们先通过 jQuery 原型上的一个构造函数(init)来简化无 new 创建对象;再通过改变它原型的指针(将 init.prototype
赋值为 jQuery.prototype
)实现实例对象能调原型属性和方法(简单来说是修正 this 指向)
看看 jQuery 构造器源码
接下来看看 jQuery 的简化版源码吧
// 定义 jQuery 的类
var jQuery = function(selector, context) {
// return new jQuery.prototype.init(selector, context, rootjQuery)
// jQuery 用 init 方法创建的,它是 jQuery.fn.init 的实例而非 jQuery 的实例
return new jQuery.fn.init(selector, context, rootjQuery)
}
// 静态方法和实例方法共享设计
// 定义 jQuery 原型的方法
jQuery.fn = jQuery.prototype = {
jquery: 'X.X.X',
constructor: jQuery,
// 内部 init 类
init: function(selector, context, rootjQuery) {
...
return this;
},
// 原型属性,jQuery 对象的默认长度为 0
length: 0,
// 原型方法,略
size: function() {},
push: function() {},
splice: function() {},
}
// 把 jQuery.prototype 的能力搬运到 jQuery.prototype.init.prototype 上
// 即 jQuery.prototype.init.prototype = jQuery.prototype
// 因为 jQuery.fn = jQuery.prototype,所以就用以下代码赋值(效果一样)
jQuery.fn.init.prototype = jQuery.fn
和我们之前所写的 jQuery 的区别在于,多了 jQuery.fn
,它的作用是提供静态方法给 jQuery。 jQuery 中有一种写法是$.fn
,它能直接调用 jQuery 原型上的一些方法:
既然可以用 $.prototype 来表示原型,为什么还要定义个 $.fn 呢,笔者猜测因为 fn 是 function 的简写,名字好理解,又短,写起来比 $.prototype 更舒服
如此一来,我们就解决了 jQuery 无 new 构造的疑问
概括地讲:通过工厂模式 + 将 jQuery 的构造函数附着于原型链上实现无 new 构造
jQuery 是如何实现 「重载」的?
先不谈设计模式,顺着前文源码,我们看到 jQuery 的构造函数其实是 jQuery.prototype.init
(或者叫 jQuery.fn.init
,后续为书写方便,用此单词)
要想回答 jQuery 是如何重载的,就要看 jQuery.fn.init
先来看看 jQuery 的使用方法:
// 9种方法的重载
// 接受一个字符串,其中包含了用于匹配元素集合的 CSS 选择器
jQuery([selector,[context]])
// 单个 DOM
jQuery(element) // $("p")
// DOM 数组
jQuery(elementArray)
// JS 对象
jQuery(object)
// 传入空参数
jQuery()
// jQuery 对象
jQuery(jQuery object) // $.ajax $.extend
// 传入原始 HTML 的字符串来创建 DOM 元素
jQuery(html,[ownerDocument])
jQuery(html,[attributes]) // $( "<p id='test'>My <em>new</em> text</p>" ).appendTo( "body" );
// 绑定一个在 DOM 文档载入完成后执行的函数
jQuery(callback) // $(fn) 等同于 $(document).ready(fn);
具体可在官网中查看jQuery
这九种用法整体上分为三大块:选择器、dom处理、dom加载。因为 jQuery 内部采用的是类数组对象的方式存储结构,所以我们即能可以像对对象一样处理 jQuery,也可以像数组一样使用 push、pop、shift、unshift、sort、each、map(这些API框架自己写的)等类数组的方法操作 jQuery 对象
通过用法我们也能看出,jQuery 的构造函数接受两个参数:选择器 selector 和 上下文 context,而我们的重载正是通过对这两个参数的有无,所属的类型来实现
var jQuery = function(selector, context) {
return new jQuery.fn.init(selector, context, rootjQuery)
}
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
init: function(selector, context, rootjQuery) {
var match, elem, doc;
// 如果是 $(""), $(null), $(undefined), $(false) 返回 jQuery 实例
if ( !selector ) {
return this;
}
// 如果是个 dom 节点的话
if ( selector.nodeType ) {
this.context = this[0] = selector;
this.length = 1;
return this;
}
// 如果 seletor 是个字符串的话
if ( typeof selector === "string" ) {
// 做很多判断,针对每一个情况,用 if else 来隔离
// ...
// 如果是函数
} else if ( jQuery.isFunction( selector ) ) {
// $(document).ready(fn)
return rootjQuery.ready( selector );
}
// 如果选择器中的选择器没有的话,就赋值
if ( selector.selector !== undefined ) {
this.selector = selector.selector;
this.context = selector.context;
}
return jQuery.makeArray( selector, this );
},
// ...
}
概括地讲:所谓的重载,就是做各种 if else 判断,不过这里也有技巧,把空、只有一种场景放在最前面
jQuery 有哪些设计模式
像前文说所的,它用了工厂模式实现了无 new 构造,它还使用了订阅模式实现数据绑定,使用了插件系统、事件委托、链式调用 等等
jQuery 缓存系统如何实现
司徒正美的 缓存机制
weakSet 实现
jQuery 的插件系统如何设计的?
jquery.extend
jQuery 如何实现链式操作的?
$("#p1").css("color","red").slideUp(2000).slideDown(2000);
返回 this
jQuery 是如何做浏览器特性检测的?
为什么有人认为 jQuery 的 API 设计很优雅?
其他知识点
IIFE营造的沙箱模式
IIFE 是为了保护变量,如何保护变量的呢?
你既然是一个库,那就有不同的方法之类,如果都写在一个文件中,引入后,方法名也许就和其他库的方法名冲突,用 IIFE 能保护库的变量不被外部函数影响
IIFE的意思是立即调用函数表达式,关键在于它首先是个函数,其次立即调用。因为是函数,所以函数中的变量收到函数作用域的保护;其次立即调用即引入就执行代码
模块加载的发展,IIFE,为什么要用这个,现代用打包工具来打包库
IIFE——AMD/CMD——ESM——ES model——现代打包工具打包库
命名冲突
//jQuery1.2
var _jQuery = window.jQuery, _$ = window.$;//先把可能存在的同名变量保存起来
jQuery.extend({
noConflict: function(deep) {
window.$ = _$;//这时再放回去
if (deep)
window.jQuery = _jQuery;
returnjQuery;
}
})
core 核心模块
动画引擎:原型下的 Animation
事件:原型下的 Event
多库共存
noConflict
https://api.jquery.com/jQuery.noConflict/
换个思路理解
jQuery库的写法是工厂模式的写法,即它相当于一个工厂,你需要衣服,放入衣料(原材料),返回一件衣服;投入一块猪肉,返回猪罐头;jQuery 是你传入一个 selector,返回一个对应的实例对象。这种模式的好处是使用者不用使用 new 就能拿到一个实例对象,这个 new 的调用是框架里面实现了,直接返回给调用者一个 new 好了的实例,对于需要频繁拿多个实例的库来说,使用起来更方便
jQuert 使用的是 工厂模式,他的
总结
笔者也没读完所有的源码,一是已经很久没用 jQuery...
要知道无论是人,还是库,都会在抢占市场中努力,现阶段的打包器webpack、vite,前端时间的 tubrutsx 说自己的速度是 vite 的10倍,这不久被 尤大批了吗
jQuery 用 $ 符号,难道其他库没想到吗?它之前也有不少库用 $,jQuery 用 $ 一来降低开发者的学习成本,二来因为方便所以更让人接受,
在 jQuery 最开始的版本,jQuery 是支持传参的,在 v1.4 之后,jQuery 就不支持传参了,直接用 $()
构建一个实例,在通过ajax()
、.css()
、.add()
、.hide()
之类的方法来操作 DOM 节点
jQuery 最核心的功能是操作 DOM,所有疑似 DOM 的东西都可以扔进$()
里,然后他就是 DOM 了
jQuery 设计思想
https://icodeq.com/2022/ba3c0710acd5/