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上,

jQuery.fn的更多相关文章

  1. jquery.fn.extend与jquery.extend--(初体验二)

    1.jquery.extend(object); 为扩展jQuery类本身.为类添加新的方法. jquery.fn.extend(object);给jQuery对象添加方法. $.extend({ a ...

  2. jQuery为开发插件提拱了两个方法:jQuery.fn.extend(); jQuery.extend();

    jQuery为开发插件提拱了两个方法,分别是: jQuery.fn.extend(); jQuery.extend(); jQuery.fn jQuery.fn = jQuery.prototype ...

  3. 记jQuery.fn.show的一次踩坑和问题排查

    最近很少已经很少用jQuery,因为主攻移动端,常用Zepto,其实很多细节和jQuery并不一样.最近又无意中接触到了PC的需求和IE6, 使用了jQuery,刚好踩坑了,特意记录一下. 本文内容如 ...

  4. jQuery.extend和jQuery.fn.extend的区别【转】

    解释的很有意思,清晰明了又有趣,转来分享下,哈哈哈 jQuery.extend和jQuery.fn.extend的区别,其实从这两个办法本身也就可以看出来.很多地方说的也不详细.这里详细说说之间的区别 ...

  5. ES6的Iterator,jquery Fn

    ES6的Iterator对象详解 Iterator实现原理 创建一个指针对象,指向当前数据结构的起始位置.也就是说,遍历器对象本质上,就是一个指针对象. 第一次调用指针对象的next方法,可以将指针指 ...

  6. jQuery原生框架中的jQuery.fn.extend和jQuery.extend

    extend 方法在 jQuery 中是一个很重要的方法,jQuey 内部用它来扩展静态方法或实例方法,而且我们开发 jQuery 插件开发的时候也会用到它.但是在内部,是存在 jQuery.fn.e ...

  7. jQuery.fn.extend() 与 jQuery.extend()

    jQuery.fn如何扩展. jQuery插件 $.fn(object)与$.extend(object) jQuery提供了两个方法帮助开发插件 $.extend(object);扩展jQuery类 ...

  8. jQuery.fn.extend(object) object中this的指向

    看到下面的代码后,一下子懵逼了.这个this指向哪儿去了. jQuery.fn.extend({ check: function() { return this.each(function() { t ...

  9. jQuery.extend()方法和jQuery.fn.extend()方法源码分析

    这两个方法用的是相同的代码,一个用于给jQuery对象或者普通对象合并属性和方法一个是针对jQuery对象的实例,对于基本用法举几个例子: html代码如下: <!doctype html> ...

  10. 区别和详解:jQuery extend()和jQuery.fn.extend()

    1.认识jQuery extend()和jQuery.fn.extend() jQuery的API手册中,extend方法挂载在jQuery和jQuery.fn两个不同对象上方法,但在jQuery内部 ...

随机推荐

  1. Attach file to database

    D:\Program Files\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQL\DATA databaseName.mdf databaseName.l ...

  2. Retina 显示屏

    Retina 直接翻译是视网膜的意思.在IT上,是Apple 公司提出的.意思是指一个显示屏的颗粒度 px 密度高到人类无法看见.要了解细节必须先了解基础知识inch 英寸 1 inch = 2.52 ...

  3. OpenRTSP的使用

    由于需要研究OpenRTSP的源码,所以先学习下使用. -d [time]--------这个是录制时间,就是单位秒,超时后,程序自动结束. -i   -----------以.avi文件格式生成. ...

  4. 【C++基础之十一】虚函数的用法

    虚函数的作用和意义,就不进行说明了,这里主要讨论下虚函数的用法. 1.典型的虚函数用法 可以看到,只有标识为virtual的函数才会产生多态的效果,而且是编译多态.它只能借助指针或者引用来达到多态的效 ...

  5. layer iframe层的使用,传参

    父层 <div class="col-xs-4 text-left" style="padding-left: 50px;"><button ...

  6. 抛出异常的区别 throw 和throw ex

    在面试的过程中提到了异常捕获的的几种用法,之前一直使用但是没有仔细留意,调试程序的过程中发现还是有区别的,主要区别在堆栈信息的起始点不同,下边我们通过实例来看这集中不同的抛出异常的方法. 一般我们推荐 ...

  7. VS2010发布、打包安装程序

    1. 在vs2010 选择“新建项目”→“ 其他项目类型”→“ Visual Studio Installer→“安装项目”: 命名为:Setup1 . 这是在VS2010中将有三个文件夹, 1.“应 ...

  8. HDOJ 1202 The calculation of GPA

    Problem Description 每学期的期末,大家都会忙于计算自己的平均成绩,这个成绩对于评奖学金是直接有关的.国外大学都是计算GPA(grade point average) 又称GPR(g ...

  9. HDU-1869六度分离

    Problem Description 1967 年,美国著名的社会学家斯坦利·米尔格兰姆提出了一个名为“小世界现象(small world phenomenon)”的著名假说,大意是说,任何2个素不 ...

  10. java基础知识(二)

    java的布局管理: borderLayout:则将板块分为东西南北中五个方向,每添加一个组件就要指定组件摆放的方位,放置在东西南北四个方向的组件将贴边放置.当拉大Frame的时候,处在center( ...