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

在前两章,为了方便调试,我们写了一个非常简单的 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上,有需要的同学可以自行下载调试。共勉~

时间: 2024-10-13 11:13:51

从零开始,DIY一个jQuery(3)的相关文章

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

在上篇文章我们简单实现了一个 jQuery 的基础结构,不过为了顺应潮流,这次咱把它改为模块化的写法,此举得以有效提升项目的可维护性,因此在后续也将以模块化形式进行持续开发. 模块化开发和编译需要用上 ES6 和 rollup,具体原因和使用方法请参照我之前的<冗余代码都走开——前端模块打包利器 Rollup.js 入门>一文. 本期代码均挂在我的github上,有需要的童鞋自行下载. 1. 基本配置 为了让 rollup 得以静态解析模块,从而减少可能存在的冗余代码,我们得用上 ES6 的解

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

从本篇开始会陪大家一起从零开始走一遍 jQuery 的奇妙旅途,在整个系列的实践中,我们会把 jQuery 的主要功能模块都了解和实现一遍. 这会是一段很长的历程,但也会很有意思 —— 作为前端领域的经典之作,jQuery 里有着太多奇思妙想,如果能够深入理解它,对于我们稳固js基础.提升前端大法技能来说大有裨益. 另外,本系列的相关代码均可以从 我的github 上获取到. 1. 免 new 实现 我们在使用很多插件的时候,都需要使用 new XXX() 的写法来实例化一个引用: var li

从零开始构建一个的asp.net Core 项目

最近突发奇想,想从零开始构建一个Core的MVC项目,于是开始了构建过程. 首先我们添加一个空的CORE下的MVC项目,创建完成之后我们运行一下(Ctrl +F5).我们会在页面上看到"Hello World!". 既然是从零开始构建的项目,我们需要搞明白这个"Hello World!"是从哪里出现的? 点开我们的项目,我们会看到VS为我们生成了两个类,一个是Program.cs 和startup.cs,和一个空文件夹(wwwroot),除此之外VS在也没有为我们多

从零开始构建一个centos+jdk7+tomcat7的docker镜像文件

从零开始构建一个centos+jdk7+tomcat7的镜像文件 centos7系统下docker运行环境的搭建 准备centos基础镜像 docker pull centos 或者直接下载我准备好的镜像 docker pull registry.cn-hangzhou.aliyuncs.com/repos_zyl/centos:0.0.1 准备jdk7和tomcat7安装包 创建工作目录, mkdir -p /z/docker 准备下载jdk7的tar.gz包http://download.o

一个jQuery扩展工具包

带有详尽注释的源代码: var jQuery = jQuery || {}; // TODO // ###################################string操作相关函数################################### jQuery.string = jQuery.string || {}; /** * 对目标字符串进行html解码 * * @name jQuery.string.decodeHTML * @function * @grammar j

DIY一个基于树莓派和Python的无人机视觉跟踪系统

DIY一个基于树莓派和Python的无人机视觉跟踪系统 无人机通过图传将航拍到的图像存储并实时传送回地面站几乎已经是标配.如果想来点高级的--在无人机上直接处理拍摄的图像并实现自动控制要怎么实现呢?其实视觉跟踪已经在一些高端的消费级无人机上有了应用,不过玩现成的永远没有自己动手来劲;).前段时间DIY了一个无人机三轴云台的视觉跟踪系统,除去云台花了¥370,本文将设计思路与实验效果分享出来. 一.基本配置 1.1 硬件 计算平台:树莓派3 (¥219.00) 摄像头:USB网络摄像头(¥108.

深入浅出React Native 3: 从零开始写一个Hello World

这是深入浅出React Native的第三篇文章. 1. 环境配置 2. 我的第一个应用 将index.ios.js中的代码全部删掉,为什么要删掉呢?因为我们准备从零开始写一个应用~学习技术最好的方式就是自己动手写,看别人的代码一百遍的效果也不如自己写一遍来的效果大~ 我们要做的事情主要分成以下两步: 1. 创建组件 2. 将创建好的组件显示在app上 打开index.ios.js文件,输入 var HelloWorld = React.createClass({ render: functio

分享一个jquery功能强大的提示信息插件代码

代码属于提示文字特效,很好,使用有些复杂,请参demo使用 下载地址:jquery功能强大的提示信息插件代码 预览DEMO:DEMO 分享一个jquery功能强大的提示信息插件代码,布布扣,bubuko.com

一个jquery在不同浏览器下的兼容性问题。

<div id ='pdiv' style='visibility:hidden;'> <div id='cdiv'>子元素</div> </div> 以上HTML. 父div设置了visibility为hidden.当使用jquery获取子div的visibility的值时,在不同版本的IE浏览器得到的值不一样: 在>=IE8时 $("#cdiv").css("visibility")的值为"hidd