jQuery源码解读----part 2

分离构造器

经过new操做符构建一个对象,通常通过四步:javascript

A.建立一个新对象css

B.将构造函数的做用域赋给新对象(因此this就指向了这个新对象)html

C.执行构造函数中的代码java

D.返回这个新对象jquery

最后一点就说明了,咱们只要返回一个新对象便可。其实new操做符主要是把原型链跟实例的this关联起来,这才是最关键的一点,因此咱们若是须要原型链就必需要new操做符来进行处理。不然this则变成window对象了。算法

改造jQuery无new的格式,咱们能够经过instanceof判断this是否为当前实例:设计模式

var $$ = ajQuery = function(selector) {
    if(!(this instanceof ajQuery)){ // 第二次看仍是以为这一句很NB
        return new ajQuery(selector);
    }
    this.selector = selector;
    return this
}

但在jQuery实际上采起的手段是把原型上的一个init方法做为构造器,这样貌似更节省代码空间?数组

var $$ = ajQuery = function(selector) {
    //把原型上的init做为构造器
    return new ajQuery.fn.init( selector );
}

ajQuery.fn = ajQuery.prototype = {
    name: 'aaron',
    init: function() {
        console.log(this)
    },
    constructor: ajQuery
}

但这样子还缺点东西,init是ajQuery原型上做为构造器的一个方法,那么其this就不是ajQuery了,因此this就彻底引用不到ajQuery的原型了,因此这里经过new把init方法与ajQuery给分离成2个独立的构造器。app


静态与实例方法共享设计

接着上面分割出2个构造器的疑问,来看看jQuery的一个遍历接口:框架

$(".aaron").each()   //做为实例方法存在
$.each()             //做为静态方法存在

看似实例和静态方法须要两个函数来实现,但在jQuery源码中是这样的:

jQuery.prototype = {
    // 调用实例方法其实是将实例对象this做为一个参数,调用对应的静态方法,这样就造成了共享
    each: function( callback, args ) {
        return jQuery.each( this, callback, args );
    }
}

实例方法取于静态方法,换句话来讲这是静态与实例方法共享设计,静态方法挂在jQuery构造器上,原型方法挂在哪里呢?------jQuery经过new原型prototype上的init方法看成构造器,那么init的原型链方法就是实例的方法了,因此jQuery经过2个构造器划分2种不一样的调用方式一种是静态,一种是原型。

那若是要将2个构造器原型关联起来,关键就是靠下面一句:

ajQuery.fn.init.prototype = ajQuery.fn

540905880001daac05540230

这样init构造出来的实例对象也可以继承jQuery原型上的方法了。


方法链式调用的实现

jQuery的核心理念是Write less,Do more(写的更少,作的更多),那么链式方法的设计与这个核心理念不谋而合。那么从深层次考虑这种设计其实就是一种Internal DSL。

DSL是指Domain Specific Language,也就是用于描述和解决特定领域问题的语言。

jQuery的Internal DSL形式带来的好处——编写代码时,让代码更贴近做者的思惟模式;阅读代码时,让读者更容易理解代码的含义;应用DSL能够有效的提升系统的可维护性(缩小了实现模型和领域模型的距离,提升了实现的可读性)和灵活性,而且提供开发的效率。

jQuery的这种管道风格的DSL链式代码,总的来讲:

☑ 节约JS代码;

☑ 所返回的都是同一个对象,能够提升代码的效率

实现链式操做的原理你们都懂的,就只须要在方法内返回当前的这个实例对象this就能够了,由于返回当前实例的this,从而又能够访问本身的原型了,这样的就节省代码量,提升代码的效率,代码看起来更优雅。可是这种方法有一个问题是:全部对象的方法返回的都是对象自己,也就是说没有返回值,因此这种方法不必定在任何环境下都适合。


插件接口的设计

jQuery插件的开发分为两种:

☑ 一种是挂在jQuery命名空间下的全局函数,也可称为静态方法;

☑ 另外一种是jQuery对象级别的方法,即挂在jQuery原型下的方法,这样经过选择器获取的jQuery对象实例也能共享该方法。

提供的接口:

$.extend(target, [object1], [objectN]);
$.fn.extend();

接口的使用:

// 拓展到jQuery上的静态方法
jQuery.extend({
    data:function(){},
    removeData:function(){}
})

// 拓展到实例对象上的原型方法
jQuery.fn.extend({
    data:function(){},
    removeData:function(){}
})

而jQuery源码中对于上面两种扩展,实际上是同指向同一方法的不一样引用(这里有一个设计的重点,经过调用的上下文,咱们来肯定这个方法是做为静态仍是实例处理,在javascript的世界中一共有四种上下文调用方式:方法调用模式、函数调用模式、构造器调用模式、apply调用模式),而这一切都是依靠this来完成的。

☑  jQuery.extend调用的时候上下文指向的是jQuery构造器,this指向的是jQuery

☑  jQuery.fn.extend调用的时候上下文指向的是jQuery构造器的实例对象了,this指向实例对象

所以在源码中是这样的:

aAron.extend = aAron.fn.extend = function() {
    var options, src, copy,
        target = arguments[0] || {},
        i = 1,
        length = arguments.length;

    // 只有一个参数,就是对jQuery自身的扩展处理
    if (i === length) {
        target = this; // 调用的上下文对象,前一个方法对应jQuery,后一个方法对应实例
        i--;
    }
    for (; i < length; i++) {
        // 从i开始取参数,不为空开始遍历
        if ((options = arguments[i]) != null) {
            for (name in options) {
                copy = options[name];
                // 覆盖拷贝
                target[name] = copy;
            }
        }
    }
    return target;
}

我来说解一下上面的代码:由于extend的核心功能就是经过扩展收集功能(相似于mix混入),因此就会存在收集对象(target)与被收集的数据,由于jQuery.extend并无明确实参,并且是经过arguments来判断的,因此这样处理起来很灵活。arguments经过判断传递参数的数量能够实现函数重载。其中最重要的一段target = this,经过调用的方式咱们就能确实当前的this的指向,因此这时候就能肯定target了。最后就很简单了,经过for循环遍历把数据附加到这个target上了。固然在这个附加的过程当中咱们还能够作数据过滤、深拷贝等一系列的操做了。


回溯处理的设计

经过jQuery处理后返回的不只仅只有DOM对象,而是一个包装容器,返回jQuery对象。而这一个对象中有一个preObject的属性。

1559366209776

要了解这个属性是作什么的,首先了解一下jQuery对象栈,jQuery内部维护着一个jQuery对象栈。每一个遍历方法(在当前选中范围内的DOM再进行筛选的操做,例如.find()方法)都会找到一组新元素(一个jQuery对象),而后jQuery会把这组元素推入到栈中。

而每一个jQuery对象都有三个属性:context、selector和prevObject(用id选择器的话这个属性不必定有),其中的prevObject属性就指向这个对象栈中的前一个对象,而经过这个属性能够回溯到最初的DOM元素集中。

能够看下下面的例子:

$("div").find('.foo').find('.aaa') // 这里的preObject属性就会指向$("div").find('.foo')的DOM集合
$("div").find('.foo')  // 往前一级的preObect属性就是指向$("div")的DOM集合

而这种能够回溯到以前选择的DOM集合的机制,是为这两个方法服务的:

.end() // 回溯到前一个jQuery对象,即prevObject属性
.addBack() // 把当前位置和前一个位置的元素结合组合起来,而且将这个新的组合的元素集推入栈的上方

而利用这个回溯机制和对应的方法能够进行以下的操做:

<ul class="first">
    <li class="foo">list item 1</li>
    <li>list item 2</li>
    <li class="bar">list item 3</li>
</ul>

<script>
    // foo类li标签背景设置为红色, bar类li标签背景设置为绿色
    $("#test2").click(function(){
        //经过end连贯处理
        $('ul.first')
            .find('.foo')
            .css('background-color', 'red')
            .end()
            .find('.bar')
            .css('background-color', 'green');
    })
</scripts>

利用这个DOM元素栈能够减小重复的查询和遍历的操做,而减小重复操做也正是优化jQuery代码性能的关键所在。


end

end方法可以帮助咱们回溯到上一个DOM合集,所以该方法返回的就是一个jQuery对象,在源码中的表现就是返回了prevObject对象:

end: function() {
    return this.prevObject || this.constructor(null);
}

那么prevObject在什么状况下会产生?

在构建jQuery对象的时候,经过pushStack方法构建,以下代码:

pushStack: function( elems ) {
    // Build a new jQuery matched element set
    // 这里将传进来的DOM元素,经过调用jQuery的方法构建成一个新的jQuery对象
    var ret = jQuery.merge( this.constructor(), elems );

    // Add the old object onto the stack (as a reference)
    // 在此对象上把前一个jQuery对象添加到prevObject属性中
    ret.prevObject = this;
    ret.context = this.context;

    // Return the newly-formed element set
    // 最后返回这个jQuery对象
    return ret;
 }

那么在find方法中,为了将前一个jQuery对象推入栈中,就会调用这个pushStack方法来构建:

jQuery.fn.extend({
    find: function(selector) {

        //...........................省略................................

        //经过sizzle选择器,返回结果集
        jQuery.find(selector, self[i], ret);

        // Needed because $( selector, context ) becomes $( context ).find( selector )
        ret = this.pushStack(len > 1 ? jQuery.unique(ret) : ret); // 这里就是将实例对象推入栈中,而后返回新的jQuery对象
        ret.selector = this.selector ? this.selector + " " + selector : selector;
        return ret;
    }
}

仿栈与队列的操做

jQuery既然是模仿的数组结构,那么确定会实现一套类数组的处理方法,好比常见的栈与队列操做push、pop、shift、unshift、求和、遍历循环each、排序及筛选等一系的扩展方法。

jQuery提供了.get()、:index()、 :lt()、:gt()、:even()及 :odd()这类索引值相关的选择器,他们的做用能够过滤他们前面的匹配表达式的集合元素,筛选的依据就是这个元素在原先匹配集合中的顺序。

先来看看get方法的实现源码:

get: function(num) {
    return num != null ?  // 不传参为undefined, 走false线
    // Return just the one element from the set
    (num < 0 ? this[num + this.length] : this[num]) :
    // Return all the elements in a clean array
    slice.call(this); // 返回整个DOM元素数组
}

get与eq的区别

熟悉jQuery的童鞋都清楚,get返回的是DOM元素,而eq返回的是jQuery对象,这样就能够继续执行链式操做。

eq实现的原理:

eq: function( i ) {
    var len = this.length,
        j = +i + ( i < 0 ? len : 0 );
    return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );

上面实现代码的逻辑就是跟get是同样的,区别就是经过了pushStack产生了一个新的jQuery对象。

若是须要的是一个合集对象要怎么处理?所以jQuery便提供了一个slice方法,根据下标范围取元素集合,并生成一个新的jQuery对象。

slice方法实现源码:

slice: function() {
    return this.pushStack( slice.apply( this, arguments ) );
},

迭代器

迭代器是一个框架的重要设计。咱们常常须要提供一种方法顺序用来处理聚合对象中各个元素,而又不暴露该对象的内部,这也是设计模式中的迭代器模式(Iterator)。

针对迭代器,这里有几个特色:

☑ 访问一个聚合对象的内容而无需暴露它的内部。

☑ 为遍历不一样的集合结构提供一个统一的接口,从而支持一样的算法在不一样的集合结构上进行操做。

☑ 遍历的同时更改迭代器所在的集合结构可能会致使问题。

另外还要考虑这四点:

☑ 聚合对象,多是对象,字符串或者数组等类型

☑ 支持参数传递

☑ 支持上下文的传递

☑ 支持循环中退出(返回false的时候退出循环,节省性能)

简单实现一个迭代器:

function each(obj, callback, context, arg) {
    var i = 0;
    var value;
    var length = obj.length;
    for (; i < length; i++) {
        value = callback.call(context || null, obj[i], arg);
        if (value === false) {
            break;
        }
    }

jQuery的each迭代器

$.each()函数和$(selector).each()是不同的,后者是专门用来遍历一个jQuery对象的,是为jQuery内部服务的。

jQuery的实例方法最终也是调用的静态方法,咱们在以前就解释过jQuery的实例与原型方法共享的设计。

$.each()实例方法以下:

// 内部是直接调用的静态方法
each: function(callback, args) {
    return jQuery.each(this, callback, args);
},

jQuery.each静态方法:

each: function(obj, callback, args) {
    var value,
        i = 0,
        length = obj.length,
        isArray = isArraylike(obj);

    if (args) {
        if (isArray) {
            for (; i < length; i++) {
                value = callback.apply(obj[i], args);

                if (value === false) {
                    break;
                }
            }
        } else {
            for (i in obj) {
                value = callback.apply(obj[i], args);

                if (value === false) {
                    break;
                }
            }
        }

实现原理几乎一致,只是增长了对于参数的判断。对象用for in遍历,数组用for遍历。

相关文章
相关标签/搜索