微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

从零开始,DIY一个jQuery(3)

2016-08-22 20:20 前端大全

(点击上方公众号,可快速关注)

来源:VaJoy Larn

链接:http://www.cnblogs.com/vajoy/p/5748815.html


在前两章,为了方便调试,我们写了一个非常简单的 jQuery.fn.init 方法:


jQuery.fn.init = function (selector, context, root) {

        if (!selector) {

            return this;

        } else {

            var elem = document.querySelector(selector);

            if (elem) {

                this[0] = elem;

                this.length = 1;

            }

            return this;

        }

    };


因此我们在 demo 里执行 $(‘div’) 时可以取得这么一个类数组对象:



在完整的 jQuery 中通过 $(selector) 的形式获取的对象也基本如此 —— 它是一个对象而非数组,但可以通过下标(如 $div[index] )或 .get(index) 接口来获取到相应的 DOM 对象,也可以直接通过 .length 来获取匹配到的 DOM 对象总数。


这么实现的原因是 —— 方便,该对象毕竟是 jQuery 实例,继承了所有的实例方法,同时又直接是所检索到的DOM集合(而不需要通过 $div.getDOMList() 之类的方法来获取),简直一石二鸟。


如下图所示便是一个很寻常的 JQ 类数组对象(初始化执行的代码是 $(‘div’) ):



1. Sizzle 引入


在 jQuery 中,检索DOM的能力来自于 Sizzle 引擎,它是 JQ 最核心也是最复杂的部分,在后续有机会我们再对其作详细介绍,当前阶段,我们只需要直接“获取”并“使用”它即可。


Sizzle 是开源的选择器引擎,其官网是 http://sizzlejs.com/ ,直接在首页便能下载到最新版本。


我们在 src 目录下新增一个 /sizzle 文件夹,并把下载到的 sizzle.js 放进去(即存放为 src/sizzle/sizzle.js ),接着得对其做点小修改,使其得以适应我们 rollup 的打包模式。


其原先代码为:


(function( window ) {

 

var i,

    support,

 

//...省略一大堆有的没的<br>

Sizzle.noConflict = function() {

    if ( window.Sizzle === Sizzle ) {

        window.Sizzle = _sizzle;

    }

 

    return Sizzle;

};

 

if ( typeof define === "function" && define.amd ) {

    define(function() { return Sizzle; });

// Sizzle requires that there be a global window in Common-JS like environments

} else if ( typeof module !== "undefined" && module.exports ) {

    module.exports = Sizzle;

} else {

    window.Sizzle = Sizzle;

}

// EXPOSE

 

})( window );


将这段代码的头和尾替换为:


var i,

    support,

 

//...省略

 

Sizzle.noConflict = function() {

    if ( window.Sizzle === Sizzle ) {

        window.Sizzle = _sizzle;

    }

 

    return Sizzle;

};

 

export default Sizzle;


同时新增一个初始化文件 src/sizzle/init.js ,用于把 Sizzle 赋予静态接口 jQuery.find:


import Sizzle from './sizzle.js';

 

var selectorInit = function(jQuery){

    jQuery.find = Sizzle;

};

 

export default selectorInit;


别忘了在打包的入口文件里引入该模块并执行:


import jQuery from './core';

import global from './global';

import init from './init';

import sizzleInit from './sizzle/init';  //新增

 

global(jQuery);

init(jQuery);

sizzleInit(jQuery);  //新增

 

export default jQuery;


打包后我们就能愉快地通过 jQuery.find 接口来使用 Sizzle 的各种能力了(使用方式可以参考 Sizzle 的API文档):



留意 $.find(XXX) 返回的是一个匹配到的 DOM 集合的数组(注意类型直接就是Array,不是 document.querySelectorAll 那样返回的 nodeList )。


我们需要多做一点处理,来将这个数组转换为前头提到的类数组JQ对象。


另外,虽然现在 JQ 的工具方法有了检索DOM的能力,但其实例方法是木有的,鉴于构造器的静态属性不会继承给实例,会导致我们没法链式地来支持 find,比如:


$('div').find('p').find('span')


很明显,这可以在 jQuery.fn.extend 里多加一个 find 接口来实现,不过不着急,咱们一步一步来。


2. $.merge 方法


针对上述的第一个需求点,我们修改下 src/core.js ,往 jQuery.extend 里新增一个 jQuery.merge 静态方法,方便把检索到的 DOM 集合数组转换为类数组对象:


jQuery.fn = jQuery.prototype = {

    jquery: version,

    length: 0,  // 修改点1,JQ实例.length 默认为0

    //...

}

 

jQuery.extend( {

    merge: function( first, second ) {  //修改点2,新增 merge 工具接口

        var len = +second.length,

            j = 0,

            i = first.length;

 

        for ( ; j &lt; len; j++ ) {

            first[ i++ ] = second[ j ];

        }

 

        first.length = i;

 

        return first;

    },

    //...

});


merge 的代码段太好理解了,其实现的能力为:


<div>hello</div>

<div>world</div>

 

<script>

    var divs = $.find('div'); //纯数组

    var $div1 = $.merge( ['hi'], divs); //右边的数组合并到左边的数组,形成一个新数组

    var $div2 = $.merge( {0: 'hi', length: 1}, divs); //右边的数组合并到左边的对象,形成一个新的类数组对象

 

    console.log($div1);

    console.log($div2);

</script>


运行输出:



因此,如果我们在 jQuery.fn.init 中,把 this 传入为 $.merge 的 first 参数(留意这里this为JQ实例对象自身,默认 length 实例属性为0),再把检索到的 DOM 集合数组作为 second 参数传入,那么就能愉快地得到我们想要的 JQ 类数组对象了。


我们简单地修改下 src/init.js :


jQuery.fn.init = function (selector, context, root) {

        if (!selector) {

            return this;

        } else {

            var elemList = jQuery.find(selector);

            if (elemList.length) {

                jQuery.merge( this, elemList );  //this是JQ实例,默认实例属性 .length 为0

            }

            return this;

        }

    };


我们打包后执行:


<div>hello</div>

<div>world</div>

 

<script>

    var $div = $('div');

    console.log($div);

</script>


输出正是我们所想要的类数组对象:



3. 扩展 $.fn.find


针对第二个需求点 —— 链式支持 find 接口,我们需要给 $.fn 扩展一个 find 方法:


jQuery.fn.extend({

    find: function( selector ) {  //链式支持find

        var i, ret,

            len = this.length,

            self = this;

 

        ret = [];

 

        for ( i = 0; i &lt; len; i++ ) {  //遍历

            jQuery.find( selector, self[ i ], ret );  //直接利用 Sizzle 接口,把结果注入到 ret 数组中去

        }

 

        return ret;

    }

});


这里我们依旧直接使用了 Sizzle 接口 —— 当带上了第三个参数(数组类型)时,Sizzle 会把检索到的 DOM 集合注入到该参数中去(API文档)。


我们打包后执行下方代码:


<div><span>hi</span><b>hello</b></div>

<div><span>你好</span></div>

 

<script>

    var $span = $('div').find('span');

    console.log($span);

</script>


效果如下:



可以看到,我们要的子元素是出来了,不过呢,这里获取到的是纯数组,而非 JQ 对象,处理方法很简单 —— 直接调用前面刚加上的 $.merge 方法即可。


另外也有个问题,一旦咱们获取到了子孙元素(如上方代码中的span),那么如果我们需要重新取到其祖先元素(如上方代码中的div),就又得重新去走 $(‘div’) 来检索了,这样麻烦且效率不高。


而我们知道,在 jQuery 中是有一个 $.fn.end 方法可以返回上一次检索到的 JQ 对象的:


$('div').find('span').end()  //返回$('div')对象


处理方法也很简单,参考浏览器的历史记录栈,我们也来写一个遵循后进先出的栈操作方法 pushStack:


jQuery.fn = jQuery.prototype = {

    jquery: version,

    length: 0,

    constructor: jQuery,

    /**

     * 入栈操作

     * @param elems {Array}

     * @returns {*}

     */

    pushStack: function( elems ) {  //elems是数组

 

        // 将检索到的DOM集合转换为JQ类数组对象

        var ret = jQuery.merge( this.constructor(), elems );  //this.constructor() 返回了一个 length 为0的JQ对象

 

        // 添加关系链,新JQ对象的prevObject属性指向旧JQ对象

        ret.prevObject = this;

 

        return ret;

    }

    //省略...

}


这样就解决了上面说的两个问题,我们改下 $.fn.find 代码:


jQuery.fn.extend({

    find: function( selector ) {  //链式支持find

        var i, ret,

            len = this.length,

            self = this;

 

        ret = [];

 

        for ( i = 0; i &lt; len; i++ ) {  //遍历

            jQuery.find( selector, self[ i ], ret );  //直接利用 Sizzle 接口,把结果注入到 ret 数组中去

        }

 

        return this.pushStack( ret );  //转为JQ对象

    }

});


从性能上考虑,我们这样写会更好一些(减少一些merge里的遍历):


jQuery.fn.extend({

    find: function( selector ) {  //链式支持find

        var i, ret,

            len = this.length,

            self = this;

 

        ret = this.pushStack( [] ); //转为JQ对象

 

        for ( i = 0; i &lt; len; i++ ) {  //遍历

            jQuery.find( selector, self[ i ], ret );  //直接利用 Sizzle 接口,把结果注入到 ret 数组中去

        }

 

        return ret

    }

});

 

4. $.fn.end、$.fn.eq 和 $.fn.get


鉴于我们在 pushStack 中加上了 oldJQ.prevObject 的关系链,那么 $.fn.end 接口的实现就太简单了:


jQuery.fn.extend({

    end: function() {

        return this.prevObject || this.constructor();

    }

});


直接返回上一次检索到的JQ对象(如果木有,则返回一个空的JQ对象)。


这里顺便再多添加两个大家熟悉的不能再熟悉的 $.fn.eq 和 $.fn.get 工具方法,代码非常的简单:


jQuery.fn.extend({

    end: function() {

        return this.prevObject || this.constructor();

    },

    eq: function( i ) {

        var len = this.length,

            j = +i + ( i &lt; 0 ? len : 0 );  //支持倒序搜索,i可以是负数

        return this.pushStack( j &gt;= 0 &amp;&amp; j &lt; len ? [ this[ j ] ] : [] ); //容错处理,若i过大或过小,返回空数组

    },

    get: function( num ) {

        return num != null ?

 

            // 支持倒序搜索,num可以是负数

            ( num &lt; 0 ? this[ num + this.length ] : this[ num ] ) :

 

            // 克隆一个新数组,避免指向相同

            [].slice.call( this );  //建议把 [].slice 封装到 var.js 中去复用

    }

});


通过 eq 接口我们可以知道,后续任何方法,如果要返回一个 JQ 对象,基本都需要裹一层 pushStack 做处理,来确保 prevObject 的正确引用。


当然,这也轻松衍生了 $.fn.first 和 $.fn.last 两个工具方法:


jQuery.fn.extend({

    first: function() {

        return this.eq( 0 );

    },

    last: function() {

        return this.eq( -1 );

    }

});


本章就先写到这里,避免太多内容难消化。事实上,我们的 $.fn.init 、$.find 和 $.fn.find 都还有一些不完善的地方:


1. $.fn.init 方法没有兼顾到各种参数类型的情况,也还没有加上第二个参数 context 来做上下文预设;


2. 同上,$.find 也未对兼顾到各种参数类型的情况;


3. $.fn.find 返回结果有可能带有重复的 DOM,例如:


<div><div><span>hi</span></div></div>

 

<script>

    var $span = $('div').find('span');

    console.log($span);  //重复了

</script>


这些存在的问题我们都会在后面的篇章做进一步的优化。



------------- 推荐 -------------


范品社推出的极客T恤,含程序员、电影、美剧和物理题材,面料舒适、100%纯棉,有黑、白、灰、藏青色,单件 ¥59.9、两件减¥12、四件减¥28、六件减¥42,详见网店商品页介绍。



(上面为部分 T 恤款式)


网店地址:https://fanpinshe.taobao.com


淘口令:复制以下红色内容,然后打开手淘即可购买


范品社,使用¥极客T恤¥抢先预览(长按复制整段文案,打开手机淘宝即可进入活动内容)

 
前端大全 更多文章 前端工程师必须收藏的 CSS 资源大全 HTTP 协议入门 关于 bind 你可能需要了解的知识点以及使用场景 Javascript 深拷贝 简单封装分页功能pageView.js
猜您喜欢 VR发展史与现在火爆的原因 python集合类型实例 58同城数据库架构设计思路(下) 凡哥,不哭,十年数据库经验大师教你全方面管理数据库隐私 tidyr包更新的三个新特性