从零实现一个简易的jQuery框架之二—核心思路详解

如何读源码

jQuery总体框架甚是复杂,也不易读懂。可是若想要在前端的路上走得更远、更好,研究分析前端的框架无疑是进阶路上必经之路。可是庞大的源码每每让咱们不知道从何处开始下手。在很长的时间里我也被这种问题困扰着,本身也慢慢摸索到一个比较不错的看源码的“姿式”。html

必定不推荐的就是拿到源码直接开始啃,首先咱们必定要对这个框架的总体的架构有必定的了解,每一个模块之间的联系是怎样的;前端

而后找一找有没有关于源码分析的书籍,若是有的话那么恭喜你了,你能够直接跟着书的思路开始看源码;node

若是没有框架源码分析相关书籍的话,那么就只能本身啃源码了,能够从成熟框架的早期源码开始看起,这样一开始的代码量很少,多看几遍仍是能够理解的。git

看源码时不只要知道其然,还要知道其因此然。即不只要知道这样写,还须要知道为何这样写。这就要求咱们不只要看源码,并且要敲源码,换几种不一样的思路来实现源码实现的功能能让咱们更好的理解做者为何这样写。github

------------------------------------------------------------------------------------------分隔线,下面介绍jQuery框架的实现核心思路.编程

为方便阅读和理解,其核心代码只有70几行。 源码连接请点击这里,若是对您有用的话,欢迎star。数组

一、jQuery框架整体架构

(function(){ //替换全局的$,jQuery变量
var _jQuery = window.jQuery, _$ = window.$, //jQuery实现
    jQuery = window.jQuery = window.$ = function( selector, context ) { return new jQuery.fn.init( selector, context ); }; //jQuery原型方法
jQuery.fn = jQuery.prototype = { init: function( selector, context ) {}, //一些原型的属性和方法
}; //原型替换
jQuery.fn.init.prototype = jQuery.fn; //原型扩展
 jQuery.extend = jQuery.fn.extend = function() { ... }; jQuery.extend({ // 一堆静态属性和方法
 }); })(); 

二、$()实现细节

咱们知道使用jQuery的惟一入口就是全局属性jQuery、$。咱们能够先实现一个jQuery类。闭包

var $ = jQuery = function () { }; jQuery.fn = jQuery.prototype = { name : "jQuery", size : function () { return this.length; } }; var my$ = new $(); console.log(my$.name);

其实直接用jQuery生成一个jQuery实例,也能够实现jQuery框架相同的效果。可是jQuery框架并无使用new为jQuery类建立一个新实例,而是直接调用jQuery()方法,而后在后面链式调用原型链上的方法。以下所示:架构

$().size()

这是怎么实现的呢?也就是说咱们须要把jQuery即看做是一个类,同时又是一个普通的函数。而这个函数调用返回jQuery类的实例。app

var $ = jQuery = function () { return new jQuery();//返回类的实例
}; jQuery.fn = jQuery.prototype = { name : "xiaoyu", size : function () { return this.length; } }; var my$ = $(); console.log($().name); //Uncaught RangeError: Maximum call stack size exceeded

执行上述代码,提示内存外溢的错误,说明执行$()时出现了循环引用。可见执行$()不能返回jQuery的实例,而应该返回其它类的实例才不会致使栈溢出。实际上jQuery也是这么作的。

那么如何返回一个类的实例呢?

var $ = jQuery = function () { return new jQuery.fn.init();//产生一个init()的实例
}; jQuery.fn = jQuery.prototype = { init: function() { console.log(this); return this; }, name : "xiaoyu", size : function () { return this.length; } }; console.log($().__proto__ === jQuery.fn.init.prototype);//$().__proto__ -> init.prototype

执行上述代码,执行$()返回了一个实例对象,这已经很接近jQuery框架的。

可是还有一个原型指向问题:在jQuery中,执行$()函数返回的实例对象的__proto__指向的是jQuery()函数的prototype属性,而咱们本身实现的jQuery类执行$()返回的实例对象的__proto__指向的是init()函数的prototype属性。

因此咱们在执行$()函数以前,还须要手动改变init()函数的prototype指向,使其指向jQuery.prototype。

var $ = jQuery = function () { return new jQuery.fn.init();//产生一个init()的实例
}; jQuery.fn = jQuery.prototype = { init: function() { console.log(this); return this; }, name : "xiaoyu", size : function () { return this.length; } }; //在实例化前,将init.prototype覆盖为jQuery.prototype
jQuery.prototype.init.prototype = jQuery.prototype; console.log($().__proto__ === jQuery.prototype);//$().__proto__ -> jQuery.prototype

三、实现一个简易的DOM选择器

第二讲咱们已经完成了jQuery框架的基本的实现:执行$()函数可以返回一个jQuery对象。

咱们说过$()函数包含两个参数selector和context。其中selector表示选择器,context表示选择器的选择的内容范围。$()函数执行返回的是一个jQuery对象,是一个类数组对象。本质上是一个对象,虽然拥有数组的length和index,却没有数组的其余方法。

在jQuery中,假如咱们须要操做一个DOM元素,咱们能够这样选中它。

$('div').html("hello");//选中document下的全部div标签,并设置全部选中的DOM元素的innerHTML内容

下面咱们就实现一个简易的标签选择器的功能。

核心思路是:

  • 经过传入的selector参数,操做原生JS来实现DOM元素的过滤,获取咱们须要的DOM元素集合,并将DOM元素集合做为属性添加到jQuery对象中,并返回jQuery对象
  • 实现链式操做是经过在上一步操做结束时返回jQuery对象。
var $ = jQuery = function (selector,context) {    //定义类
    return new jQuery.fn.init(selector,context);    //返回选择器的实例
}; jQuery.fn = jQuery.prototype = {    //jQuery的原型对象
    init: function(selector,context) {    //定义选择器的构造器
        selector = selector || document;    //默认值为document
        context = context || document;    //默认值为document
        if (selector.nodeType) {    //若是传入的参数是DOM节点
            this[0] = selector;        //把参数节点传递给实例对象的index
            this.length = 1;        //设置长度为1
            this.context = selector; return this;    //返回jQuery对象
 } if (typeof selector === 'string') {//若是传进来的是标签字符串
            let ele = document.getElementsByTagName(selector);    //获取指定名称的元素
            for (let i = 0; i < ele.length; i++) {    //将获取到的元素放入实例对象中
                this[i] = ele[i]; } this.length = ele.length; return this; } else { this.length = 0; this.context = context; return this; } }, name : "jQuery", size : function () { return this.length; } }; jQuery.prototype.init.prototype = jQuery.prototype;
let div = $('div').size();

如上所述的代码,$()函数已经基本传入DOM元素和元素标签返回一个jQUery对象的功能。

经过上面实现的一个简易的DOM选择器,咱们知道:jQuery对象是经过jQuery框架包装DOM对象后产生的一个新的对象。框架为jQuery对象定义了独立的方法和属性(定义在jQUery.prototype原型属性上),所以jQuery对象没法直接调用DOM对象的方法,DOM对象也没法直接调用jQuery对象的方法。

咱们也能够很轻易地实现jQuery对象和DOM对象的相互转换。

  • jQuery对象转换为DOM对象:借助jQuery对象的类数组下标选择jQuery对象中的某个DOM元素。
  • DOM元素转换为jQuery对象:直接把DOM元素看成参数传递给$()函数,$()函数会自动把DOM对象包装为jQuery对象。

3.一、实现$('div').html("hello")功能

核心思路:在原型上封装一个html()函数,根据传递进来的参数来判断是获取第一个DOM元素的innerHTML仍是设置每个DOM元素innerHTML。

var $ = jQuery = function (selector,context) {    //定义类
    return new jQuery.fn.init(selector,context);    //返回选择器的实例
}; jQuery.fn = jQuery.prototype = {    //jQuery的原型对象
    init: function(selector,context) { //定义选择器的构造器
    //省略初始化构造器的主体代码
}, constructor: jQuery, //定义jQuery中的html()方法 html: function(val) { if (val) { for(let i = 0; i < this['length']; i++){ this[i].innerHTML = val; } }else { return this[0].innerHTML; } }, name : "jQuery", size : function () { return this.length; } }; jQuery.prototype.init.prototype = jQuery.prototype; let div = $('div').html('hello');

OK!一个简易的html()函数的功能已经实现完成了,咱们能够看一下jQuery源码是如何实现的。以便学习别人的编程思想。

html: function( value ) { return value === undefined ? (this[0] ?
                this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") :
                null) : this.empty().append( value ); },
//源码使用三目运算符判断参数是否为空,若是为空,则返回第一个元素的innerHTML;若不为空,则先清空匹配元素中的内容,并使用append插入值。

四、功能扩展函数extend

根据通常的习惯,若是要为jQuery或者jQuery.prototype添加函数或方法,能够直接经过"."语法实现,或者在jQuery.prototype对象上添加一个属性便可。

可是分析jQuery源码能够知道jQuery是经过extend()函数来实现扩展功能的,即插件功能。

这样作有什么好处呢?

extend可以方便用户快速的扩展jQuery框架的功能,但不会破坏jQuery框架的原型结构从而避免后期人工手动添加工具函数或方法时破坏jQuery结构的单纯性。

同时也方便管理。若是不须要某个插件时简单的删除掉便可,而不须要在jQuery框架源代码中去删除。

咱们本身也能够实现一个简单的函数扩展功能,只需把指定对象的方法复制给jQuery对象或者jQuery.prototype对象。

//接受一个对象做为参数(实现批量的扩展)
jQuery.extend = jQuery.prototype.extend = function (obj) { for (let key in obj) { if (obj.hasOwnProperty(key)) { this[key] = obj[key]; } } return this; }

五、命名空间问题

但还须要考虑的一个问题就是命名空间的问题:当一个页面中存在多个框架或者众多代码时,咱们是很难确保代码不发生冲突的。

因此不免会出现命名冲突或代码覆盖的现象。咱们必须把jQuery代码封装在一个孤立的环境中,避免其余代码的干扰。

咱们能够经过匿名函数执行,造成闭包,将代码封装在一个封闭的环境中,只经过惟一的入口window.jQuery访问。

(function(){ var jQuery = window.jQuery = window.$ = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced'
        return new jQuery.fn.init( selector, context ); }; })(window);

5.一、命名冲突

同时,为了防止同其余框架协做时发生$简写的冲突,咱们能够封装一个noConflictl()方法解决$简写冲突。

思路分析:在匿名执行jQuery框架的最前面,先用_$,_jQuery两个变量存储外部的$,jQuery的值。执行noConflict()函数时再恢复外部变量$,jQuery的值。

(function(){ var window = this, _jQuery = window.jQuery,//存储外部jQuery变量
        _$ = window.$,//存储外部$变量
        jQuery = window.jQuery = window.$ = function( selector, context ) { return new jQuery.fn.init( selector, context ); }; jQuery.noConflict = function( deep ) { window.$ = _$;//将外部变量又从新赋值给$
        if ( deep ) window.jQuery = _jQuery;//将外部变量又从新赋值给jQuery
        return jQuery; }, })();

至此,咱们已经模拟实现了一个简单的jQuery框架。之后就能够根据x项目须要不断的扩展jQUery的方法便可。

 PS:写文章不宜,若是这篇文章对您有帮助的话,但愿您多多点击推荐哦!

相关文章
相关标签/搜索