jQuery分析(3) - jQuery.fn.init
1.前言
上一篇jQuery分析(2)中了解了jQuery库的骨架实现原理,这就好比摇滚音乐,摇滚音乐不是某种音乐他就像一个音乐盒子,里面包含了各种不同的摇滚风格(山地、朋克、乡村、流行、硬摇、金属、迷幻等)。那么上一篇只是大致了解了jQuery的基本形状,从这篇文章开始会深入jQuery库的各种函数,深入详细的去了解他,那将值得慢慢探索,发现新的神奇好玩的东西。
2.辅助函数
在jQuery.fn.init方法里面使用到了一些jQuery的静态函数,在这里提前统一的介绍
- jQuery.merge 合并两个数组,将第二个参数数组合并到第一个参数数组中。
- jQuery.parseHTML 解析html字符串,第一个参数html字符串,第二个参数是产生fragment的context,第三个参数是否忽略scripts默认忽略
- isPlainObject 判断一个参数是否为javascript对象即{}
- jQuery.isFunction 判断一个参数是否为函数
- jQuery.makeArray 合并数组(内部使用)
3.jQuery.fn.init 函数概括👻
下面图是jQeury的构造函数参数即$()调用的参数种类集合图
下面的代码是可能处理各种参数的方式,多余的代码我已删除掉,下面代码清晰看到selector无非就是三种类型:1、字符串 2、DOMElement 3、函数。下面将会就这三种类型进行详细深入的分析他们的实现原理,这将需要一步一步来理解,首先脑子里要清晰每一步做了什么为什么这样做,这样一步一步下来才能更好的去理解jQuery的写法。
// 匹配html标签写法和id选择器
// 第一个分组是 <div> 中的div,第二个分组是#id中的id
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
jQuery.fn.init = function(selector, context, root) {
var match, elem;
// 没有selector将会返回当前this,以便创建空的jQuery对象在后面会用到。
if (!selector) {
return this;
}
// 获得初始化文档的jQuery对象
root = root || rootjQuery;
// 处理参数为字符串参数
if (typeof selector === "string") {
// 处理参数为DOM节点
} else if (selector.nodeType) {
// 处理参数为function
} else if (jQuery.isFunction(selector)) {
}
// 处理参数为NodeLists
return jQuery.makeArray(selector, this);
};
// init函数继承jQuery
init.prototype = jQuery.fn;
// 初始化document为jQuery对象
rootjQuery = jQuery( document );
4.参数为字符串类型分解🐢
因为这个jQuery.fn.init函数代码很多所以单独的参数类型会把他的代码单独提出来分解,下面看提出来的参数为字符串的代码。
对于字符串参数的几种调用方法参见上面脑图
// 匹配html标签写法和id选择器
// 第一个分组是 <div> 中的div,第二个分组是#id中的id
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
// 参数为字符串处理方式
if (typeof selector === "string") {
if (selector.charAt(0) === "<" &&
selector.charAt(selector.length - 1) === ">" &&
selector.length >= 3) {
// 字符串是单标签的DOM格式直接创建正则匹配的格式以跳过正则匹配以节约性能
match = [null, selector, null];
} else {
// 匹配到DOM字符串 或者ID选择器
match = rquickExpr.exec(selector);
}
// 如果selector是一个html字符串或者是一个ID选择器
if (match && (match[1] || !context)) {
// html字符串解析
if (match[1]) {
context = context instanceof jQuery ? context[0] : context;
// 解析html(单标签或多标签)
jQuery.merge(this, jQuery.parseHTML(
match[1],
context && context.nodeType ? context.ownerDocument || context : document,
true
));
// 构建html元素时传递了第二个参数为一个对象,那么会对对象的key和value进行解析
if (rsingleTag.test(match[1]) && jQuery.isPlainObject(context)) {
for (match in context) {
// Properties of context are called as methods if possible
if (jQuery.isFunction(this[match])) {
this[match](context[match]);
// ...and otherwise set as attributes
} else {
this.attr(match, context[match]);
}
}
}
return this;
// id选择器处理方式
} else {
elem = document.getElementById(match[2]);
// Check parentNode to catch when Blackberry 4.6 returns
// nodes that are no longer in the document #6963
if (elem && elem.parentNode) {
// Handle the case where IE and Opera return items
// by name instead of ID
if (elem.id !== match[2]) {
return rootjQuery.find(selector);
}
// Otherwise, we inject the element directly into the jQuery object
this.length = 1;
this[0] = elem;
}
this.context = document;
this.selector = selector;
return this;
}
// 其他选择器(class、element、attr)等
} else if (!context || context.jquery) {
return (context || root).find(selector);
// 其他选择器(class、element、attr)等且有context(一个DOMElement)
} else {
return this.constructor(context).find(selector);
}
}
- 4-1.字符串处理
- 4-1-1.selector为单标签的html会跳过正则表达式匹配,直接构造出一个正则表达结果结果放置match变量中。代码
- 4-1-2.匹配selector是否为为多标签或者ID选择器结果放置match变量中代码
- 4-1-3.selector是一个html字符串会对他们进行解析(单标签/多标签)代码,如果还传递了额外的props属性也会在解析和创建完DOM节点后附加到这个DOM上代码
- 4-1-4.selector为ID选择器则直接调用
document.getElementById
进行ID选择元素,把选择到的元素放入this[0]中,随后修正length、context、selector即可代码 - 4-1-5.selector为其他选择器(class、element、attr等)并且没有给定第二个参数(context)或者第二个参数(context)是一个jquery对象那么会调用
(context || root).find(selector)
进行查找元素代码 - 4-1-6.selector为其他选择器(class、element、attr等)并且第二个参数(context)为一个DOMElement会先构建context为一个jQuery对象再利用这个对象进行.find(selector)查找代码
5.字符串解析流程中涉及的函数分析
4-1把所有的字符串解析流程罗列了出来,在这个流程中所涉及了一些关键的函数在这里给拆分解析一下。
- 5-1.jQuery.parseHTML解析html字符串为DOM节点
// data: string of html
// context (optional): If specified, the fragment will be created in this context,
// defaults to document
// keepScripts (optional): If true, will include scripts passed in the html string
jQuery.parseHTML = function( data, context, keepScripts ) {
if ( !data || typeof data !== "string" ) {
return null;
}
// 修正参数,只有2个参数情况下忽略context
if ( typeof context === "boolean" ) {
keepScripts = context;
context = false;
}
// 修正context默认为document
context = context || document;
var parsed = rsingleTag.exec( data ),
scripts = !keepScripts && [];
// Single tag
if ( parsed ) {
return [ context.createElement( parsed[ 1 ] ) ];
}
parsed = buildFragment( [ data ], context, scripts );
// 移除已经执行过的脚本
if ( scripts && scripts.length ) {
jQuery( scripts ).remove();
}
// 合并并返回标dom集合的数组
return jQuery.merge( [], parsed.childNodes );
};
这个函数一共有三个参数,参数一是一个html字符串,参数二是创建fragment的context,参数三是表示是否保留scripts脚本默认为false
// 单标签
var parsed = rsingleTag.exec( data ),
scripts = !keepScripts && [];
// Single tag
if ( parsed ) {
return [ context.createElement( parsed[ 1 ] ) ];
}
如果data参数为一个单标签html字符串("
// 多标签
parsed = buildFragment( [ data ], context, scripts );
如果data参数为一个多标签html字符串("
最后返回一个数组元素,里面是所有解析好的DOM元素
- 5-2.buildFragment 创建文档片
其实在整个selector为字符串参数的代码处理中,buildFragment应该还是算代码比较多的了,其他那些class、element、attr等都是Sizzle选择器引擎搞定了,所以buildFragment还是一个比较有看头的函数,其中文档碎片技术和必须要外包裹标签的创建方法其实我们平时编码时也会经常使用,可以借鉴一二。
/*
buildFragment 重要的参数是前面3个
elems 一个待转换的html字符串
context 转换上下文
scripts 是否忽略script标签
*/
function buildFragment(elems, context, scripts, selection, ignored) {
var j, elem, contains,
tmp, tag, tbody, wrap,
l = elems.length,
// Ensure a safe fragment
// 创建文档碎片
safe = createSafeFragment(context),
nodes = [],
i = 0;
for (; i < l; i++) {
elem = elems[i];
if (elem || elem === 0) {
// Add nodes directly
// 如果在elems中的某个数组元素是对象直接添加到nodes
if (jQuery.type(elem) === "object") {
jQuery.merge(nodes, elem.nodeType ? [elem] : elem);
// Convert non-html into a text node
// 转换非html的字符串为文本节点
} else if (!rhtml.test(elem)) {
nodes.push(context.createTextNode(elem));
// Convert html into DOM nodes
} else {
//给文档碎片创建一个元素,用来装接下来我们需要的html字符串
tmp = tmp || safe.appendChild(context.createElement("div"));
// Deserialize a standard representation
// 取得标签名称,并转为小写
tag = (rtagName.exec(elem) || ["", ""])[1].toLowerCase();
// 需要其他元素包裹的标签
wrap = wrapMap[tag] || wrapMap._default;
//把我们的html字符串放入刚刚创建文档碎片的div中形成dom元素
// 如果需要包裹元素则把html字符串进行包裹 <td>123</td> => <table><tbody><tr><td>abc</td></tr></tbody></table>
tmp.innerHTML = wrap[1] + jQuery.htmlPrefilter(elem) + wrap[2];
// Descend through wrappers to the right content
// 取得刚刚创建的元素
j = wrap[0];
while (j--) {
tmp = tmp.lastChild;
}
// Manually add leading whitespace removed by IE
if (!support.leadingWhitespace && rleadingWhitespace.test(elem)) {
nodes.push(context.createTextNode(rleadingWhitespace.exec(elem)[0]));
}
// Remove IE's autoinserted <tbody> from table fragments
if (!support.tbody) {
// String was a <table>, *may* have spurious <tbody>
elem = tag === "table" && !rtbody.test(elem) ?
tmp.firstChild :
// String was a bare <thead> or <tfoot>
wrap[1] === "<table>" && !rtbody.test(elem) ?
tmp :
0;
j = elem && elem.childNodes.length;
while (j--) {
if (jQuery.nodeName((tbody = elem.childNodes[j]), "tbody") &&
!tbody.childNodes.length) {
elem.removeChild(tbody);
}
}
}
// 把创建好的dom节点也就是tmp的子节点合并到nodes中
jQuery.merge(nodes, tmp.childNodes);
// Fix #12392 for WebKit and IE > 9
tmp.textContent = "";
// Fix #12392 for oldIE
while (tmp.firstChild) {
tmp.removeChild(tmp.firstChild);
}
// Remember the top-level container for proper cleanup
tmp = safe.lastChild;
}
}
}
// Fix #11356: Clear elements from fragment
// 清除文档碎片中的元素
if (tmp) {
safe.removeChild(tmp);
}
// Reset defaultChecked for any radios and checkboxes
// about to be appended to the DOM in IE 6/7 (#8060)
if (!support.appendChecked) {
jQuery.grep(getAll(nodes, "input"), fixDefaultChecked);
}
i = 0;
while ((elem = nodes[i++])) {
// Skip elements already in the context collection (trac-4087)
if (selection && jQuery.inArray(elem, selection) > -1) {
if (ignored) {
ignored.push(elem);
}
continue;
}
// 元素是否已经包含在document中
contains = jQuery.contains(elem.ownerDocument, elem);
// Append to fragment
// 将创建好的元素再次添加到文档碎片中,并取得scirpt标签
tmp = getAll(safe.appendChild(elem), "script");
// Preserve script evaluation history
if (contains) {
setGlobalEval(tmp);
}
// Capture executables
// 收集要执行的脚本
if (scripts) {
j = 0;
while ((elem = tmp[j++])) {
if (rscriptType.test(elem.type || "")) {
scripts.push(elem);
}
}
}
}
tmp = null;
// 返回创建好的文档碎片
return safe;
}
关于buildFragment函数里面还有一些兼容性的解决方案还没分析到,后面再做吧。至此关于jQuery.fn.init函数的构造流程也就分析完毕。