从零开始写jQuery框架
随着时代发展,javascript阵营里面出现了越来越多的优秀的框架,大大简化了我们的开发工作,在我们使用这些框架的时候是不是也应该饮水思源想想它们都是怎样构建起来的呢?如果你不满足于仅仅是使用一些现成的API,而是深入了解它们内部的实现机制(照某人的说法, API是贬值最快的东西),最好的办法就是阅读它们的源代码了,前提是你读得懂。
最近两天研究了一下jQuery的源码,在这里将本人一些粗浅认识分享出来,不当之处请各位指正。好了,下面我们就来看看jQuery大概是怎样工作的,我假定你已经具备了一些基本的javascript知识,如果基础不够俺推荐你阅读《JavaScript高级程序设计》和《悟透JavaScript》这两本书。本文不适合对js里面的类、对象、函数、prototype等概念没有了解的朋友。
我们从最开始的说起:
首先构造一个对象给使用者,假定我们这个框架叫Shaka(俺的名字;))
varShaka=function(){};这里我们创建了一个空函数,里面什么也没有,这个函数实际上就是我们的构造函数。为了让我们生成的对象能够调用在prototype里定义出来的方法,我们需要用原型的方式(把Shaka当作是一个类)给Shaka添加一些方法,于是定义::
Shaka.fn=Shaka.prototype={};这里的Shaka.fn相当于Shaka.prototype的别名,方便以后使用,它们指向同一个引用。
OK,我们添加一个sayHello的方法, 给Shaka添加一个参数,这样这个框架最基本的样子已经有了,如果它有生命的话那么它现在是1岁, 看代码:<script type="text/javascript"> var Shaka = function(age){ this.age = age; }; Shaka.fn = Shaka.prototype = { sayHello: function() { alert('I am a little baby, my age is ' + this.age + ' years old.'); }}; var babyShaka = new Shaka(1); babyShaka.sayHello(); </script>
好啦,先别激动, 我们注意到这个框架跟jQuery在使用上是有一些差别的, 比如在jq 中我们可以这样写
jQuery('#myid').someMethod();这是怎样做到的呢,也就是说jQuery()这个构造函数返回了一个jQuery的对象实例,因此我们可以在上面调用它的方法,所以Shaka的构造函数应该返回一个实例,它看起来应该是这个样子:
varShaka=function(){return//返回Shaka的实例;};那么我们要如何取得一个Shaka的实例呢,我们先来回顾一下使用prototype方式来模拟类的时候varsomeObj=newMyClass();这个时候实际上是创建一个新对象someObje,把新对象作为this指针,调用MyClass函数,即类的构造函数,然后someObj就获得了在MyClass.prototype里面定义的方法,这些方法内的this指针指当前对象实例。
在jQuery中使用了一个工厂方法来创建一个实例,这个方法位于jQuery.prototype中,现在我们重新来定义Shaka.prototype,给它添加一个init方法用于返回一个Shaka的实例,并且把Shaka的构造函数稍稍改变一下:
varShaka=function(age){returnnewShaka.fn.init(age);};
Shaka.fn=Shaka.prototype={
init:function(age){this.age=age;returnthis;},
sayHello:function(){alert('Iamalittlebaby,myageis'+this.age+'yearsold.');}
};
Shaka.fn.init.prototype=Shaka.fn;//这里newShaka.fn.init(age)创建的对象具有init方法的prototype指向对象的方法,因此我们将init方法的prototype指向Shaka的prototype,这样创建出来的对象就具有了Shaka.prototype里面定义的方法。
OK,现在我们的小宝宝变成大一点的宝宝了,打个招呼先:<script type="text/javascript"> var Shaka = function(age) { return new Shaka.fn.init(age); }; Shaka.fn = Shaka.prototype = { init: function(age) { this.age = age; return this; }, sayHello: function() { alert('I am a little big baby, my age is ' + this.age + ' years old.'); } }; Shaka.fn.init.prototype = Shaka.fn; Shaka(2).sayHello(); </script>
嗯,好象有点样子了,但是光这样还不行,来点实际的, 我们在新框架中实现jquery里val()方法的部分功能,这个方法不加参数调用时返回指定ID的input的值,加参数时为设定这个input的值,与jQery一样,我们约定使用id来查找对象时使用"#"符号。把要查找的目标ID作为构造函数的参数传进去,我们给Shaka.prototype添加一个val()方法, 给Shaka添加一个selector的属性用于存储我们要查找的目标。:
Shaka.fn=Shaka.prototype={
init:function(selector){this.selector=selector;returnthis;},
val:function(newValue){//方法实现代码}
};
var Shaka = function(selector) { return new Shaka.fn.init(selector); };<form method="post" action="" name="myform"> 我几岁了? <br /> <input id="myInput" type="text" value="Hello world!" size="50" /> </form> <script type="text/javascript"> var Shaka = function(selector) { return new Shaka.fn.init(selector); }; Shaka.fn = Shaka.prototype = { init: function(selector) { if(selector) this.selector = selector; return this; }, val: function(newValue) { //start val function body if(!(this.selector && this.selector.indexOf('#') == 0 && this.selector.length != 1)) return; //简单地判断传入值非法, 最好使用正则 var id = this.selector.substring(1); var obj = document.getElementById(id); if(obj)//如果对象存在 { if(newValue == undefined) return obj.value;//获取目标对象的值. obj.value = newValue;// 将目标对象的value属性设置为newValue. return this; //为了使方法可以连续调用。 } //end val function body } }; Shaka.fn.init.prototype = Shaka.fn; alert('object old value is '+Shaka('#myInput').val()); alert(Shaka('#myInput').val('I am 3 years old now!').val()); </script>
接着来
到目前为止我们已经创建一个可以工作的框架雏形,为了使程序可以更方便地被调用,比如jQuery可以使用$符号来简写,我们也弄一个,在此之前我们先来回顾两个东西: 1.我们在脚本中可以这样定义变量: varfoo='someThing'; bar='otherthing'; 这样两种写法都是合法的,但是意义完全不同,第一个语句创建了一个新的变量,而第二个是定义了window对象的一个属性,相当于window.bar='otherthing';,因此我们想使我们的Shaka具有这样的调用方式能力:$.someMethod();就需要将Shaka设置为window的一个属性,于是我们的Shaka构造函数就得写成这样: varShaka=window.Shaka=window.$=function(selector){returnnewShaka.fn.init(selector);}; 2.javascript的匿名函数. 创建并执行一个匿名函数的基本形式: (function(){ alert('Hello World!'); })(); 为什么要用到匿名函数呢,因为我们不想把Shaka的内部实现暴露出来,这样容易与其它代码冲突,只提供一个单一的入口,我们可以这样测试一下:<script type="text/javascript"> (function(){ function privateFunction(){ alert('You can not see me, haha'); }; })(); function publicMethod() {alert('I am public');}; alert('匿名函数内部的函数是不可访问的, privateMethod 目前是: ' + typeof privateMethod); alert('全局函数可访, publicMethod 目前是: ' + typeof publicMethod); </script> 然后,还有一个问题需要解决,俺们的框架做出来了但是还很简陋,在这之前我们需要让它与其它的框架协同工作,因此带来一个问题, 如果我们都使用$作为简写形式就会冲突了, 象jQuery一样,我们需要提供一个noConfilit的方法“出让”$的使用权。在我们的程序最开始处加入下面的代码: var_$=window.$; 意思是将此前定义的$对象引用放到_$中,然后我们再给Shaka扩展一个方法出来,如果其它开发者需要自行扩展的话也可以使用这个方式(jQuery的extend方法提供了更为强大的功能,请大家自行研究): (function($){//extendmethoddefinition.})(Shaka); 意思是将Shaka作为这个匿名函数的参数来调用这个方法。 前面我们讲过Shaka.fn就是Shaka.prototype的别名,因此我们要在Shaka.prototype里面添加新的方法就可以写成这样 (function($){ $.fn.noConflict=function(){ window.$=_$;//把$还给在开始处取得的引用. }; })(Shaka); 现在我们来看一个完整的:<form method="post" action="" name="myform"> <h1> 我几岁了?</h1> <br /> <input id="myInput" type="text" value="Hello world!" size="50" /> <br /><br /> <input id="otherInput" type="text" size="50" /> </form> <script type="text/javascript"> //我们在这里模拟一下在这之前如果加载了其它框架的情形, 这个时候window.$不为空. window.$ = { whoAmI: function(){ alert('This function result is from other js lib.');} }; (function(){ // 创建最外层匿名函数. window._$ = window.$;//将别的框架定义的$暂存. //给Shaka加上$ 的别名. var Shaka = window.Shaka = window.$ = function(selector) { return new Shaka.fn.init(selector); }; Shaka.fn = Shaka.prototype = { init: function(selector) { if(selector) this.selector = selector; return this; }, val: function(newValue) { //start val function body if(!(this.selector && this.selector.indexOf('#') == 0 && this.selector.length != 1)) return; //简单地判断传入值非法, 最好使用正则 var id = this.selector.substring(1); var obj = document.getElementById(id); if(obj)//如果对象存在 { if(newValue == undefined) return obj.value;//获取目标对象的值. obj.value = newValue;// 将目标对象的value属性设置为newValue. return this; //为了使方法可以连续调用, 返回当前实例。 } //end val function body } }; Shaka.fn.init.prototype = Shaka.fn; })(); //扩展新的方法. (function($){ //alert(obj.fn); $.noConflict = function(){ window.$ = window._$;//把$还给在开始处取得的引用. }; })(Shaka); //如果没有引入其它的框架,可以这么写 //alert('object old value is '+$('#myInput').val()); //alert($('#myInput').val('I am 3 years now!').val()); //强制使用完整名称. Shaka.noConflict(); alert('object old value is '+Shaka('#myInput').val()); alert(Shaka('#myInput').val('I am 5 years old now!').val()); //Shaka('#otherInput').val('这里的值是使用Shaka(\'#otherInput\').val()方法来写入的哦'); //或者可以这样写也行,仍然使用$, 把Shaka作为匿名函数的参数$传进去。 (function($){ //又可以用$了, 哈哈 $('#otherInput').val('这里的值是使用Shaka(\'#otherInput\').val()方法来写入的哦'); })(Shaka); //现在仍然可以使用$调用其它框架的方法. $.whoAmI(); </script> 现在好象不错了,我们的Shaka baby已经5岁了;) , 当然这还只是个简陋的东西,要实现健壮而强大的功能还需要付出很多努力, 希望诸位爹妈把自己的孩子培养成人才, good luck! |