zoukankan      html  css  js  c++  java
  • jQuery.fn

    DIY一个jQuery

    写了一个非常简单的 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,
    
    //...省略一大堆有的没的
    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 < 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 < 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 < 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 < 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 < 0 ? len : 0 );  //支持倒序搜索,i可以是负数
            return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); //容错处理,若i过大或过小,返回空数组
        },
        get: function( num ) {
            return num != null ?
    
                // 支持倒序搜索,num可以是负数
                ( num < 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>
    复制代码

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

    另外提几个点:

    1. 部分读者是从公众号上阅读本系列文章的,建议也要同时关注本人博客好一些 —— 有时我会对文章做一些更改,让其更易读懂; 
    2. 对于前两篇文章,部分基础较差的读者貌似不太好理解,我其实有考虑写个番外篇来帮你们梳理这块(特别是原型链的)知识点,如果觉得有需要的话可以留言给我,要求的人多的话我就动笔了; 
    3. 工作较忙,发文频率大约是1到2周一篇文章。近期其实蛮多读者催我更文的,但为了保持文章质量,需要多点时间,不希望数量上来了质量却下去了。

    本文的代码挂在我的github上,

  • 相关阅读:
    176. Second Highest Salary
    175. Combine Two Tables
    172. Factorial Trailing Zeroes
    171. Excel Sheet Column Number
    169. Majority Element
    168. Excel Sheet Column Title
    167. Two Sum II
    160. Intersection of Two Linked Lists
    个人博客记录
    <meta>标签
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/5774688.html
Copyright © 2011-2022 走看看