浅谈jQuery构造函数

$()函数到底做的什么

  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 >'),进入 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~。