jQuery源码解析

jQuery 是一个很是优秀且经典的库。怎么形容它的优秀呢?即便近两年流行了如 Vue 、 React 等众多热门的库,但对于封装方法、思想而言,这些库都未曾超越jQuery。所以,对于前端工程师而言,阅读 jQuery 源码是一条提高自个人必经之路。那么接下来,就让咱们一块儿走进 jQuery 内幕的世界。javascript

1、jQuery源码目录解析

1)目录结构解析

首先,咱们从 jQuery 源码的 github 上下载并使用 vscode 打开 jQuery 源码。
图片描述html

打开 jQuery 目录,能够很明显的看见 package.json 和 gruntfile.js 两个文件,熟悉 grunt 的小伙伴,看见 gruntfile.js 就很清楚,该目录代码使用的是 grunt 做为其构建工具。前端

  • 咱们为何要使用构建工具呢?
  • 一句话:自动化。对于须要反复重复的任务,例如压缩、编译、单元测试、linting等,自动化工具能够减轻你的劳动,简化你的工做。
  • 为何要使用 Grunt 呢?
  • Grunt 生态系统很是庞大,而且一直在增加。因为拥有数量庞大的插件可供选择,所以,你能够利用 Grunt 自动完成任何事,而且花费最少的代价。

打开src文件夹,文件夹里面就是 jQuery 的源码目录,咱们能够从目录清晰的看见jQuery的各个模块:
图片描述java

接下来,咱们打开src文件夹中的jquery.js,便可看到 jQuery 的代码加载:
图片描述
从图片中,咱们能够看见,采用的是AMD方式定义。咱们甚至能够直接从该文件看出 jQuery 有哪些功能,可供咱们使用。jquery

2、jQuery经典细节解析

1)经典细节1——当即执行函数

首先,咱们能够从jquery官网,使用grunt编译一下 jQuery 源码或下载编译事后、未压缩版本的 jQuery 。若使用grunt编译,咱们能够从dist/jquery.js中,看到以下代码:
图片描述git

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

咱们对其,进行一番简化:github

(function(global,factory){
    ...
})(window,funciton(){});

这样,就很是一目了然了,这是经典的当即执行函数(IIFE):json

(function(){ ... })()

Q:采用当即执行函数,这样作,有什么好处呢?
A:经过定义一个匿名函数,建立了一个新的函数做用域,至关于建立了一个“私有”的命名空间,该命名空间的变量和方法,不会破坏污染全局的命名空间。此时如果想访问全局对象,将全局对象以参数形式传进去便可。此外,新做用域内对象想访问传入的全局对象时,就不须要一步一步的往上找,可提升效率。segmentfault

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方法是,这个过程是:

一、object -> people,name -> find,func -> find0;
二、old -> people[find],为undefined
三、people[find],关联的是find0

在第二次执行addMethod方法是,这个过程是:

一、object -> people,name -> find,func -> find1;
二、old 为 object[name],即上一次执行object[name]=function(){..}时的函数,这个函数关联的是find0。
三、people[find],关联的是find1

两次调用后,此时,若调用people.find("a")的话,过程以下:

一、两次addMethod()后,形式参数为1个参数,调用people.find("a"),实际参数为1个参数
二、形参长度与实参长度相等,调用return func.apply(this,arguments),即find1
三、运行find1,打印出“a”

你看到这,是否也和博主同样,以为这是无所必要的呢?接下来,就是令你兴奋的时刻:
若调用people.find()的话,这个过程会以下:

一、两次addMethod()后,形式参数为1个参数,调用people.find(),实际参数为0个参数
二、形参长度与实参长度不相等,先调用return old.apply(this,arguments),咱们在第二次调用addMethod中阐述了,它关联的是find0,于是,此时的程序,会再次调用第一次addMethod中无参数的function(){...},即find0
三、此时的形式参数为0个,实际参数为0个
三、运行find0,打印出["a", "b", "c"]
相关文章
相关标签/搜索