目标:模拟jquery的$符号选择元素并调用hide与show方法
mquery.js代码
/*¡ * mquery like jquery * require sizzle.js http://sizzlejs.com/ * * Date 2014-3-21 * author meng */ (function(_window,sizzle){ if(!sizzle){ throw new ReferenceError("没有引入Sizzle.js!"); } MQuery.fn=MQuery.prototype; function mq(selecter){ return new MQuery(selecter); } function MQuery(selecter){ console.log(typeof selecter); if(typeof selecter!=="string" &&!(selecter instanceof Element)){ throw new TypeError("错误的参数类型,参数必须是字符串,或者Node对象或者NodeList对象"); } if(typeof selecter=="string"){ this.elements=sizzle(selecter); } else if(typeof selecter == "object" &&selecter instanceof Element){ this.elements=[selecter]; } this.length=this.elements.length; } //define hide MQuery.fn.hide=function(){ this.each(function(e){ e.style.display='none'; }); } //define show MQuery.fn.show=function(){ this.each(function(e){ e.style.display=''; }); } //define each MQuery.fn.each=function(callback,index){ var es=this.elements; for (var i = es.length - 1; i >= 0; i--) { callback(es[i],i); }; } _window.$=mq; _window.$.fn=_window.$.prototype=MQuery.prototype; })(window,Sizzle);
html代码
<!DOCTYPE html> <html> <head> <title>mquery</title> <style> .p{ width: 50px; height: 50px; border: 1px solid #ddd; margin-bottom: 20px; padding: 20px; } </style> <script type="text/javascript" src="core/sizzle.js"></script> <script type="text/javascript" src="core/mquery.js"></script> </head> <body> <p class="p">ppppp1</p> <p class="p">ppppp2</p> <button onclick="hide()">hide</button> <button onclick="show()">show</button> <script type="text/javascript"> var p = $("p"); p.hide(); function show(){ p.show(); } function hide(){ p.hide(); } p.hide(); var p2=$(document.getElementsByTagName("p")[0]); console.log(p2); </script> </body> </html>
运行html,发现实现了本文的目标,一开始运行$("p").hide(),隐藏页面所有的P标签,点击显示按钮调用$("p").show() 显示所有P标签。点击隐藏按钮隐藏P标签。
注:mquery.js依赖于sizzle.js,请先去js官网下载最新的sizzle.js。
代码解析
首先所有代码都包括在(function(_window,sizzle){})(window,Sizzle);里面。因为js是函数作用域,也就是说每个函数都有自己单独的作用域,如果我们声明的变量或者函数不是在一个函数内部也就是说在任何函数外声明,那么这些变量以及函数都属于全局作用域,全局作用域在js代码的任何地方都能访问。这样很容易跟其他js的变量起冲突。(如果一个变量声明多次,js会以最后一次为准)。所以我们用一个function把所有的变量包住,这样这个js里面的变量只有在这个function内部有效,不会污染全局作用域。
然后自动调用这个函数,这里把window,跟Sizzle当参数传进来,因为函数内部需要使用这2个对象。这样函数内部就能在自己的作用域访问这2个对象,比从全局作用域访问这2个对象效率要高一些,并且代码清晰明了,知道需要哪些依赖。
if(!sizzle){
throw new ReferenceError("没有引入Sizzle.js!");
}
检查是否存在sizzle,如果不存在则抛出一个ReferenceError。注:这里sizzle为0、""、null、false、undefined都会抛出异常。
function mq(selecter){ return new MQuery(selecter); } function MQuery(selecter){ console.log(typeof selecter); if(typeof selecter!=="string" &&!(selecter instanceof Element)){ throw new TypeError("错误的参数类型,参数必须是字符串,或者Element对象"); } if(typeof selecter=="string"){ this.elements=sizzle(selecter); } else if(typeof selecter == "object" &&selecter instanceof Element){ this.elements=[selecter]; } this.length=this.elements.length; }
声明了2个函数,因为被一个匿名函数包着,所以这2个函数只有在这个函数内部或者这个函数内部的函数能访问。不会影响到全局作用域。
Mquery是一个构造函数,为了区分构造函数与普通函数,构造函数首字母一般大写。这个构造函数必须接收一个字符串参数,如果参数类型不是字符串或者Element类型抛出类型错误异常。
没错其实这个就是相当于mquery的$()函数,jquery的$()参数能接收选择器字符串、html标签字符串、dom对象。
注:我们这里没有区分字符串是否为html标签,所以不支持传HTML标签字符串。
MQuery构造函数有2个对象elements与length,element对象是一个dom数组。length是这个数组的长度。
注:Sizzle()方法返回的就是一个dom数组。
MQuery.fn=MQuery.prototype;
//define hide MQuery.fn.hide=function(){ this.each(function(e){ e.style.display='none'; }); } //define show MQuery.fn.show=function(){ this.each(function(e){ e.style.display=''; }); } //define each MQuery.fn.each=function(callback,index){ var es=this.elements; for (var i = es.length - 1; i >= 0; i--) { callback(es[i],i); }; }
MQuery.fn=MQuery.prototype; 给MQuery声明了一个fn属性,并指向MQuery的原型,这里参考了jquery的做法。以后要给MQuery的原型新增方法可以直接通过fn。
这里给MQuery的原型定义了3个方法分别是hide、show、each。在原型上定义的方法能被每个实例共享,所以所有MQuery对象都能拥有这3个方法。
jquery的$()获取页面元素经常是一次性获取多个,然后调用某个方法会对所有获取对象起作用。很简单就能实现这种效果,我们通过sizzle获取的元素已经是一个dom数组了并保存在elements数组里面,所以我们只需要内部遍历element对象,依次调用就行了。
function mq(selecter){ return new MQuery(selecter); } _window.$=mq;
但是jquery是通过$()来获取对象的,我们可以这样模拟jquery的实现
内部声明一个mq函数,函数内部直接实例化MQuery。然后把这个函数暴露出去(通过赋值给window对象并取名为$)。这样就可以像jquery一样通过$()来获取MQuery对象。
_window.$.fn=_window.$.prototype=MQuery.prototype;
这里最后一句是把$的原型指向MQuery的原型,这样以后可以通过$.fn来扩展Mquery的原型,跟jquery的插件写法差不多。
自己的代码就贴到这,下一步是看一下Jquery的实现,然后对比一下差距在哪里。
先从网上下载最新的jquery2.x的源代码,下载地址:https://github.com/jquery/jquery。
jquery的源代码在src文件夹下。
打开jquery.js
define([ "./core", "./selector", "./traversing", "./callbacks", "./deferred", "./core/ready", "./data", "./queue", "./queue/delay", "./attributes", "./event", "./event/alias", "./manipulation", "./manipulation/_evalUrl", "./wrap", "./css", "./css/hiddenVisibleSelectors", "./serialize", "./ajax", "./ajax/xhr", "./ajax/script", "./ajax/jsonp", "./ajax/load", "./effects", "./effects/animatedSelector", "./offset", "./dimensions", "./deprecated", "./exports/amd", "./exports/global" ], function( jQuery ) { return jQuery; });
jquery源代码使用了require.js。 感兴趣的可以去官网看看。http://www.requirejs.org/。
首先引用了core.js 打开core.js发现了jquery的主要函数Jquery定义。
源代码太长,所以把Jquery的结构贴下来。
define([ "./var/arr", "./var/slice", "./var/concat", "./var/push", "./var/indexOf", "./var/class2type", "./var/toString", "./var/hasOwn", "./var/support" ], function( arr, slice, concat, push, indexOf, class2type, toString, hasOwn, support ) { var document = window.document, version = "@VERSION", //首先定义了Jquery函数。 jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context ); }, //定义Jquery的原型。 jQuery.fn = jQuery.prototype = { // The current version of jQuery being used jquery: version, //因为重写了JQuery的原型所以把constructor从新指向jQuery constructor: jQuery, // Start with an empty selector selector: "", // The default length of a jQuery object is 0 length: 0, //方法定义 toArray: function() {}, get: function( num ) {}, pushStack: function( elems ) {}, each: function( callback, args ) {}, map: function( callback ) {}, slice: function() {}, first: function() {}, last: function() {}, eq: function( i ) {}, end: function() {}, // For internal use only. // Behaves like an Array's method, not like a jQuery method. push: push, sort: arr.sort, splice: arr.splice }; jQuery.extend = jQuery.fn.extend = function() {}; jQuery.extend({ // Unique for each copy of jQuery on the page expando: "jQuery" + ( version + Math.random() ).replace( /D/g, "" ), // Assume jQuery is ready without the ready module isReady: true, error: function( msg ) {}, noop: function() {}, isFunction: function( obj ) {}, isArray: Array.isArray, isWindow: function( obj ) {}, isNumeric: function( obj ) {}, isPlainObject: function( obj ) {}, isEmptyObject: function( obj ) {}, type: function( obj ) {}, globalEval: function( code ) {}, camelCase: function( string ) {}, nodeName: function( elem, name ) {}, each: function( obj, callback, args ) {}, trim: function( text ) {}, makeArray: function( arr, results ) {}, inArray: function( elem, arr, i ) {}, merge: function( first, second ) {}, grep: function( elems, callback, invert ) {}, map: function( elems, callback, arg ) {}, guid: 1, proxy: function( fn, context ) {}, now: Date.now, support: support }); // Populate the class2type map jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); }); function isArraylike( obj ) {} return jQuery; });
- 这里jquery定义原型方式与mquery不一样。jquery是jquery.fn=jquery.prototype={};这样的好处是代码看起来更美观原型方法都定义在一个大括号里,但是这样相当于重写了jquery的原型,这样原型的constructor会指向Object而不是jquery。所以jquery又重新指定了constructor: jQuery。
- jquery定义了extend函数这里暂时不管。
- 关于jquery函数的定义jQuery = function( selector, context ) {return new jQuery.fn.init( selector, context );}这里比较有意思。jquery的内部new了一个jquery的原型上的一个构造函数。(当然不能直接new jquery否则会无限循环,也不能直接返回this,这样需要前台调用的时候new jquery(),这就是为什么mquery定义了一个mq的函数,mq内部再new mquery()的原因。)要想知道为什么只有去看一下这个init函数了。
// Initialize a jQuery object define([ "../core", "./var/rsingleTag", "../traversing/findFilter" ], function( jQuery, rsingleTag ) { // A central reference to the root jQuery(document) var rootjQuery, // A simple way to check for HTML strings // Prioritize #id over <tag> to avoid XSS via location.hash (#9521) // Strict HTML recognition (#11290: must start with <) rquickExpr = /^(?:s*(<[wW]+>)[^>]*|#([w-]*))$/, init = jQuery.fn.init = function( selector, context ) { var match, elem; // HANDLE: $(""), $(null), $(undefined), $(false) if ( !selector ) { return this; } // Handle HTML strings if ( typeof selector === "string" ) { if ( selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) { // Assume that strings that start and end with <> are HTML and skip the regex check match = [ null, selector, null ]; } else { match = rquickExpr.exec( selector ); } // Match html or make sure no context is specified for #id if ( match && (match[1] || !context) ) { // HANDLE: $(html) -> $(array) if ( match[1] ) { context = context instanceof jQuery ? context[0] : context; // scripts is true for back-compat // Intentionally let the error be thrown if parseHTML is not present jQuery.merge( this, jQuery.parseHTML( match[1], context && context.nodeType ? context.ownerDocument || context : document, true ) ); // HANDLE: $(html, props) if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { for ( match in context ) { // Properties of context are called as methods if possible if ( jQuery.isFunction( this[ match ] ) ) { this[ match ]( context[ match ] ); // ...and otherwise set as attributes } else { this.attr( match, context[ match ] ); } } } return this; // HANDLE: $(#id) } else { elem = document.getElementById( match[2] ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 if ( elem && elem.parentNode ) { // Inject the element directly into the jQuery object this.length = 1; this[0] = elem; } this.context = document; this.selector = selector; return this; } // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { return ( context || rootjQuery ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) } else { return this.constructor( context ).find( selector ); } // HANDLE: $(DOMElement) } else if ( selector.nodeType ) { this.context = this[0] = selector; this.length = 1; return this; // HANDLE: $(function) // Shortcut for document ready } else if ( jQuery.isFunction( selector ) ) { return typeof rootjQuery.ready !== "undefined" ? rootjQuery.ready( selector ) : // Execute immediately if ready is not present selector( jQuery ); } if ( selector.selector !== undefined ) { this.selector = selector.selector; this.context = selector.context; } return jQuery.makeArray( selector, this ); }; // Give the init function the jQuery prototype for later instantiation init.prototype = jQuery.fn; // Initialize central reference rootjQuery = jQuery( document ); return init; });
init函数实现在core文件夹下面的init.js。 至于上面那个答案参见第116行init.prototype = jQuery.fn; 这里把init函数的原型又指向了jquery的原型- - , 所以new init()是可以共享jquery原型的所有方法的。
这个init函数对应了Mquery的构造函数。 不过jquery能接受2个参数,第一个是选择器字符串,第二个是上下文,如果传了第二个人参数那么将会在这个上下文内查找想要的元素,效率会有所提升。这里没搞懂的是sizzle是按照css的语法来查找元素的,css是能够指定上下文的,例如:传入"#header .logo",应该已经指定上下文为#header了吧。应该不需要第二个人参数了吧 - -搞不懂。只有等功力够了研究一下sizzle了。
jquery的init函数内部非常复杂因为jquery的$()能支持css选择器、dom元素、html字符串、函数。并且对$("#id")有特殊判断,直接调用了document.getElementById,这里也没想明白为什么不在sizzle内部处理呢?