45

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"]

无悔铭
1.6k 声望504 粉丝

追求卓越,成功也会在不经意之间追上你


引用和评论

0 条评论