从零开始,DIY一个jQuery3

在前两章,为了方便调试,我们写了一个非常简单的 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;
            }
            ;
        }
    };

因此我们在 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 的打包模式。

其原先代码为:

(( window ) {

var i,support,//...省略一大堆有的没的
Sizzle.noConflict = () { if ( window.Sizzle === Sizzle ) { window.Sizzle = _sizzle; } return Sizzle; }; if ( typeof define === "function" && define.amd ) { define(function() { Sizzle; }); Sizzle requires that there be a global window in Common-JS like environments } else typeof module !== "undefined" && module.exports ) { module.exports = Sizzle; } { window.Sizzle = Sizzle; } EXPOSE })( window );

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

...省略

Sizzle.noConflict =  Sizzle;
};

export default Sizzle;

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

import Sizzle from './sizzle.js';

var selectorInit = (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;

         first;
    },1)">...
});

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

<div>hello</>
>world>

script>
    var divs = $.find('div); //纯数组
     $div1  $.merge( [hi],divs); 右边的数组合并到左边的数组,形成一个新数组
     $div2  $.merge( {0: 1},1)">右边的数组合并到左边的对象,形成一个新的类数组对象

    console.log($div1);
    console.log($div2);
>

运行输出:

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

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

    jQuery.fn.init = var elemList = jQuery.find(selector);
             (elemList.length) {
                jQuery.merge( this,elemList );  this是JQ实例,默认实例属性 .length 为0
            }
            ;
        }
    };

我们打包后执行:

 $div  $();
    console.log($div);
>

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

3. 扩展 $.fn.find

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

jQuery.fn.extend({
    find: function( selector ) {  链式支持find
        .length,self = ;

        ret = [];

        for ( i = 0; i < len; i++ ) {  遍历
            jQuery.find( selector,self[ i ],ret );  直接利用 Sizzle 接口,把结果注入到 ret 数组中去
        }

         ret;
    }
});

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

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

><span>hib></>你好 $span ).find(span);
    console.log($span);
>

效果如下:

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

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

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

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

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

可能你在第一时间会想到,是否使用一个数组,通过 push 和 pop 来实现入栈和出栈的功能。

事实上我们有更简单的形式 —— 给新的 JQ 对象新增一个 .prevObject 属性并指向旧 JQ 对象,这样一来,我们想获取当前 JQ 对象之前的一次 JQ 对象,通过该属性就能直接取到了:

jQuery.fn = jQuery.prototype =/**
     * 入栈操作
     * @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 = ;

         ret;
    }
    省略...
}

这样通过 pushStack 接口包装下,就解决了上面说的两个问题,我们改下 $.fn.find 代码:

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

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

;

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

         ret
    }
}); 

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

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

jQuery.fn.extend({
    end: () {
        this.prevObject || .constructor();
    }
});

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

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

.constructor();
    },eq: ( i ) {
        var len = 支持倒序搜索,i可以是负数
        this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); 容错处理,若i过大或过小,返回空数组
    },get: ( num ) {
        return num != null ?

             支持倒序搜索,num可以是负数
            ( num < 0 ? this[ num + this.length ] : [ num ] ) :

             克隆一个新数组,避免指向相同
            [].slice.call( this );  建议把 [].slice 封装到 var.js 中去复用
    }
});

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

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

jQuery.fn.extend({
    first: this.eq( 0 );
    },last: this.eq( -1 );
    }
});

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

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

2. 同上,$.fn.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上,有需要的同学可以自行下载调试。共勉~

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


1.第一步 设置响应头 header(&#39;Access-Control-Allow-Origin:*&#39;); //支持全域名访问,不安全,部署后需要固定限制为客户端网址 header(&#39;Access-Control-Allow-Methods:POST,GET,OPTIONS,D
$.inArray()方法介绍 $.inArray()函数用于在数组中搜索指定的值,并返回其索引值。如果数组中不存在该值,则返回-1; $.inArray(value,array) --value是要查找的值,array是被查找的数组。 有如下实例: &lt;!DOCTYPE html&gt; &l
jquery.serializejson.min.js的妙用 关于这个jquery.serializejson.min.js插件来看,他是转json的一个非常简单好用的插件。 前端在处理含有大量数据提交的表单时,除了使用Form直接提交刷新页面之外,经常碰到的需求是收集表单信息成数据对象,Ajax提
JS 将form表单数据快速转化为object对象(json对象) jaymou 于 2020-03-03 11:11:05 发布 3534 收藏 3 分类专栏: 前端 文章标签: javascript jquery 版权 前端 专栏收录该内容 5 篇文章0 订阅 订阅专栏 直接上代码 /** *
jQuery的区别:$().click()和$(document).on(&#39;click&#39;,&#39;要选择的元素&#39;,function(){})的不同 文章地址:https://www.cnblogs.com/sqh17/p/7746418.html 解决:动态创建的元素的事件
jQuery插件之jquery.spinner数字智能增减插件 参考地址:http://www.helloweba.com/view-blog-282.html 左右加减数字 像京东提交订单时目前使用的是左右加减数字的效果,这个效果直接明了,操作简单。我们使用jquery.spinner.js插件实
layui标签或一般标签均可&lt;div class=&quot;layui-form-item&quot;&gt; &lt;label class=&quot;layui-form-label&quot;&gt;异地仓名称&lt;/label&gt; &lt;div class=&quot;la
网上对于select option 动态添加修改如下, $(&quot;#selectId&quot;).append(&quot;&lt;option value=&#39;&quot;+value+&quot;&#39;&gt;&quot;+text+&quot;&lt;/option&gt;&
jQuery中的 $.extend() 和 $.fn.extend() ANGWH 于 2020-05-24 06:39:59 发布 注意:$.extend是为jQuery类添加添加类方法,用$.调用(类似$.ajax),$.fn.extend则是为jQuery对象添加方法(实例方法),用DOM元素
jquery 循环数组输出显示在html页面 jquery 没有双向数据绑定,但是很多需求确实需要我们从后台接收到数组或者对象循环显示在前台页面上,这时我们可以用字符串拼接,元素添加的方法去实现 js部分如下: 复制代码 $(function(){ var a=[&quot;1aa&quot;,&q
javascript事件委托理解,jQuery .on()方法一步到位实现事件委托 Javascript-概念原理 专栏收录该内容 10 篇文章0 订阅 订阅专栏 本篇文章借鉴自:博客园文章,只为自己巩固下事件委托方面的知识 概述: 什么叫事件委托?他还有一个名字叫做事件代理,(时间代理 事件委托,
JQuery-$.when().done().fail()的使用 原文引用于&#160;Echoo华地于&#160;2022-01-06 14:07:10 发布 jQuery的开发速度很快,几乎每半年一个大版本,每两个月一个小版本。 每个版本都会引入一些新功能。今天我想介绍的,就是从jQuery 1
jQuery tableExport导出 excel 上篇写的是jQuery&#160;导出word,就试试导出excel。看见网上写的很乱,我这就把我写的整理下来,有部分来自网上capy 1. js文件的引用 &lt;script type=&quot;text/javascript&quot;
jQuery的遍历-prev()和next()方法 &lt;div class=&quot;box&quot; id=&quot;box&quot;&gt; &lt;a href=&#39;#&#39; class=&quot;a&quot;&gt; &lt;input type=&quot;tex
attr()和addClass()的区别 方法 addClass() attr()用途&#x9;追加样式&#x9;设置样式对同一个网页元素操作&#x9;&lt;p&gt;test&lt;/p&gt;第1次使用方法&#x9;$(&quot;p&quot;).addClass(&quot;high&quot;);&#x9;$(&quot;p&
前端——函数(匿名函数、自执行函数) FreshLemon_ 于 2019-06-11 17:11:49 发布 函数声明:function box(){} 函数表达式:var box = function(){}; 匿名函数:function(){} (属于函数表达式) 1声明了一个函数: var
js: 获取标签元素data-*属性值的方法 彭世瑜 于 2022-05-23 09:59:50 发布 2165 收藏 1 文章标签: javascript 前端 jquery 版权 标签上有两个属性data-id 和 data-user-name, 需要通过js去获取 &lt;style&gt;
JavaScript函数详解:匿名函数、具名函数、函数传参、不定参、返回值、JS预解析机制 1.具名函数 定义: 调用: 方式1:方法名(); 可以多次调用 方式2:在事件中调用,直接写函数名,不需用括号 2.匿名函数 没有名字的函数 匿名函数在使用时只有两种情况: 1.匿名函数自执行:声明后不需要
如何等待ajax完成再执行相应操作 ajax广泛应用于异步请求,对于大多数业务来说,这是十分方便的,但对于一些特殊的业务,ajax的异步性会起到相反的作用。 例如在ajax请求成功后,后续的操作需要依赖ajax执行成功后的相应操作。 // 声明一个表示状态的全局变量 status var statu
一步一步教你写一个jQuery的插件教程(Plugin) 更新时间:2009年09月03日 02:10:54 作者: 我将会在下面的例子中一个一个的说明上面这几个条件,做完这些事情后我们就会创建一个高亮显示text的简单插件。 jQuery 的plugin开发需要注意的事情, 1. 明确jQuery