jQuery方法
发布在读懂jQuery2014年8月20日view:5373
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

第一篇介绍了jQuery对象,这一篇来看下jQuery对象是怎么生成的。下面的代码是生成jQuery对象的源码,小Y把用到的代码取了出来,加了注释:


// 这个rootjQuery变量就是所有jQuery对象最终指向的对象
var rootjQuery = $(document),    

    // 定义一个本地的jQuery变量,我们平时用的$() 就是这个函数
    jQuery = function( selector, context ) {
        // jQuery对象其实就是init构造函数的增强版
        // 这里使用了工厂模式,我们不需要用这样的语法构造jQuery对象:new jQuery( selector, context)
        return new jQuery.fn.init( selector, context, rootjQuery );
    },

    // 正则,匹配单个的HTML tag
    rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,

    // 正则,判断是否为HTML标签或者ID选择器
    rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/;

// 这里给jQuery.prototype设置了一个别名jQuery.fn
jQuery.fn = jQuery.prototype = {

    init: function( selector, context, rootjQuery ) {
        var match, elem, ret, doc;
        // Handle $(""), $(null), $(undefined), $(false)
        // 如果传入的参数为空,则直接返回this
        if ( !selector ) {
            return this;
        }

        // Handle $(DOMElement)
        // 处理单个的DOM Element $(DOMElement),将context和第一个元素都设为这个DOM Element
        if ( selector.nodeType ) {
            this.context = this[0] = selector;
            this.length = 1;
            return this;
        }

        // 处理HTML字符串,可以先看后面的,把这段留到最后
        if ( typeof selector === "string" ) {
            // 判断是否为 /^<.+$>/
            if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
                // 假设以<开头,以>结尾的字符串为HTML并跳过正则表达式检测
                match = [ null, selector, null ];
            } else {
                match = rquickExpr.exec( selector );
            }
            // 匹配HTML或确保ID没有指定context
            // match[1]不为null,则为HTML字符串,match[2]不为null,则为元素id
            // 此处不处理 id + context 
            if ( match && (match[1] || !context) ) {
                // HANDLE: $(html) -> $(array)
                // 如果match[1]有值,则为HTML,jQuery解析并构造元素。
                if ( match[1] ) {
                    context = context instanceof jQuery ? context[0] : context;
                    doc = ( context && context.nodeType ? context.ownerDocument || context : document );
                    // scripts被设为true用以向后兼容,表示保留HTML中的脚本
                    // selector是由文档碎片中的childnodes组成的数组
                    selector = jQuery.parseHTML( match[1], doc, true );

                    // 如果match[1]为空的单标签元素(如:
<\div>)且context为对象字面量 // 如果context对象不为空,则将对象中的属性添加到selector数组中仅有的dom节点中 if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { this.attr.call( selector, context, true ); } // merge函数的参数应该为两个数组,目的是将第二个数组中的项合并到第一个数组, // 而this并不是一个数组,this是选择器init构造函数的实例对象,该对象继承 // jQuery.prototype对象中的length属性(默认为0),因此可以理解好merge函数源码。 // 将selector中的DOM项合并到this对象中,并返回该对象 return jQuery.merge( this, selector ); // HANDLE: $(#id) // 这里是最好理解的,直接通过ID获取 } else { elem = document.getElementById( match[2] ); // 这里检测elem.parentNode是因为黑莓 4.6会将不再存在于文档中的元素返回 #6963 if ( elem && elem.parentNode ) { // ie6,7和Opera存在此bug,当一个标签name和一个标签id值相等时, // document.getElementById(#id)函数将返回提前出现的标签元素 if ( elem.id !== match[2] ) { // 如果存在以上Bug,则返回由find函数返回的document文档的后代元素 return rootjQuery.find( selector ); } // 如果不存在上面的情况,则将元素直接放入jQuery对象中 this.length = 1; this[0] = elem; } this.context = document; this.selector = selector; return this; } // HANDLE: $(expr, $(...)) // context不存在或者context为jQuery对象 } else if ( !context || context.jquery ) { return ( context || rootjQuery ).find( selector ); // HANDLE: $(expr, context) // context为className或者dom节点元素 // 等同于jQuery(context).find(selector) } else { return this.constructor( context ).find( selector ); } // 处理 $(function),即DOM ready的简写 // 等同于$(document).ready(fn) ,因为rootjQuery就是$(document) } else if ( jQuery.isFunction( selector ) ) { return rootjQuery.ready( selector ); } // 处理$(jQuery对象),其实就是复制一下 if ( selector.selector !== undefined ) { this.selector = selector.selector; this.context = selector.context; } // 处理 $(jQuery对象)或$(HTML Collection) // 当第一个参数selector为jQuery对象或HTML Collection时, // 将selector中的DOM elements合并到this对象中,并返回this对象 return jQuery.makeArray( selector, this ); } } // 通过将jQuery.fn.init的prototype属性指向jQuery.fn,所有jQuery对象就可以使用jQuery.fn中的方法, // 这也是jQuery插件机制的关键 jQuery.fn.init.prototype = jQuery.fn; // 此对象为document的jQuery对象,所有的jQuery对象最终都将指向它 // 可以在chrome dev tools中观察prevObject rootjQuery = jQuery(document);

前面正则的示意图:

/^<(\w+)\s*\/?>(?:<\/\1>|)$/

Regular expression visualization

/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/

Regular expression visualization

下面来看一下init函数中接受的参数:

  1. null, undefined, false, ””这些为空的值,直接返回this
  2. DOM Element, 单个的DOM元素,则将context和jQuery对象的第一个元素设为DOM元素
  3. 字符串
    1. HTML Tag或未指定context的ID选择器
    2. 未指定context或context为jQuery对象
    3. 指定了非jQuery对象的context,等同于$(context).find(expr)
  4. 函数,等同于DOM Ready,亦即$(document).ready(fn);
  5. HTML Collection或者jQuery对象,如果是jQuery对象,只做简单复制

接下来让我们考虑一下为什么jQuery要这么实现吧。或者如果让你写一个jQuery库,你打算怎么实现呢?

第一步:定义一个jQuery方法,将jQuery对象的方法放在prototype对象中


var jQuery = function() {

};
jQuery.fn = jQuery.prototype = {
    jquery: '1.8.1',
    // jQuery object methods
    someMethods: ....
};
window.$ = window.jQuery = jQuery;

这样一来,如果我们要创建一个jQuery对象并调用prototype中的方法,必须这样:


new $(selector).someMethods();
$(selector).someMethods();    //TypeError: Cannot read property 'someMethods' of undefined

第二种方法因为忘记了new关键词而报错,而且每次都要new一个jQuery对象很麻烦,因此,我们对jQuery方法做一些改进。

第二步:使用工厂方法 在设计模式中,工厂方法模式很常见,我们也使用工厂方法模式来改进上面的代码。


var jQuery = function() {
    // using Factory Pattern
    return new F();
},
F= function(){
    // do the jQuery.fn.init method's work;
    return this;
};
F.fn = F.prototype = {
    jquery: '1.8.1',
    // jQuery object methods
    someMethods: ....
};
window.$ = window.jQuery = jQuery;

现在,我们可以不用每次都new一个jQuery对象啦。

$(selector).someMethods()
console.log($(selector).jquery);  // output : 1.8.1

现在不会报错啦,而且jQuery对象也能访问prototype中的方法啦。

但是现在,我们无法扩展jQuery对象了,那么为了达到扩展的目的,要再加上这么一句:


var jQuery = function() {
    // using Factory Pattern
    return new F();
},
F= function(){
    // do the jQuery.fn.init method's work;
    return this;
};
F.fn = F.prototype = {
    jquery: '1.8.1',
    // jQuery object methods
    someMethods: ....
};

jQuery.fn = jQuery.prototype = F.fn;   // 为了扩展jQuery对象

window.$ = window.jQuery = jQuery;

至此,代码的功能已经能实现了。但是不知道John大神是处于什么目的,把F挂到了jQuery.fn.init上,这里我们只好猜啦,经过网友们的帮助,小Y的猜想是这样的:

为了逻辑上的清晰,jQuery对象的扩展放在F.fn上,而jQuery的扩展应该放在F上,那么extend方法应该这么定义:

F.extend = F.fn.extend = function() {};

但是,在外部,只暴露了$和$.fn,即我们无法访问到F,也就无法调用F.extend来扩展jQuery自身。而又不能将jQuery指向F,那就死循环了,因此将F挂到jQuery的原型中就是一个可选的方法。

因此,我们用jQuery.fn.init替换上面代码中的F,就有:


var jQuery = function() {
    // using Factory Pattern
    return new jQuery.fn.init();
},
jQuery.fn.init= function(){
    // do the jQuery.fn.init method's work;
    return this;
};
jQuery.fn.init.fn = jQuery.fn.init.prototype = {
    jquery: '1.8.1',
    // jQuery object methods
    someMethods: ....
};

jQuery.fn = jQuery.prototype = jQuery.fn.init.fn;

window.$ = window.jQuery = jQuery;

整理代码,得到jQuery的写法:


var jQuery = function() {
    // using Factory Pattern
    return new jQuery.fn.init();
};
jQuery.fn = jQuery.fn = {
    jquery: '1.8.1',
    init= function(){
        // do the jQuery.fn.init method's work;
        return this;
    },
    // jQuery object methods
    someMethods: ....
};

jQuery.fn.init.fn = jQuery.fn;   

window.$ = window.jQuery = jQuery;

这样的理解不知道算不算对,如果有不对的地方,或者有更好的解释,请大家不吝赐教。

但是,这里有一个疑问,要对外暴露F的目的是为了扩展F,那么要让F可以在外部被扩展,可以这么实现:


F.fn.extend = function(){
    // current 'extend' method code in jQuery
};
jQuery.extend = function(){
    F.fn.extend.apply(F, arguments.slice());
};

不知道那么写是不是John大神的个人爱好 囧rz

小广告:我的博客 YuuuuC.me #^_^#

参考: 1. http://www.cnblogs.com/baochuan/archive/2012/11/22/2782343.html 2. http://www.jb51.net/article/33818.htm 3. http://cnodejs.org/topic/53e73361977012ba5590e66d

评论
发表评论
暂无评论
WRITTEN BY
YuC_C
陈胜后裔吗?博客:lovecicy.com
TA的新浪微博
PUBLISHED IN
读懂jQuery

用了这么久jQuery了,想看看到底jQuery怎么写的,为什么这么牛呢。本栏解读的版本为1.8.1,本人(小Y)才疏学浅,如有疏漏、错误之处,还望各位大牛雅正。个人博客:YuuuuC

我的收藏