把jquery.js源码文件稍微收缩一下,大致看一下在html文件中引入这个js文件时,执行了什么:
( function( global, factory ) {
"use strict";
if ( typeof module === "object" && typeof module.exports === "object" ) {
module.exports = global.document ?
factory( global, true ) :
function( w ) {
if ( !w.document ) {
throw new Error( "jQuery requires a window with a document" );
}
return factory( w );
};
} else {
factory( global );
}
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
// 此处省略一万字
} );
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
原来加载时,直接执行了一个IIFE(立即调用函数表达式),由于主要是在html环境中执行的,所以函数调用的实参就是 window,和匿名函数 function(window, noGlobal),进入函数后,if 判断无效,直接调用 factory(global),实际上也就是用参数window和undefined调用了 function(window=window, noGlobal=undefined)这个匿名函数。
接下来进入这个匿名函数看看,好家伙,竟然有小1万行代码,直接看最后倒数几行:
if ( typeof noGlobal === "undefined" ) {
window.jQuery = window.$ = jQuery;
}
return jQuery;
- 1
- 2
- 3
- 4
前面我们传的参数noGlobal是undefined,所以进入了 if 语句,也就是说给window对象设置了2个全局属性, jQuery和$,让他们直接等于了jQuery这个对象,所以实际上window.$和就是同一个对象,也就是jQuery这个库的根对象。所以的操作都是基于这个属性开始的。
那个jQuery是个什么东西呢,再看第118行(代码经常变动,行数可能会不一样的):
jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context );
};
- 1
- 2
- 3
原来它本质上就是一个函数,jQuery就是它的函数名,有2个参数selector,和context,我们大部分情况下使用时就是传入给selector参数了。
再看一下:
jQuery.fn = jQuery.prototype = {/*...*/};
- 1
原来fn等于函数原型啊,通常会把实例方法定义在原型里。找到了,再找一下函数吧。在2417行:
init = jQuery.fn.init = function( selector, context, root ) {}
init.prototype = jQuery.fn;
- 1
- 2
init也是一个函数,但是在jQuery调用的时候,使用了new操作符,这样实际上init就是一个构造函数,我们返回了init实例,然后把这个函数的原型设置成了和一样的函数,这样init实例也具备了中所有的方法。
假设我们这样使用jQuery:
const h1 = $('h1');
- 1
执行时,本质上等于调用了:
const h1 = new jQuery.fn.init('h1');
- 1
咱们进去看一下init做了什么:
// 处理: $(""), $(null), $(undefined), $(false)这些调用直接返回new init()本身
if ( !selector ) {
return this;
}
- 1
- 2
- 3
- 4
空谈误人,咱们创建一个测试html文件,来看看测试的结果吧:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<!-- 引入 jQuery -->
<script src="../scripts/" type="text/javascript"></script>
<script type="text/javascript">
//等待dom元素加载完毕.
$(document).ready(function () {
console.log("Hello World!");
});
</script>
</head>
<body>
</body>
</html>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
在chrome中打开这个文件,并且打开开发者工具,进入console面板,可以看到log输出"Hello World!":
顺便验证一下前面所说的jQuery, $的关系吧,把下面的代码直接粘贴到console中执行:
console.log(jQuery === $);//true
console.log($(''));// {} 对象
- 1
- 2
咱们接下来看一下$(document)是个什么玩意?继续读init函数的源码,收缩一下,能看到接下来要走一个if-else判断:
显示应该进入第一个else if分支,因为 == 9,表示Document类型,就执行了下面几条语句:
else if (selector.nodeType) {
this[0] = selector;
this.length = 1;
return this;
}
- 1
- 2
- 3
- 4
- 5
把document赋给了属性"0",并且给属性"length"赋值为1,看来,这个对象是当成了类似数组来用啊。
接下来看一下ready函数是怎么执行的,在原型里我们可以找到这个实例函数,在3342行:
var readyList = jQuery.Deferred();
jQuery.fn.ready = function (fn) {
readyList
.then(fn)
.catch(function (error) {
jQuery.readyException(error);
});
return this;
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
其中 fn 就是我们在代码里输出"Hello World"的匿名函数,看ready函数里的内容,很像ES6新增加的Promise类型方法。那我们看看 readyList是个什么类型,readyList = (),那这个函数返回值是什么呢,我们在2971行找到了这个函数:
jQuery.extend({
Deferred: function (func) {
var tuples = [
- 1
- 2
- 3
这个方法是扩展的方法,方法就是把Deferred方法添加到jQuery这个函数对象中,这样jQuery就可以直接调用这个方法了,看看Deferred()创建了一个什么对象:
好家伙,仔细看看里面的代码,它真的用ES5的方式实现了ES6中新增的Promise类型,如果你学过Promise的使用,那么就很容易理解().catch()这种调用的方式,ready()方法就是把回调函数放到事件队列里,等下一次执行的时候就会调用到它,其实想想,当前正在执行DOM加载,那么加载完成后才能继续队列中的下一个事件,显然这时候执行到回调函数的时候,DOM元素初始加载肯定完成了。但是ready()方法和肯定有区别,因为ready()中的函数只在下一次处理事件队列时执行,这个肯定不能保证图片能否加载完成。