jQuery 是一个非常优秀且经典的库。怎么形容它的优秀呢?即使近两年流行了如 Vue 、 React 等众多热门的库,但对于封装方法、思想而言,这些库都不曾超越jQuery。因此,对于前端工程师而言,阅读 jQuery 源码是一条提升自我的必经之路。那么接下来,就让我们一起走进 jQuery 内幕的世界。
一、jQuery源码目录解析
1)目录结构解析
首先,我们从 jQuery 源码的 github 上下载并使用 vscode 打开 jQuery 源码。
打开 jQuery 目录,可以很明显的看见 package.json 和 gruntfile.js 两个文件,熟悉 grunt 的小伙伴,看见 gruntfile.js 就很清楚,该目录代码使用的是 grunt 作为其构建工具。
- 我们为什么要使用构建工具呢?
- 一句话:自动化。对于需要反复重复的任务,例如压缩、编译、单元测试、linting等,自动化工具可以减轻你的劳动,简化你的工作。
- 为什么要使用 Grunt 呢?
- Grunt 生态系统非常庞大,并且一直在增长。由于拥有数量庞大的插件可供选择,因此,你可以利用 Grunt 自动完成任何事,并且花费最少的代价。
打开src文件夹,文件夹里面就是 jQuery 的源码目录,我们可以从目录清晰的看见jQuery的各个模块:
接下来,我们打开src文件夹中的jquery.js,即可看到 jQuery 的代码加载:
从图片中,我们可以看见,采用的是AMD方式定义。我们甚至可以直接从该文件看出 jQuery 有哪些功能,可供我们使用。
二、jQuery经典细节解析
1)经典细节1——立即执行函数
首先,我们可以从jquery官网,使用grunt编译一下 jQuery 源码或下载编译过后、未压缩版本的 jQuery 。若使用grunt编译,我们可以从dist/jquery.js中,看到如下代码:
(function(global, factory){
...
})(typeof window !== "undefined" ? window : this, function( window, noGlobal(){...});
我们对其,进行一番简化:
(function(global,factory){
...
})(window,funciton(){});
这样,就非常一目了然了,这是经典的立即执行函数(IIFE):
(function(){ ... })()
Q:采用立即执行函数,这样做,有什么好处呢?
A:通过定义一个匿名函数,创建了一个新的函数作用域,相当于创建了一个“私有”的命名空间,该命名空间的变量和方法,不会破坏污染全局的命名空间。此时若是想访问全局对象,将全局对象以参数形式传进去即可。此外,新作用域内对象想访问传入的全局对象时,就不需要一步一步的往上找,可提高效率。
2)经典细节2——init()
我们看如下一段代码:
var s = new $('.test');
var p = $('.test');
console.log(s);
console.log(p);
我们引入一下jQuery,并处理一下这段代码,可以看到效果如下:
令人惊讶的是,new出来的和直接调用的,居然是一模一样的。
这是为什么呢?
这就涉及到了jQuery的经典的init操作:
我们打开jQuery目录下的src/core.js文件,我们可以看见一段非常经典的代码:
从上面这张图,我们可以了解到:
- 第一个红框:调用 jQuery ,返回的是new jQuery.fn.init(selector,context);而init方法被挂在到了jQuery.fn上的。
- 第二个红框:jQuery.fn = jQuery.prototype = {...};
[注]我们也可以从src/core/init.js中,看init是如何具体实现初始化的。
为了方便讲解,我们对其进行一些简化:
//1
jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context );
}
//2
jQuery.fn = jQuery.prototype = {
init:function( selector, context ){
...
}
}
//3
init = jQuery.fn.init = function( selector, context, root ){
...
}
init.prototype = jQuery.fn;
- 步骤1:我们从代码块2开始看,jQuery.prototype = jQuery.fn,且都挂载了init()函数。
- 步骤2:再看代码块3,jQuery.fn.init.prototype = jQuery.fn,而我们从步骤1中,了解到jQuery.prototype = jQuery.fn。因此,jQuery.fn.init.prototype = jQuery.fn = jQuery.prototype。
- 步骤3:最后,再回过来看代码块1,function返回的是new jQuery.fn.init(..)。我们再看步骤2,jQuery.fn.init.prototype = jQuery.prototype。那么,new jQuery.fn.init(..)就相当于function返回了一个new jQuery()。
饶了一大圈,就相当于 jQuery = new JQuery();
Q:那么,为啥要绕那么远呢?
A:为了得到jQuery原型链上的方法。
[特别标注]如果你看了五遍,依旧看不懂这个过程,亦或对Q/A没有看懂,你可能对js创建对象中的构造函数模式、原型模式、组合模式等的理解还不够深刻,你可以戳我这篇博文学习一下,亦可翻阅《javascript 高级程序设计》第六章-面向对象的程序设计中的创建对象部分内容。
3)经典细节3————链式调用
接下来,我们看一段官方给的jQuery链式对象的示例:
//html
<div class="grandparent">
<div class="parent">
<div class="child">
<div class="subchild"></div>
</div>
</div>
<div class="surrogateParent1"></div>
<div class="surrogateParent2"></div>
</div>
//js
//return [div.surrogateParent1]
$("div,parent").nextAll().first();
//return [div.surrogateParent2]
$("div.parent").nextAll().last();
$("div,parent").nextAll().first()这是我们使用jQuery时,经常使用的调用方法,链式调用。那它是如何做到的呢?我们先看一眼这个代码:
var test = {
a:function(){
console.log('a');
},
b:function(){
console.log('b');
},
c:function(){
console.log('c');
}
}
test.a().b().c();
结果如何呢?
答案很明显,b()和c()是无法访问的。jQuery是如何实现它的呢?很简单,返回它本身即可。如:
var test = {
a:function(){
console.log('a');
return this;
},
b:function(){
console.log('b');
return this;
},
c:function(){
console.log('c');
}
}
test.a().b().c();
//a
//b
//c
4)经典细节4————闭包下的重载
$('.test','td')
$(['.test','#id'])
$(function(){...})
$()就是一个函数,参数不同,就涉及到了函数的重载。参数个数不等,用传统js实现起来非常困难。那么jQuery究竟是如何实现的呢?
我们通过两段代码,领悟它的实现方式:
(1)首先我们看一个普通的例子:
function addMethod( object, name, func ) {
var old = object[name];
object[name] = function(){
if(func.length === arguments.length){
return func.apply(this,arguments);
}else{
return old.apply(this,arguments);
}
}
}
var people = {
name:["a","b","c"]
}
var find0 = function(){
return this.name;
}
addMethod(people,'find',find0);
console.log(people.find());//["a", "b", "c"]
调用people.find,将find()方法加到了people中,调用people下的find()方法后,返回的是people.name,即:["a", "b", "c"]。
(2)我们加上一些代码,形成重载,再来看看这个例子:
添加一个addMethod(people,'find',find1):
function addMethod( object, name, func ) {
var old = object[name];
object[name] = function(){
if(func.length === arguments.length){
return func.apply(this,arguments);
}else{
return old.apply(this,arguments);
}
}
}
var people = {
name:["a","b","c"]
}
var find0 = function(){
return this.name;
}
//新增
var find1 = function(name){
var arr = this.name;
for(var i = 0;i <= arr.length;i++ ){
if(arr[i]=name){
return arr[i];
}
}
}
addMethod(people,'find',find0);
//新增
addMethod(people,'find',find1);
console.log(people.find());//["a", "b", "c"]
console.log(people.find("a"));//a
在第一次执行addMethod方法是,这个过程是:
1、object -> people,name -> find,func -> find0;
2、old -> people[find],为undefined
3、people[find],关联的是find0
在第二次执行addMethod方法是,这个过程是:
1、object -> people,name -> find,func -> find1;
2、old 为 object[name],即上一次执行object[name]=function(){..}时的函数,这个函数关联的是find0。
3、people[find],关联的是find1
两次调用后,此时,若调用people.find("a")的话,过程如下:
1、两次addMethod()后,形式参数为1个参数,调用people.find("a"),实际参数为1个参数
2、形参长度与实参长度相等,调用return func.apply(this,arguments),即find1
3、运行find1,打印出“a”
你看到这,是否也和博主一样,觉得这是无所必要的呢?接下来,就是令你兴奋的时刻:
若调用people.find()的话,这个过程会如下:
1、两次addMethod()后,形式参数为1个参数,调用people.find(),实际参数为0个参数
2、形参长度与实参长度不相等,先调用return old.apply(this,arguments),我们在第二次调用addMethod中阐述了,它关联的是find0,因而,此时的程序,会再次调用第一次addMethod中无参数的function(){...},即find0
3、此时的形式参数为0个,实际参数为0个
3、运行find0,打印出["a", "b", "c"]
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。