高级前端修炼之路:深入理解JQuery底层源码

      对于前端开发人员来说,JQuery并不陌生。JQuery是一个封装好的JavaScript库,它包含多个可重用函数,用来辅助我们简化JavaScript开发。JQuery面向用户良好的设计使我们在开发过程中不用去记忆大量原生的文档 API,极大的提高了我们的开发效率。JQuery改变了一个时代编写JavaScript的方式,虽然现在的组件化框架慢慢让JQuery淡出了我们的视线,但毫无疑问,JQuery的封装思想依旧是值得每个JS开发人员学习和借鉴的。

      今天这篇文章将带领大家学习部分JQuery函数,并对它们进行源码模拟实现。在学习源码的过程中,对我们来说最重要的,是要关注每个方法的设计思想。JQuery独特巧妙的设计思想是它的精华所在,只要掌握了代码的设计思想,即使是离开这些插件库,我们也能自己实现自己的封装插件。

      现在我们开始来探索JQuery的各种方法。首先我们创建一个立即执行函数来形成独立的作用域,以避免污染全局变量。

      除了工具类方法,使用JQuery的第一步几乎都是选择dom元素。JQuery有两种方法可以选择dom元素:$()和jQuery(),这两个方法都是一样的,我们只需要写个jQuery()函数,让$和jQuery指向同一个函数就好了。

      接着就要发挥jQuery函数选择元素的作用了,我们这里先只对class选择器和id选择器进行模拟。

      我们用JQuery插件选中class名或id名,返回的都是一个init()函数的实例,说明这里用到了new操作符,我们可以jQuery函数的原型上写一个初始化函数init(),然后每次调用$()选中元素都返回该函数的实例:

 

      对于id选择器来说,原生js返回一个dom元素,对于class选择器则返回一个类数组,类数组中依次包含选中的dom元素。class选中后返回的类数组始终有一个length属性,我们可以根据该属性对id和class进行区分,并将其封装成jQuery对象返回:

     

      我们看到自己写的方法返回了和原来JQuery插件类似的结果,但这里缺了length属性,我们可以自己给它们加上length属性:

 

      这样效果就一致了。现在即然能拿到JQuery选中的class元素或id元素,我们就可以扩展JQuery的其它方法了,我们首先来扩展JQuery的css()方法。

      JQuery插件中设置css可为css方法添加一种或多种属性,设置多种css属性传入的参数必须是对象的形式。只添加一种属性比较简单,我们就直接模拟设置多种css属性的css方法:

       这里循环遍历this长度,是因为有可能我们选中的class不止一个元素,所以要循环遍历每个元素,再循环遍历对象参数中的每个属性依次给dom元素设置上。然而,现在选中的JQuery元素是不能直接使用css方法的,因为我们把css方法定义在JQuery对象的原型上,而选中的元素是jQuery原型上的init函数的实例,实例寻找init的原型是找不到css方法的。

           我们这里只要改变init的原型指向,让它指向jQuery的原型,这样init函数的实例就可以找到jQuery原型上的其它方法了:

      然而,JQuery的精髓就在于它的链式调用,它可以在使用完某个方法后继续给选中的元素使用下一个方法,那我们要怎样实现这个功能呢?

      答案其实很简单,我们只要在每次调用这个方法后将原来选中的元素返回出去,就可以供下一个方法继续使用了:

      this对象指向调用它的对象,也就是选中的jQuery元素,因此直接返回this就可以得到我们想要的效果。

      接下来我们来模拟JQuery的get()方法,当给JQuery的get()传入数字,会返回所选中的某个dom元素。如果不传则返回一个数组,数组中包含所有选中的元素。如果传入的数字是负数,它还会帮我们从后面开始选中dom元素:

           我们直接来实现这个方法:

      要注意这里返回的是dom元素而不是JQ对象,所以使用这个方法后面是不能使用链式调用的。对于将原来JQ对象选中元素后的类数组转换为数组,我们还可以简写成 [ ].slice.call() 的形式:

      当然,这个get方法,我们还可以运用三目运算符继续简化:

      如果你看过JQuery的源码,你会发现源码里面很多方法都是用这种三目运算符简化的形式。

      JQuery中还有一个eq() 方法,这个方法和get方法很相似,都是选取某个位置的元素,区别在于返回的不是dom元素而是包裹dom元素的JQ对象:

     

      返回的JQ对象可以使用其它的JQuery方法。如果不传数字,则会返回一个空的JQ对象而不是原来选中的整个JQ对象。我们现在来尝试模拟这个方法:

      我们返回用JQuery()方法调用后的JQ对象,然而,此处的JQuery() 方法我们只能实现对class和id的选中,不能实现对dom元素的选中,我们现在要继续丰富JQuery()方法,让它可以直接选中dom元素:

      对于dom元素来说,它们都是Element对象的实例。我们只要判断它的原型链上有没有Element对象,就可以判断它是不是文档中的dom元素。

      现在我们使用eq()方法选中元素已经可以返回JQ对象了:

       接下来我们来模拟add() 方法,JQuery里add() 方法可以把元素添加到已存在的元素组合中,并可以接着对元素组合使用其它方法。

      仔细观察用JQuery插件使用add方法后的返回值,可以看到有一个prevObject属性,保存着调用add方法前的元素。

      我们创建一个jQuery空对象,然后将调用add方法前后选中的元素放入这个空对象,并给这个空对象的prevObject设置为调用add方法之前的选中的jQuery元素,最后将这个新的jQuery对象返回:

      不过,对于jQuery() 方法,我们目前只实现了对class名、id名和dom元素的获取,还没提供传入空参数返回空jQuery对象的方法,我们现在来进行补充:

      对于传入空参数的情况,我们把构造函数隐式创建的this对象返回就可以得到一个空的jQuery对象了:

     现在我们来试一下,已经可以返回正确的结果了:

      之所以要模拟这个prevObject属性,是因为它是JQuery里的end() 方法的实现方式。回退方法end() 会返回组合元素之前的元素,即直接返回prevObject属性保存的元素。

      有了prevObject属性,实现end() 方法只需要一行代码就够了:

     

      其实JQuery插件的eq()方法后面也可以使用end() 方法回退到上一个元素对象,我们给eq方法补充prevObject属性:

        这样也能在eq() 方法后面使用end() 方法了:

     不过,即然add() 方法和eq() 方法都用到了prevObject属性,那么我们干脆直接这个赋值操作封装到一个方法里。JQuery是用一个入栈操作函数来实现这样的功能的:

      这样我们就可以直接通过入栈函数进行prevObject属性的设置了:

      下一个要讲的是JQuery的on() 方法。JQ的on() 方法用于给元素绑定事件,该方法一般包含两个参数:事件类型和回调函数。可以用on给元素绑定同一个事件类型触发不同回调函数,也可以绑定不同的事件类型。

      上面的方法都是JS自带的事件,on方法也可以绑定自定义事件,然后用trigger方法来触发:

      我们直接来模拟on方法的自定义事件功能,首先要遍历每个选中的元素,给每个选中的元素都绑定上事件:

      然后我们再定义一个空对象,以便将绑定的事件添加进去:

      接着我们就可以将绑定的事件类型和回调函数作为空对象的属性名和属性值保存进去。我们这里用数组保存回调函数,目的就是当对象中已经存在相同的事件类型,可以直接给对应的数组继续添加回调函数:

         现在on方法已经模拟完成,我们就来模拟trigger方法。trigger除了可以直接触发on方法绑定的事件,还可以给绑定的事件传参,如图所示:

 

      on绑定的回调函数第一个参数默认返回该事件类型的Event对象,我们不对这个参数进行考虑,只实现trigger触发事件和传参功能。首先我们先获取传入的参数:

      arguments是JS函数的实参列表,保存传入的实参。它是一个类数组,当传入的参数大于一个时,说明除了触发事件类型还传入了其它参数,我们用 [ ].slice.call() 方法将实参列表它转换为数组,并从第二位参数开始截取。

      接着我们来触发dom元素的绑定事件,我们遍历每个dom元素,并查看当前dom元素的cacheEvent对象中是否有对应的事件类型,如果有,就遍历该事件类型所对应的数组。

      forEach() 遍历每次返回的ele就是数组中的函数,我们对每个函数执行,不过,JQuery插件中回调函数执行时的this是始终指向绑定对象的,我们这里得手动修改函数的this指向,让它指向调用trigger方法的dom元素:

      这里forEach函数里的this指向全局对象,我们手动将dom元素保存到self中并传给apply()方法使函数指向dom元素并执行。这样就实现了我们自己的trigger方法了:

      接下来要讲JQuery中的队列方法。队列是一种先进先出的思想,JQuery中的队列是一个或多个等待运行的函数,使用queue() 方法可以用于获取或设置当前匹配元素上待执行的函数队列,而使用dequeue() 方法则用于移除每个匹配元素的指定队列中的第一个函数,并执行被移除的函数。具体用法如下:

 

      给queue方法传两个参数代表给队列添加函数,只传第一个参数则代表获取队列中的函数数组。我们可以为匹配元素的某个队列添加多个执行函数,每次用dequeue() 都会在该队列中将第一个函数弹出并执行:

          每执行完队列中的一个函数,都要手动调用才会继续执行下一个队列函数。在queue方法的回调函数中有一个参数,能代替dequeue() 方法,为我们自动执行下一个队列函数,我们一般用next来代表它:

      选择的dom元素可以不止一个,队列方法会为每个dom元素都绑定上队列事件,并按顺序执行:

      现在我们开始来模拟JQuery的队列方法吧,我们先对queue()方法中传入的参数进行保存:

      在JQuery中,只传一个参数会返回队列中保存的函数数组,如果是对多个元素进行队列获取,则默认取出第一个dom元素中的保存的队列函数:

      如果传入的参数有两个,即有传入回调函数,则进行函数绑定。先判断是否已经设置过该名称的队列,如果没有,则创建该属性并将回调函数放入数组中作用属性值;如果已经设置过该队列名,则继续在数组后面添加回调函数:

      我们这里可以使用简化的三目运算符方式:

      不过,我们不能忘了选取的dom元素可能不止一个,这里我们只给选中dom元素的第一个添加队列属性,我们要遍历每个dom元素并给它们每个都添加上队列属性:

      最后将this对象返回以便该方法执行链式调用:

      入列方法写好后,我们就要写出列方法了。我们首先遍历每个dom元素,并给它们包装成jQuery对象以便调用方法:

       接着将所传的队列名保存起来,同时从dom元素上取出该队列的执行函数数组:

 

      再接着使用shif() 剪切出数组中的第一个函数:

 

      如果数组中没有函数,则不执行任何操作,直接将this返回:

      当队列所对应的数组中存放有函数时,我们先定义一个参数next等于一个函数,并在函数中执行下一个出列操作。在我们执行回调函数时把next当作实参传进去,这样回调函数中就可以通过next参数继续执行下一步dequeue()操作了:

      现在我们来运行一下我们的队列方法:

     控制台打印出了正确的结果:

      JQuery里的delay()和animate() 方法,都使用了队列思想。animate()方法之所以可以在调用多个animate() 方法后一帧一帧的执行动画效果,就是通过队列函数一个一个的执行。delay() 方法可延迟元素动画效果的执行,可传入数字参数作为延迟时间,不传队列名的话默认会向队列中名为‘fx’的队列添加待执行的回调函数。这个方法其实是配合着animate() 使用的,因为animate() 方法也会默认向名为’fx‘的队列添加执行函数,并触发执行’fx‘队列中的函数。首先我们来模拟delay() 方法:

      首先对元素本身的‘fx‘队列添加执行函数,因为是延迟方法,所以这里采用定时器的方法,规定指定时间后执行’fx’中下一个函数(即animate()方法绑定的函数):

      接着来写animate()方法,animate方法我们模拟它的两个参数:动画效果的对象参数和回调函数。

     对于animate()方法,我们首先创建一个简易的运动函数,用于让元素的样式发生变化:

      运动函数将对传入的对象进行遍历,然后依次修改所对应的CSS属性,并且在修改完后执行给startMove()所传入的回调函数(注意这里不是指传入myAnimate()的回调函数)。

      接着我们就要写给队列‘fx’添加的待执行函数了,我们遍历每个dom元素,并对每个dom元素调用依次运动函数,然后在所有dom元素都遍历完成,再执行myAnimate ()传入的回调函数,并且自动调用下一个‘fx’队列中的函数:

      写完待执行函数,就要把它添加到队列‘fx’中了,我们这里分为两种情况:

      当队列‘fx’还未创建时,说明当前myAnimate() 方法是第一次调用,并且前面没有调用过delay() 方法,我们此时将待执行函数添加进队列‘fx’并立即执行它。而当队列’fx’已被创建过,说明前面已经调用了delay()方法,我们只对队列 ‘fx’ 进行入列操作。但在入列操作之前要触发delay() 方法添加的队列函数。所以我们判断当队列长度为1时就先触发’fx’里的回调函数,触发过后后面向‘fx’中添加的回调函数就会在delay() 方法里的next参数调用下继续触发接下来添加进 ’fx’ 队列中的函数。

      现在我们的animate() 方法已经基本模拟完了,但是还差一步,我们的运动函数,是在一瞬间改变css样式的,而我们用animate() 方法产生的动画效果,是有缓冲效果的,我们这里来完善一下运动函数:

      我们使用定时器让元素的样式随着speed 一点点改变到目标样式值,等到成功改变到目标样式值后再清楚定时器并执行回调函数。这里的window.getComputedStyle() 方法是用来获取元素当前css样式的方法(不直接用obj.style[attr]获取是因为一开始得不到相应样式值会返回一个空字符串,用parseInt() 后会变成NaN,具体原因这里暂时不探讨。而window.getComputedStyle() 会实时返回当前元素所对应的样式值)

      现在我们的delay()方法和animate()方法已经模拟完毕了,当然,模拟的结果并不是完善的,比如在JQuery中animate()方法前面可以连续调用多个delay()方法再执行,而我们这里一次只能调用一个delay(),因为我们对队列‘fx’的长度判断固定在一位才能执行出列操作。但无所谓,对于delay()方法和animate()方法,我们更主要的是在于学习它们队列思想的应用。

      接下来要讲JQuery的一些工具类方法。工具类方法是直接通过$符号就可以进行调用的方法,不需要选择任何dom元素。首先我们来讲$.Callbacks() 方法。

      $.Callbacks() 是JQuery的回调管理函数,每次调用Callbacks(),都会返回一个callbacks对象,这个对象有一个仅仅只有自己才能访问的数组。我们在调用了这个方法后,可以通过add()方法向里面添加回调函数,通过fire() 方法执行里面的回调函数。

 

      我们也可以在fire() 里添加参数传给add里面的每个回调函数:

      add() 方法可分多次添加回调函数:

      但默认情况下,执行fire() 方法后再通过add添加的方法不会执行:

      默认情况下,fire() 方法也可以多次调用:

      如果我们向$.Callbacks() 添加 ‘once‘ 参数,则fire() 只能被调用一次:

      如果我们向$.Callbacks() 添加 ‘memory’ 参数,则在fire() 后面添加的函数也会被执行:

          可以同时添加once 和 memory 参数,那使用效果会把两者相结合:

      还可以向$.Callbacks() 添加 ‘unique’ 参数和 ’stopOnFalse’ 参数。unique 参数表示重复添加的回调函数只会被添加一次,stopOnFalse 参数表示某个回调函数返回false之后中断后面的回调函数的执行。我们现在暂时只模拟 once 参数和 memory 参数的作用。

      首先我们定义好一些接下来要用到的变量,并且将返回值作为一个对象添加进add和fire方法:

      然后先写好add方法,为了方便这里的add只让它每次接收一个参数:

      我们首先取得调用fire() 方法时传入的实参,然后依次遍历add方法所添加的函数:

       接着我们来完善$.Callbasks() 传入参数的功能,如果传入了once参数,则在第一次调用fire方法后把函数列表清空,这样下次调用fire方法就不会重复执行回调函数了:

      接着要完善 memory 参数的功能,memory参数表示在fire 过后还会执行add 新添加的函数,js是线性执行的,我们得在add里面继续触发fire方法:

      我们首先得判断是否执行过fire方法,每次执行fire方法,我们要将函数列表的游标置0,然后用isFired参数表明已经调用过fire方法。add 方法判断是否执行过fire 方法,执行过的话就重新调用fire方法,在刚添加进函数列表的第一个位置开始遍历执行新添加进的函数。

      现在就模拟完毕了,如果同时传入了once 和 memory 参数,这个工具函数也会依据上述流程正确的执行。

      讲完$.Callbacks() 方法,就来讲JQuery的延迟对象方法$.Deferred()。$.Deferred() 是一个构造函数,用来返回一个链式实用对象方法来注册多个回调,并且调用回调队列,传递任何同步或异步功能成功或失败的状态。

      $.Deferred() 方法可通过done、fail和progress 分别注册成功回调函数、失败回调函数和进行时回调函数,再分别通过resolve、reject和notify方法相继触发,如图所示:

      当调用了resolve触发成功回调函数后,就不会再触发失败回调函数或进行时回调函数。当调用了reject触发了失败回调函数后,就不会再触发成功回掉函数或进行时回调函数。而触发了进行时回调函数后,还可以接着触发成功回调或者失败回调函数,或者继续触发进行时回调函数。

      如果我们要将触发Deferred延迟对象回调函数的功能封装起来,只允许外界进行回调函数的注册,不允许触发,我们可以用延迟对象的promise() 方法:

     deferred对象也是支持链式调用的,我们在注册回调函数时可进行链式调用:

      此外,我们还可以用then方法简化注册的写法。then方法可传入三个参数,分别对应成功回调函数done、失败回调函数fail和进行时回调函数progress:

      可以对同一个延迟对象调用多个then方法,如果上一个then方法触发progress回调函数,则一定会立刻触发下一个then方法的progress回调函数。如果上一个then方法触发了success或fail回调函数,下一个then方法并不会固定触发某个回调函数。此外,上一个then方法的返回值可作为下一个then方法函数执行的参数:

结果:

第一次触发失败回调,第二次触发成功回调:

第一次触发成功回调,第二次触发成功回调:

第一次触发进行时回调(立刻触发下一个then的进行时回调),然后第一次接着触发了失败回调,第二次触发成功回调:

      除了触发同一个延迟对象的then,我们还可以让每次触发的回调函数返回值等于一个新的延迟对象,并根据我们的需求直接触发自己想要的回调函数:

结果:

第一次触发旧的延迟对象上的成功回调,第二次触发新的延迟对象上的成功回调:

第一次触发旧的延迟对象上的失败回调,第二次依然触发新的延迟对象上的成功回调:

第一次触发依次旧的延迟对象上的进行时回调两次和失败回调,第二次依然触发新的延迟对象上的成功回调:

      讲完了$.Deferred() 的一些用法,我们现在就正式来模拟一下这个方法部分功能的实现。$.Deferred方法是基于我们上面的$.Callbacks方法的,我们首先创建一个二维数组,用来定义三个$.Callbacks() 对象:

      接着我们创建一个deferred空对象,然后循环遍历该二维数组,给deferred空对象添加上相应的方法,我们先来给deferred空对象添加上done、fail和progress注册属性,并把各自对应的$.Callbacks() 对象添加上回调函数:

      接着给deferred对象添加上resolve、reject和notify触发属性,用于触发各自所对应的回调函数:

      此处如果触发了成功回调和失败回调函数,则不再执行其它回调函数,所以我们这里要给它加个锁进行判断:

      我们这里暂时不对延迟对象的promise()方法和then()方法进行模拟,就把Deferred方法最基本的功能实现了:

 

触发了进行时回调和成功回调:

触发了失败回调:

触发了成功回调:

      JQuery的源码有一万多行,每个方法都去学透其实也不太现实。今天的文章主要是为了让大家了解一下JQuery的底层设计思想,实际的JQuery源码对各个浏览器都做了兼容,功能也更全面,所以更加复杂。JQuery的底层源码十分严谨,思想设计十分精妙,它的链式调用思想、入栈思想、队列思想等都是非常值得我们学习和借鉴的。就连ES6新增的Promise对象,也是借鉴于JQuery的延迟对象Deferred来实现的。关于JQuery的底层源码,今天就暂时探讨到这里。以后如果有机会还会对JQuery的一些方法进行补充,感谢收看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值