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

如何读源码

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

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

然后找一找有没有关于源码分析的书籍,如果有的话那么恭喜你了,你可以直接跟着书的思路开始看源码;

如果没有框架源码分析相关书籍的话,那么就只能自己啃源码了,可以从成熟框架的早期源码开始看起,这样一开始的代码量不多,多看几遍还是可以理解的。

看源码时不仅要知道其然,还要知道其所以然。即不仅要知道这样写,还需要知道为什么这样写。这就要求我们不仅要看源码,而且要敲源码,换几种不同的思路来实现源码实现的功能能让我们更好的理解作者为什么这样写。

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

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

1、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({
     // 一堆静态属性和方法
 });
})();     

2、$()实现细节

我们知道使用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类的实例。

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

3、实现一个简易的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.1、实现$('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插入值。

4、功能扩展函数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; }

5、命名空间问题

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

所以难免会出现命名冲突或代码覆盖的现象。我们必须把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.1、命名冲突

同时,为了防止同其他框架协作时发生$简写的冲突,我们可以封装一个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:写文章不宜,如果这篇文章对您有帮助的话,希望您多多点击推荐哦!

posted @ 2018-08-08 09:25  余大彬  阅读(1204)  评论(3编辑  收藏  举报