zoukankan      html  css  js  c++  java
  • jQuery源码解读----part 2

    分离构造器

    通过new操作符构建一个对象,一般经过四步:

    A.创建一个新对象

    B.将构造函数的作用域赋给新对象(所以this就指向了这个新对象)

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

    D.返回这个新对象

    最后一点就说明了,我们只要返回一个新对象即可。其实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个独立的构造器。


    静态与实例方法共享设计

    接着上面分割出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遍历。


  • 相关阅读:
    UVALive 7141 BombX
    CodeForces 722D Generating Sets
    CodeForces 722C Destroying Array
    CodeForces 721D Maxim and Array
    CodeForces 721C Journey
    CodeForces 415D Mashmokh and ACM
    CodeForces 718C Sasha and Array
    CodeForces 635C XOR Equation
    CodeForces 631D Messenger
    田忌赛马问题
  • 原文地址:https://www.cnblogs.com/simpul/p/11027193.html
Copyright © 2011-2022 走看看