浅析jQuery原理并仿写封装一个自己的库

【前言】最近项目忙的脚不沾地,刚刚结束,准备整理一下以前写的一些学习笔记和技术文章。本文原是很久之前看jq源码时写的片段,隔了很久再看都忘得差不多了。简单整理出来,做个记录。

                

为一名前端工程师,jQuery是我们熟的不能再熟的工具之一,其强大的功能和近乎完美的兼容封装使其成为前端领域必备的技能。用了很长时间的jq,却一直没有去探索学习过jQuery源码,似乎不算是一名合格的前端er。最近趁着空闲研究了一下,jQuery源码可谓是深邃如海,奥妙无穷,平时工作中偶尔需要封装工具,也不必像它这样面面俱到,但是简单学习其封装思路还是很有意义的。后面还要抽时间仔细学习每一块的源码。这里简单说一说我对jq框架封装的理解,并仿照着封装css的两个方法。

首先,jQuery的本质是一个封装了众多方法的库

这个库的框架是一个闭包

其最外层的框架或者说其骨架如下:

(function(window, undefined) {
    var jQuery = function () {}
    window.jQuery = window.$ = jQuery;//暴露全局
 })(window)复制代码

可以很明显的看出其框架结构,在一个闭包(沙箱)中,所有的函数、方法都放在沙箱内部,有一个名为jQuery的函数,这是核心函数,所有的成员都围绕它运转。

为什么要传入windowundefined,原因很简单,暴露局部变量为全局,自不必说,还有一方面是为了精简代码,减少变量检索时间,window作为形参直接在函数作用域内被检索到,无需每次再向上查找全局,极大地节省了性能提高了效率。

至于undefined,这玩意是为了处理IE8以下的一个小问题,在这些老式浏览器中,undefined是可以作为变量名并被重新赋值的,但是新式浏览器已经不支持这种做法。这里传入undefined就是为了防止undefined被重新赋值。

此时,我们可以在外部直接new一个jQuery的实例对象:

console.log(new jQuery())//空对象,只有一个__proto__属性复制代码

但是显然,这个对象和我们实际使用的jq对象相去甚远,甚至看不到相似之处,别着急,一步步来。

我们的目标是实现类似jq的方式,在外部可拿到jq对象如$('div'),这个jq对象简化一下,大致是如下结构:

['div','div','div',length:3]复制代码

这就意味着,我们必须传入一个元素选择器,在构造函数内接收,通过一些方法,返回出来这么一个对象。

因此,我们先做一个并不严谨的假设,假设jQuery是一个构造函数,通过它来创建对象。

在沙箱内部的jQuery‘构造函数’内,我们传入一个选择器,然后就可以通过构造函数new出对象来:

var jQuery = function (selector) {
    var ele = document.querySelectorAll(selector);
    Array.prototype.push.apply(this, ele);
}复制代码

docuemnt.querySelectorAll获取的是一个‘伪数组’或者说集合,得到的dom对象都存放在ele上面,但是我们需要在jq对象上拿到这些对象,所以我们必须手动的将这些对象添加到实例上。借用数组的push方法,不仅能够方便快捷的达到目的,而且数组的length属性可以自动更新。这是一个小技巧。

这里的this指向,毫无疑问就是jQuery的实例对象,因此,我们只需要在jQuery的原型上添加方法,就可以使外部的实例对象访问到这些方法,这已经非常接近jq的思路了。

在此基础上,我们简单给原型上添加两个方法css和html:

jQuery.prototype.css=function(){
    console.log('hello css')
},
jQuery.prototype.html=function(){ console.log('hello html')
},
...复制代码

通过给原型添加方法,jQuery实例对象可以直接访问使用这些方法,但是这么做似乎太麻烦了些,如果有几十上百个方法,每次都这么添加,代码太过冗余。

因此,使用原型替换的思想,改变原型指向到某个对象上,给这个对象添加方法。可以节省很多代码。

jQuery.fn = jQuery.prototype = {
   constructor: jQuery,  // 手动添加了丢失的constructor属性 
   css: function() {
       console.log("css is ok again");
   }, 
   html: function() {
       console.log("html is ok again");
   } 
}复制代码

其中,为了书写方便,我们将jQuery的原型jQuery.prototype赋值给jQuery‘构造函数’的一个属性 jQuery.fn ,后者可以完全代替前者。

至此,我们在外面new 一个jQuery对象就可以拿到一个接近原版的jq对象了,也可以访问原型链上的方法和属性。但是似乎还有哪里不对,new对象这个操作似乎应该在内部完成?没错,我们继续完善它。

在进一步完善我们的小jQuery之前,我们要打个岔,回忆一下工厂函数是怎么回事。

关于工厂函数:

 作用:创建实例对象,然后把实例对象给返回出去。

function Person(name, age){
     this.name = name;
     this.age = age;
}
//上面是构造函数,我们创建对象的做法:
var xm = new Person("xm", 20)
console.log(xm);//创建了xm对象

var xh = new Person("xh", 21);
console.log(xh);//创建了xh对象复制代码

将这个过程封装一下:

 function $(name, age){//$就是工厂函数
    return new Person(name, age);
 }// 省去外部的new操作,还能得到实例对象

var xm = $("xm", 20);
console.log(xm);

var xh = $("xh", 22);
console.log(xh);//得到两个实例对象复制代码

可见,封装好的工厂函数可以通过直接调用,批量创建对象出来。我们回到jQuery的话题来

按照这个思路,我们将jQuery函数也封装一下,使之变成工厂函数:

var jQuery = function (selector) {
    return new jQuery(selector);
}复制代码

完成了吗?似乎完成了?但是又好像有哪里不对,怎么看着这么眼熟,这不是隔壁的递归函数吗,自己调自己,把自己玩死了。so。。?难道jQuery不能当做工厂函数吗?那么问题来了,他不做谁能做呢?或者,他不是构造函数?听着有点乱,但还真被我们蒙对了!

实际上,在jQuery中,真正的构造函数,并不是jQuery函数!我们先前的假设要改一改了。

真正的构造函数,另有其人,不兜圈子了,直接上结论:jQuery函数的真正作用是“工厂函数”,正牌儿构造函数是jQuery.fn.init

这个jQuery.fn.init是什么鬼?怎么就把正主jQuery赶下位上台了呢?

直接上结论:这个jQuery.fn.init其实是jQuery函数的原型上的一个方法,它是真正的构造函数,通过它创建对象。

那么我们前面的代码要改改了。该挪窝的挪窝,该上位的上位:

var jQuery = function (selector) {
    return new jQuery.fn.init(selector);
}复制代码

init函数去它该去的地方:

jQuery.fn = jQuery.prototype = {
    constructor: jQuery, // 手动添加了constructor属性
    init: function(selector) {
    var ele = document.querySelectorAll(selector); // this??? ==> init的实例对象 
    Array.prototype.push.apply(this, ele);
    } 
}复制代码

经过这么一改,this的指向发生了变化,原本存放在jQuery原型上的dom对象们,现在变成了init的儿子,this指向了init。但是我们的方法都是存放在jQuery原型上的,难道还要手动搬回来?算了算了,太麻烦,还好有原型链这个好东西。手绘了一张草图,将就看一下:


init的实例对象想要使用jQuery的方法,丝毫不难,只需要改变原型链指向即可,将自己的原型指向由原本指向init.prototype改为指向jQuery.prototype即可,结果:


代码层面即一句话:

 jQuery.fn.init.prototype = jQuery.fn;复制代码

至此,我们的jQuery架构基本搭建完毕。此时,在沙箱外面,不需要手动new了,直接调用$(),例如$('div'),已经得到了和原版jQuery一样的对象。

剩下的就是添砖加瓦,封装一些方法了,我们以css方法为例,做个简单的封装。

完整代码如下:

(function(window, undefined) {

    var jQuery = function(selector) {  //jQuery是工厂函数
        return new jQuery.fn.init(selector); //传入选择器,实例化对象
    }  //参数selector会传入init

    jQuery.fn = jQuery.prototype = {//工厂函数的原型   
        constructor: jQuery,
        init: function(selector) { //由jQuery一路传来的形参 
            var ele = document.querySelectorAll(selector); //实现获取对象。         
                 // this ==> init的实例对象    
                 // 把获取到的元素添加到init的实例对象上          
            Array.prototype.push.apply(this, ele);
        },

        css: function(name, value) {// 通过判断参数的个数就能确定css方法要实现什么功能          
            if (arguments.length === 2) {
                // 设置单个样式 
               // 是把获取到的所有元素都设置上这个样式 
               // this ==>$("p");  伪数组,有length属性,是需要把伪数组中每一项都设置上样式 
               for (var i = 0; i < this.length; i++) {
                 this[i].style[name] = value; 
               }        
             }else if(arguments.length === 1){
                //说明是个对象 设置多个样式 || 获取样式 
               if (typeof name === "object") { 
                   // 设置多个样式 需要给获取到的所有元素都设置上多个样式
                  for(var i = 0; i < this.length; i++){ 
                       //this[i] ==> 每一个元素  
                      // 循环的是对象,是设置的样式和样式值
                      for(var k in name){  
                          this[i].style[k] = name[k]; 
                       }
                    }
             }else if(typeof name === "string"){ 
                   // 获取样式  注意点: 获取第一个元素对应的值
                    // this ==>$("p") this[0]  ==> 获取到的元素中的第一个元素 
                  // style 操作的是行内样式 
                  //window.getComputedStyle(元素, null); 获取在元素上其效果的样式
                  // 返回值: 是一个对象
                    return window.getComputedStyle(this[0], null)[name]; 
                 }
              }
            return this;// 目的:实现链式编程
         }
     }
    // 修改init的原型对象 目的是为了让init的实例对象可以访问jq上的方法
    jQuery.fn.init.prototype = jQuery.fn;
    window.jQuery = window.$ = jQuery;
})(window)复制代码

css方法的封装略显繁琐,但css这玩意不一直都这样么,在js中处理css,吃力又难受。但也没啥好办法,还好这部分没什么难度。

但是有两点仍然是需要我们注意并且必须做到的,就是关于jQuery的两个主要特点:

隐式迭代和链式编程。

前者使jq所设置的所有样式对所有获取到的对象都起作用,后者则要求在方法的封装结尾,必须返回该对象,以供连续调用实现链式编程。如果封装方法没做到这两点,那么封装出来的也就跟jq没啥关系了,这是需要格外注意的。

【结语】本文是很久以前学习时写的片段整理而成,限于个人水平,对jq封装的思想可能理解的不够深入,可能有许多地方说的不够严谨或者似是而非,还请看官大佬们不吝指出。感谢。


你可能感兴趣的:(浅析jQuery原理并仿写封装一个自己的库)