jQuery源码分析系列(一)初识jQuery

时间:2023-03-09 17:01:39
jQuery源码分析系列(一)初识jQuery

一个工厂

(function(global, factory){
"use strict"
// operation_1
})(typedef window !== "undefined" ? window : this, function (window, noGlobal) {
// operation_2
}) =====> (function(args,...){...})(params,...) // 一种自执行方法,形式为(function(){})(),也可以写为 !function(){}() 或 ;function(){}();

其中function(window, noGlobal){}有11000多行代码,并且注入到operation_1中执行。

// operation_1
// 这个一个兼容操作,旨在检测文件是否运行在CommonJS中,如NodeJS,如果是的话进入此分支,一般不会进入
if (typeof module === "object" && typeof module.exports === "object") {
module.exports = global.document ?
factory(global, true) : // 如果global.document存在,则向外面暴露出 factory 方法
function (w) {
if (!w.document) { // 因为不存在w,所以 !w.document为true,所以报错
throw new Error("jQuery requires a window with a document");
}
return factory(w);
};
} else { // 通常直接进入此分支
factory(global); // 没有传入noGlobal,将引发其他操作
}

至此完成了方法的注入调用。但是我们发现传入的实参比形参少一个noGlobal,为此我们全局查找此参数

// noGlobal第三次出现在11124行
if (typeof noGlobal === "undefined") { // 如果noGlobal未定义(未传入)
window.jQuery = window.$ = jQuery; // 将实例jQuery赋值给窗口的$或jQuery,这也就是$()和jQuery()两个选择器的由来
}

通过使用窗口(全局)的jQuery或$,就可以实现jQuery类或对象的调用,当然调用对象需要用到其构造方法,我们接着往下看



知识点:

  • 自执行方法: 形式有(function(){})()!function(){}();function(){}()
  • "use strict":开启严格模式,此模式下代码质量要求及书写规范要求更高,但能保证代码的安全,提高编译器效率,加快运行速度
  • typedef module === "object": 在其他JS模块中的执行检测,详细请跳转至此处


3个变量,6个函数

/**
* 内部变量
*/
var arr = [];
var getProto = Object.getPrototypeOf;
var slice = arr.slice;
... // 有些变量看起来没有必要定义,但是为了避免魔鬼变量(数字、字符串),代码中具有特殊用途的变量必须定义 /**
* 内部方法
*/
// 数组扁平化,将数组深度遍历并组成新的一维数组
var flat = arr.flat ? function(array){
return arr.flat.call(array);
} : function (array) { // 如果不能直接扁平化,则利用concat.apply()方法实现
// Array.prototype.concat.apply([],array) 递归扫描array数组,并扁平化追加到参数1内,因此需要输入一个空数组
return arr.concat.apply([], array);
}; // 判断是否是方法
var isFunction = function isFunction(obj) {
return typeof obj === "function" && typeof obj.nodeType !== "number"; // typeof obj.nodeType !== "number",任何DOM元素的nodetype都是数字,常见的有1 9 11
}; // 简单的理解为,创造一个类似node的Script节点(内容为code)并挂载到doc上
function DOMEval(code, node, doc) {
doc = doc || document; var i, val, script = doc.createElement("script"); script.text = code;
if (node) {
for (i in preservedScriptAttributes) { // 对于xx中的每个属性
// 有些浏览器不支持脚本的“nonce”属性。另一方面,仅仅使用“getAttribute”是不够的,因为每当“nonce”属性连接到上下文时,
// 它就会重置为空字符串。在`节点.getAttribute`添加支票是为了`jQuery.globalEval`这样它就可以通过一个对象伪造一个包含nonce的节点。
val = node[i] || node.getAttribute && node.getAttribute(i); // 如果node存在这个属性,则设置到script中
if (val) {
script.setAttribute(i, val);
}
}
}
doc.head.appendChild(script).parentNode.removeChild(script); // script挂载到头节点上
} // 获取对象的类型
function toType(obj) {
if (obj == null) {
return obj + ""; // 强转成字符串,null + "" == "null"
} return typeof obj === "object" || typeof obj === "function" ? // 本篇代码常使用三元表达式取代if/else,并嵌套逻辑表达式,阅读起来较吃力
class2type[toString.call(obj)] || "object" : // 利用Object.protoType的原型方法获取对象的类型,而不是typedef,此举更精确
typeof obj;
}

重头戏来了,下面的是jQuery对象的构造方法

// 在160行左右
jQuery = function (selector, context) { // The jQuery object is actually just the init constructor 'enhanced'
// Need init if jQuery is called (just allow error to be thrown if not included)
return new jQuery.fn.init(selector, context); // 返回的是新建的jQuery对象,这就是实现jQuery链式调用的支柱之一
}; // 在3300行左右
init = jQuery.fn.init = function (selector, context, root) {
var match, elem; // HANDLE: $(""), $(null), $(undefined), $(false)
if (!selector) {
return this;
} // Method init() accepts an alternate rootjQuery
// so migrate can support jQuery.sub (gh-2101)
root = root || rootjQuery; // 如果没传入 // Handle HTML strings
if (typeof selector === "string") {
if (selector[0] === "<" &&
selector[selector.length - 1] === ">" &&
selector.length >= 3) { // Assume that strings that start and end with <> are HTML and skip the regex check
// 假设字符串是HTML文本形式,则跳过正则表达式验证
match = [null, selector, null]; } else {
match = rquickExpr.exec(selector); // 如果selector为 "#xx"模式,则结果为["#xx",undefined,"xx"]
} // Match html or make sure no context is specified for #id
// 匹配html 或 确保没有为#id指定上下文
if (match && (match[1] || !context)) { // HANDLE: $(html) -> $(array) // 将html解析成类数组并merge返回
if (match[1]) {
context = context instanceof jQuery ? context[0] : context; // Option to run scripts is true for back-compat
// Intentionally let the error be thrown if parseHTML is not present
// 将选择器HTML标签解析成DOM元素,并与jQuery对象合并
jQuery.merge(this, jQuery.parseHTML(
match[1],
context && context.nodeType ? context.ownerDocument || context : document,
true
)); // HANDLE: $(html, props)
// 如果是一个选择器字符串是一个单独的标签并且上下文是POJO对象(也就是DOM元素)
// 因为在上面合并了DOM元素和jQuery对象,所以DOM的上下文属性也要继承到jQuery对象中(如有错误,烦请大佬评论指正)
if (rsingleTag.test(match[1]) && jQuery.isPlainObject(context)) {
for (match in context) { // Properties of context are called as methods if possible
if (isFunction(this[match])) { // 如果上下文的属性是DOM对象的可执行方法时,
this[match](context[match]); // 将props对象的fncName属性的值作为参数传递给DOM集合对象中的fncName函数并执行之 // ...and otherwise set as attributes
} else {
this.attr(match, context[match]); // 否则将上下文的属性添加到jQuery对象中
}
}
} return this; // 直接返回jQuery对象 // HANDLE: $(#id)
} else {
// 如果是匹配ID,直接用原生js
elem = document.getElementById(match[2]); if (elem) { // Inject the element directly into the jQuery object
this[0] = elem;
this.length = 1;
}
return this;
} // HANDLE: $(expr, $(...))
// 如果不存在上下文,或者传入的是jQuery对象,如$("p")、$("p",$(".test"))
} else if (!context || context.jquery) {
// 返回 root.find(selector),定位到find()定义处,你会发现find()中调用了jQuery.find()方法,又jQuery.find = Sizzle,而Sizzle是本篇代码的重要部分 -- CSS选择器
return (context || root).find(selector); // HANDLE: $(expr, context) (which is just equivalent to: $(context).find(expr)
// 如果存在真实的上下文环境,如 $("p",".test")
} else {
return this.constructor(context).find(selector);
} // HANDLE: $(DOMElement)
} else if (selector.nodeType) { // 如果传入的是节点,直接将节点返回
this[0] = selector;
this.length = 1;
return this; // HANDLE: $(function)
// Shortcut for document ready
} else if (isFunction(selector)) { // 如果传入的是方法
return root.ready !== undefined ?
root.ready(selector) : // Execute immediately if ready is not present
selector(jQuery); // 传入jQuery调用selector方法
} return jQuery.makeArray(selector, this); // 返回一个类数组
};

至此,我们了解了jQuery对象的创建过程,创建对象又包含着选择器的执行过程:

  1. 传入的选择器分为无效选择器、字符串、节点、方法四类,其中重点是字符串
  2. 传入字符串包括HTML字符串和普通字符串,利用正则表达式可以准确分别:
    1. 如果是HTML字符串,直接解析成DOM元素并组合到jQuery对象中返回
    2. 如果是ID选择器,直接调用原生JS方法得到结果并返回即可
    3. 如果上下文没指定或上下文为jQuery对象时,在根或jQuery对象中根据选择器查找下级节点(find()本质也就是Sizzle())
    4. 如果上下文为真实的DOM元素时,则根据DOM创建jQuery对象,操作同上

如果对上述执行流程不了解,请自行选择测试案例并打断点进行调试(如果错误,敬请评论指正)。

按照业务流程,下面我们应该根据创建对象时返回的find(),跳过300行代码来到Sizzle篇。但为了以后不出现遗漏,还是一行行往下看吧。

上面主要讲到了jQuery对象,那么jQuery对象的成员变量和成员函数是怎么声明定义的呢? jQuery类的静态成员又是怎么定义的呢? 我们跳回到jQuery()构造函数处接着往下看。

知识点:

  • 魔鬼变量: 变量的使用要谨慎,要具备优良的可读性和
  • 数组扁平化:两种方法实现
  • 原型方法获取数据类型
  • 三元表达式实现if/else
  • 创建jQuery对象时传入的选择器类型的不同及执行时的差别


N个变量,2N个函数

下面我们将看来两个重要函数的定义:jQuery.fn和jQuery.extend,首先我们来看jQuery.fn

jQuery.fn = jQuery.prototype = {
jquery: version,
constructor: jQuery,
length: 0,
... get: function (num) {
if (num == null) {
return slice.call(this);
} return num < 0 ? this[num + this.length] : this[num];
}, // Take an array of elements and push it onto the stack // 将元素加入栈中
pushStack: function (elems) { // 此方法是jQuery链式调用的核心 // Build a new jQuery matched element set
var ret = jQuery.merge(this.constructor(), elems); // 将新来的参数合并到constructor中,并保留指向 // Add the old object onto the stack (as a reference)
ret.prevObject = this; // Return the newly-formed element set
return ret; // 返回合并后的新对象,此对象中保存了前一个对象的信息
}, slice: function () {
// 同上,在做切片之后,能够根据返回的ret找到栈的上一个对象
return this.pushStack(slice.apply(this, arguments));
}, end: function () { // 获取对象在栈中的上一个对象,没有的话返回构造器
return this.prevObject || this.constructor();
}
}

链式调用:

链式调用即是对一个对象进行一连串链式操作,在jQuery中体现为$(ele).show().find(child).hide(),而此操作的核心就是上面的pushStack(),对象的大部分成员函数其最下层会调用pushStack()并返回一个新的对象。篇幅原因,请自行打断点进行deBugger;

原型对象 prototype:

在了解jQuery.fn之前,大家需要对jQuery.prototype有个了解。

在 JavaScript 中,每个函数对象都有一个默认的属性 prototype,称为函数对象的原型成员,这个属性指向一个对象,称为函数的原型对象。而这个原型对象上定义的成员将用来共享给所有通过这个函数创建的对象使用(类似于实例化)。通常调用new出来的对象的prototype属性就会得到这个原型对象,再通过修改此对象的成员,来达到对所有实例对象增删成员的目的。

Object 函数对象是 JavaScript 中定义的*函数对象,在 JavaScript 中所有的对象都直接或者间接地使用 Object 对象的原型。 当访问对象的成员时,首先检查成员本身,再检查其原型,再检查其原型的原型(可达Object),直到找到访问的成员。

jQuery.fn:

再来看jQuery文件中定义 jQuery.fn = jQuery.prototype = {...},我们就知道这是改变jQuery对象的原型对象,并且能够影响到所有创建的jQuery对象,也就是说在这里定义的所有成员,都能通过jQuery对象访问到。如果我们需要自定义jQuery对象的某种属性或操作,直接在这里操作即可。

如果说对象的成员能通过原型对象赋予得到,但静态类的成员怎么扩展呢? 并且拓展对象成员每次都要通过修改原型对象? 下面我们来看 jQuery.extend = jQuery.fn.extend = function(){}

jQuery.extend = jQuery.fn.extend = function () {
var options, name, src, copy, copyIsArray, clone,
target = arguments[0] || {}, // 如果传入参数,arguments[0]指传入函数的第一个参数
i = 1,
length = arguments.length,
deep = false; // Handle a deep copy situation
// 处理深拷贝场景,在这里不讨论
if (typeof target === "boolean") {
deep = target; // Skip the boolean and the target
target = arguments[i] || {};
i++;
} // Handle case when target is a string or something (possible in deep copy)
// 如果传入的不是对象也不是方法,或许是深拷贝
if (typeof target !== "object" && !isFunction(target)) {
target = {};
} // 如果只传入一个对象,那么就将target切换为jQuery对象,同时坐标定位到第一个元素
if (i === length) {
target = this; // 一般情况下的挂载载体
i--;
} for (; i < length; i++) { // Only deal with non-null/undefined values
if ((options = arguments[i]) != null) { // Extend the base object
for (name in options) { // 循环挂载此对象中的信息
copy = options[name]; // Prevent Object.prototype pollution Prevent never-ending loop
// 放置原型对象污染,防止死循环
if (name === "__proto__" || target === copy) {
continue;
} // Recurse if we're merging plain objects or arrays
// 如果是合并纯粹的对象(POJO)或数组则递归执行, 一般挂载单个对象不会进入此分支
if (deep && copy && (jQuery.isPlainObject(copy) ||
(copyIsArray = Array.isArray(copy)))) {
src = target[name]; // Ensure proper type for the source value
if (copyIsArray && !Array.isArray(src)) {
clone = [];
} else if (!copyIsArray && !jQuery.isPlainObject(src)) {
clone = {};
} else {
clone = src;
}
copyIsArray = false; // Never move original objects, clone them
// 不移动原始对象,而是克隆他们。 这涉及到js中对象引用的问题(类似c++的引用),如果修改了某引用,其原始对象也会发生改变
target[name] = jQuery.extend(deep, clone, copy); // 递归入口, // Don't bring in undefined values
} else if (copy !== undefined) {
target[name] = copy; // 一般直接在此完成挂载(由于jQuery类及对象都是类数组模式,因此使用数组下标进行赋值即可完成拓展)
}
}
}
} // Return the modified object
return target;
};

上述代码清晰地展示了成员是怎么挂载到类和对象上的,在前面提到jQuery创建时返回形式为类数组,而挂载的本质还是对类数组的插入更新。

extend方法分为jQuery.extend()和jQuery.fn.extend(),前者是直接挂载在类上,后者是挂载在对象的原型对象上。两者要区分开。并且由于作用域的不同,内部函数和挂载成员不要混淆。

知识点:

  1. 链式调用的由来:
  2. 挂载的本质:
  3. 区分成员引用: js 中的函数其实是对象,函数名是对 Function 对象的引用。
  4. 类成员与对象成员的增删改


挂载实例和内部函数

下面的代码是一些类静态成员与文件内部函数的定义,这里挑一些难以理解的介绍一下:

// 静态成员挂载:jQuery.extend
expando: "jQuery" + (version + Math.random()).replace(/\D/g, ""), // 一个用来标记的实时变量 // 判断是否是纯对象
isPlainObject: function (obj) {
var proto, Ctor; // Detect obvious negatives
// Use toString instead of jQuery.type to catch host objects
// 使用toString.call(obj)方法获取对象的类型 ======完整形式:Object.prototype.toString.call(obj)
if (!obj || toString.call(obj) !== "[object Object]") {
return false;
} proto = getProto(obj); // Objects with no prototype (e.g., `Object.create( null )`) are plain // 没有原型的对象是实体类
if (!proto) {
return true;
} // Objects with prototype are plain iff they were constructed by a global Object function // 被全局对象函数创造的对象也是实体类
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && fnToString.call(Ctor) === ObjectFunctionString; // 判断Ctor是否为Object,详情请转至fnToString定义处
}, // 注入函数调用函数
each: function (obj, callback) { // 细心的同学肯定在前面看到过each函数,不过是挂载在类原型对象中的,其内部还是调用的此方法
var length, i = 0; if (isArrayLike(obj)) {
length = obj.length;
for (; i < length; i++) {
if (callback.call(obj[i], i, obj[i]) === false) { // 将obj的每个成员传入callback并执行,最后返回新的obj(就是对原对象进行处理得到新对象)
break;
}
}
} else {
for (i in obj) {
if (callback.call(obj[i], i, obj[i]) === false) { // 大家有没有发现诸多 xx.call() 形式的函数?
break;
}
}
} return obj;
}, // 处理结果映射函数
map: function (elems, callback, arg) {
var length, value,
i = 0,
ret = []; // Go through the array, translating each of the items to their new values
if (isArrayLike(elems)) {
length = elems.length;
for (; i < length; i++) {
value = callback(elems[i], i, arg); // 对函数进行某种处理 if (value != null) {
ret.push(value); // 将处理后的值进行保存(可能是上述操作不会改变原数组的值,因此需要保存处理结果)
}
} // Go through every key on the object,
} else {
for (i in elems) {
value = callback(elems[i], i, arg); if (value != null) {
ret.push(value);
}
}
} // Flatten any nested arrays
// 嵌套数组扁平化
return flat(ret);
}, // 内部成员函数:
if (typeof Symbol === "function") { // ES6 新的数据类型 Symbol
// 添加了一个fn迭代器,等价于arr数组的默认迭代方法,此后fn迭代器会迭代arr数组
jQuery.fn[Symbol.iterator] = arr[Symbol.iterator];
} // Populate the class2type map
// 普通的对象赋值,但是意义在于将toString.call()能获得的所有类型都列了出来,避免了魔鬼变量的出现
jQuery.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),
function (_i, name) {
class2type["[object " + name + "]"] = name.toLowerCase();
}
); // 类数组判断
function isArrayLike(obj) {
var length = !!obj && "length" in obj && obj.length, // 连续与,只有obj存在则检验len属性存在,只有length存在才检验长度,避免出现报错
type = toType(obj); if (isFunction(obj) || isWindow(obj)) { // 函数或window,直接返回false
return false;
} return type === "array" || // 是数组
length === 0 || // 或者长度为0
typeof length === "number" && length > 0 && (length - 1) in obj; // 或者长度是数字且len-1在obj中,因为稀疏数组的遍历会存在跳过空值
}

xxx.call():

xxx.call()的原始形式是obj1.(function).call(obj2,args, ...),其本质就是将obj1的function放到obj2上使用。obj2在传入函数参数时不能省略,当其省略时被认为是全局对象。

通过xxx.call()可以实现继承,此外还有obj1.(function).apply(obj2, [], args, ...),其作用类似于call(),详情请转至此处

知识点:

  1. POJO对象(纯粹实体对象)的判断:
  2. 函数作为参数注入执行(解耦合):
  3. 常用的操作: 如数组创建、数组内元素定位、类数组合并、xx.call()形式调用、抛出异常函数
  4. ES6中新数据类型Symbol:
  5. 类数组


结语

至此本章也就结束了,在此大致讲述了jQuery对象的创建过程,以及类和对象成员的挂载,期间穿插着介绍了一些内部成员函数。通过这些学习,你已经对一些jQuery基本函数有了一定的了解,了解到了一些其他JS知识,也可以自定义一些组件,下一章我们将跳回jQuery对象创建函数,并沿着find()函数往下探索,欢迎来到jQuery的核心世界之一 ---- Sizzle插件。