$()函数到底做的什么
jQuery在前端领域路人皆知,对于一向喜欢玩js的博主来说,虽然能力有限,但是还是很喜欢研究他的做为。那么一个简单的美元符号$与一对常见的()括号,jQuery底层到底做了哪些工作,如果你是前端新人,并喜欢刨根问底,你可以看一下下面的介绍。如果你是有经验的牛人,你可以指出错误,毕竟博主还是个半瓶子醋,没法完全理解。
一、函数调用
$(selector, context) : $()这一个简单的代码,其实调用了jQuery的构造函数,其中的selector和context是传递给jQuery构造函数的参数表达式,前者代表着选择器字符串,后者是上下文,jQuery会根据这两个参数,在页面中找到相应的dom对象,并包装成jQuery对象返回给我们。
var jQuery = function(selector, context) {
return new jQuery.fn.init(selector, context, rootjQuery);
} jQuery.fn = jQuery.prototype = {
init: function (selector, context, rootjQuery) {
// ...
}
} jQuery.fn.init.prototype = jQuery.fn;
调用jQuery()后,返回了一个new jQuery.fn.init(),其实就是jQuery实例,因为jQuery.fn.init 的原型指向了jQuery.fn,也就是jQuery的原型,这部分不需要解释了,是原型链的问题了。
二、步入函数
就这样jQuery进入到了init函数中,接下来看下面代码
// 如果没有参数,返回当前jQuery对像
if (!selector) {
return this;
}
//如果selector有nodeType属性,则是dom对象,返回包装后的jQuery对象
if (selector.nodeType) {
this.context = this[0] = selector;
this.length = 1;
return this;
}
//如果selector是body,由于body只有一个,并且我们可以明确的指出,所以直接把body元素包装后返回jQuery对象
if (selector === "body" && !context && document.body) {
this.context = document;
this[0] = document.body;
this.selector = selector;
this.length = 1;
return this;
}
由此我们可以知道 $()反回了个空的jQuery对象, $(document.body) 走了第二个if返回了body对象的jQuery对象, $('body')走了第三个if也返回了body对象的jQuery对象。
接下来,jQuery构造函数进入的最难的部分,请看着源码来读下面的介绍。
一、如果selector是字符串类型,有两种情况:
1. 如果是dom元素字符串,如$('<div>') 或者 $('<div>', {title: 'good'})等情况,走一种处理方法
2. 如果是单独的div选择器,如$('#box') ,走一种处理方法
3.1. 如果不是上面两种情况,如果context参数存在并且是jQuery对象,则用这个jQuery对象查找selector,如 $('span', $('body')) 相当于 $('body').find('span')
3.2. 如果不是1、2两种情况,并没有context参数,就用根jQuery对象(document的jQuery对象)查找selector,如: $('span') 相当于 $(document).find('span')
4. 其他情况,就是context是dom对象时,用context包装成jQuery对象查找selector, 如$('span', document.body) => $(document.body).find('span')
二、如果selector是函数,那么进入ready方法,等待dom加载完毕执行函数,也就是$(function () {...})
三、如果以上都不是,并且selector有selector和context属性,这代表什么呢?当然是传进来的jQuery对象了,如果是这样,那jQuery返回一个类似的新对象。。。
三、难区详解(selector是字符串类型的1、2两种情况)
由容易到难,现说是id选择器的情况,再详细说是dom元素字符串的情况。。。
1. selector是个id选择器
elem = document.getElementById(match[2]); // 黑莓手机兼容解决
if (elem && elem.parentNode) {
// ie 欧朋 选择name解决
if (elem.id !== match[2]) {
return rootjQuery.find(selector);
} this.length = 1; this[0] = elem;
} this.context = document;
this.selector = selector;
return this;
如果是id选择器,就直接选出元素包装成jQuery对象返回。
2. selector是dom元素字符串
这里又分为3种情况:
1. 如果selector是一个简单的dom字符串,如: $('<div>'), $('<div></div>')
1.1 如果第二个参数context是个纯js对象,也就是键值对,保存着属性和属性值,如$('<div>', {title: 'good'}) ,那么创建dom对象并赋值这些属性
1.2 如果第二个参数存在并且是dom对象,那么就用这个dom对象创建selector这个dom, 如果context不是dom对象,就用document创建selector这个dom
2. 如果selector并不是一个简单的字符串,其中有属性赋值在里面,如 $('<div id="box">'),进入 jQuery.buildFragment 方法构造dom对象结构
四、dom对象的构造中转站
这里不贴代码了,同学们可以看着源码走下去。
jQuery.buildFragment中,jquery先取到要转化成dom的字符串,和要插入到真是dom的dom对象,也就是context参数或者是document对象。
然后根据各种条件判断,这个dom对象是否是可缓存的,如果是可缓存,查找缓存区是否已经有了,如有有了,直接返回,如果没有,那么进入jQuery.clean方法创建并缓存。
五、dom对象的构造工坊
jQuery.clean 函数中, jQuery还是先确定下要创建的dom字符串数组,和在哪个dom下创建。
进入创建循环,看到这样一句话
if (!rhtml.test(elem)) {
elem = context.createTextNode(elem);
}
博主看rhtml正则,觉得这个是判断是不是实体字符和html标签,如果不是实体字符或html标签,创建文本节点。
往下看,如果不是实体字符,jQuery把这个字符串转化为开闭式的合格dom字符串并且取到了dom的名称
elem = elem.replace(rxhtmlTag, "<$1></$2>"); var tag = (rtagName.exec(elem) || ["", ""])[1].toLowerCase()
由于tr td option等标签,是依赖于上级的标签,所以jQuery在这里需要考虑如果要创建他们,需要先创建他们上级,所以下面代码有个
wrap = wrapMap[tag] || wrapMap._default
那我们去查看wrapMap这个对象是什么
wrapMap = {
option: [1, "<select multiple='multiple'>", "</select>"],
legend: [1, "<fieldset>", "</fieldset>"],
thead: [1, "<table>", "</table>"],
tr: [2, "<table><tbody>", "</tbody></table>"],
td: [3, "<table><tbody><tr>", "</tr></tbody></table>"],
col: [2, "<table><tbody></tbody><colgroup>", "</colgroup></table>"],
area: [1, "<map>", "</map>"],
_default: [0, "", ""]
}
我们来到了这里,看到了这个对象。我们可以看到,这个对象包含了那些需要上级dom才能存活的dom列表,他们的值是一个数组,数组第一个表示的层级关系,比如option是select下的第一级, tr是table下的第二级,数组第二个和第三个元素很明显分别表示了开口标签闭口标签,那么有什么用呢?继续往下看
div.innerHTML = wrap[1] + elem + wrap[2];
看到下面几行,终于茅塞顿开,原来是这样创建的依赖上级元素的 dom元素,前后上级标签中间夹着需要创建dom的字符串,把整个字符串赋值给一个div,构成了完整的dom。知道了后,往回走。
safeChildNodes = safeFragment.childNodes
上面还有一行这个是什么意思呢?我们去看看safeFragment把
// h5标签
nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" +
"header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", safeFragment = createSafeFragment(document); function createSafeFragment(document) {
var list = nodeNames.split("|"),
safeFrag = document.createDocumentFragment(); if (safeFrag.createElement) {
while (list.length) {
safeFrag.createElement(
list.pop()
);
}
}
return safeFrag;
}
我们找到了这三个重要的表达式,哦,原来这个safeFragment保存着所有h5的标签
if (context === document) {
safeFragment.appendChild(div);
} else {
createSafeFragment(context).appendChild(div);
}
接下来jQuery保存了之前我们创建的div,还不知道要干什么,继续往下看吧。
while (depth--) {
div = div.lastChild;
}
接下来,jQuery根据层级关系,一层一层的把外部容器去掉,只留下最后一层包裹着我们想要的那个dom。
下面,就是对ie的两个兼容,ie下自动生成tobody,jQuery做了兼容,ie下破坏首行缩进,jQuery做了兼容。
elem = div.childNodes;
if (div) {
div.parentNode.removeChild(div); if (safeChildNodes.length > 0) {
remove = safeChildNodes[safeChildNodes.length - 1]; if (remove && remove.parentNode) {
remove.parentNode.removeChild(remove);
}
}
}
}
保存我们需要的dom节点后,把此次循环的div删掉。把插入到safeFragment的div也删掉,清空干净,马上要进入下一循环
var len;
if (!jQuery.support.appendChecked) {
if (elem[0] && typeof(len = elem.length) === "number") {
for (j = 0; j < len; j++) {
findInputs(elem[j]);
}
} else {
findInputs(elem);
}
}
上面这一个if针对的 checkbox和radio元素在ie下checked失效的bug
if (elem.nodeType) {
ret.push(elem);
} else {
ret = jQuery.merge(ret, elem);
}
终于,保存了此次dom元素到ret数组中。然后下面的一个if是对script标签的插入处理,博主没有去看,因为不常用啊。。
终点
终于,jQuery方法走完了,走的好慢好艰辛,走过了这次行程,下一次是不是就很好走了O(∩_∩)O~。