【jq 源码解析1-结构分析】

jq源码 | 2020-12-02 20:35:56 295次 3次

诶...朋友们好啊。 刚才有个朋友问我,他说你这看 jq 源码也没用。 我说我这个有用,这是化劲儿,传统代码是讲化劲儿的,四两拨千金。

文章大致划分为如下几个模块:

- 总体分析

- 选择器sizzle

- dom操作

- 样式操作

- 事件

- 动画


传统武术来说,一般都是先站桩,后练单招,然后才是练套路,接着开始拆单招,最后是进行对抗训练,写代码也一样,练完单招(使用各种框架),需要开始从源码中学习套路,最后再集大成于一体灵活运用与工作之中。任何需求来了都可以“不招不架就是一下”!

首先要练好内功,先练功后练拳,先分析下 jq 中的内功,在内功的支撑下打出各种组合拳。


代码结构

代码调试的时候可以直接使用打包后的代码,查看源码是使用 github 上的目录会更清晰一些。打包后的代码是一个自执行函数,采用严格模式:

打包后代码结构:
( function( global, factory ) {
    "use strict";
    // 模块化处理 支持 commonjs
    if ( typeof module === "object" && typeof module.exports === "object" ) {
        module.exports = global.document ?
            // 只有 document 对象
            factory( global, true ) :
            function( w ) {
                // node 中传入了 window 对象
                // 使用 var jQuery = require("jquery")(window);
                if ( !w.document ) {
                    throw new Error( "jQuery requires a window with a document" );
                }
                return factory( w );
            };
    } else {
        factory( global );
    }
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
    // 正常浏览器使用时将 $ jQuery 挂载到 window 上,可以直接使用
    if ( typeof noGlobal === "undefined" ) {
        window.jQuery = window.$ = jQuery;
    }
    return jQuery;
});


无 new 构建

一般在使用 jq 的时候不需要 new 这个过程,因为自身帮我们做了这件事,接、化、发一气呵成!

大体结构:
( function (window) {
    // ...
    jQuery = function (selector, context) {
        return new jQuery.fn.init(selector, context, rootjQuery);
    }

    jQuery.fn = jQuery.prototype = {
        init:  function (selector, context, rootjQuery) {
            // ...
            return this
        }
    }
    
    jQuery.fn.init.prototype = jQuery.fn;

})(window);

:先将 init 看成一个单独的函数,jQuery 做的事就是返回 init 方法的实例,并且 init 的原型 prototype 指向了 jq 的原型;

:将 init 放在 jQuery.prototype 上,因为要用化劲融入到敌方内部,偷取 this,你打我的劲化为我的力量;

:浑元形意太极中第二层级是后动手的快,也叫“慢打快”的快,表面你先用的 $,我 init 其实早就出手,将this劲发出去;

通过“接手、化手、发手”,即“接劲、化劲、发劲”完成无 new 构建的过程,对方一下子便飞出去了,还不知道是怎么回事就飞出去了,如果理解不了接化发,需要武德内径图修炼下。

实战,请分析如下四种写法区别:

console.log(new $())
console.log($())
console.log(new $)
----------------------------------------
console.log($)

上面三种写法的结果是一样的,都是 jQuery.fn.init,它的原型上是 jQuery.fn,这里面提供一些常用的 api。其中第三种写法是对手的一个虚劲,new 的时候可以省略括号。

最后一个写法就是返回的 jQuery 函数本身。


extend 插件

jq 中提供了可扩展插件的方式,jQuery.extend = jQuery.fn.extend,这两者虽然是相等操作,但是使用方式是不同的:

1)jQuery.extend(): 把两个或者更多的对象合并到第一个当中,$.xxx 方式使用

2)jQuery.fn.extend():把对象挂载到 jQuery 的 prototype 属性,来扩展一个新的 jQuery 实例方法。通过 $(dom).xxx 使用

原因是这个方法中最终返回的是 this,前者的 this 指向 jq 本身,扩展的是类的静态方法;

后者 this 指向 jq.fn(jQuery.fn = jQuery.prototype),扩展的是对象实例上的方法。

jQuery.extend = jQuery.fn.extend = function() {
    ...
    // 注意点:只有一个参数时,说明是扩展的自身静态方法或者对象的实例方法,否则就是对象的合并
    if ( i === length ) {
        target = this;
        i--;
    }

    ...
    return target;
};

同时这个方法中注意 isPlainObject 的方法实现,有很多版本实现:

var class2type = {};
var hasOwn = class2type.hasOwnProperty;
var fnToString = hasOwn.toString;
var ObjectFunctionString = fnToString.call( Object );

function isPlainObject( obj ) {
    var proto, Ctor;
    // 如果是浏览器原生的一些对象,直接不通过
    if ( !obj || toString.call( obj ) !== "[object Object]" ) {
        return false;
    }
    // 获取 __proto__
    proto = Object.getPrototypeOf( obj );

    // Object.create( null )创建的对象没有原型
    if ( !proto ) {
        return true;
    }

    Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
    // 最后一个判断确保 [object Object] 类型,比如 new function(){} 到这里就不通过
    return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
}

jq 内部也是通过插件的机制扩展各种功能。


链式调用

jq 中可以使用链式调用的方式,因为在 jQuery.fn.extend 扩展中,每个方法最终都会返回 this 自身。同时不同的 api 通过扩展的方式最终都集中到 jQuery.fn.init 的原型上。


变量冲突

在使用 jq 的时候,有些其他的库可能占用了 $ 这个变量,jq 中可以通过一个变更的手法解决这个冲突:

// 其他的库占用这两个名称,缓存记录下,如果没有占用则为 undefined
var _jQuery = window.jQuery,
    _$ = window.$;

// 这个方法执行时 window.jQuery = window.$ = jQuery 这些变量才有值
jQuery.noConflict = function(deep) {
    // 解除 $ 占用
    if (window.$ === jQuery) {
        window.$ = _$;
    }
    // 次参数代表 jQuery 的占用也解除
    if (deep && window.jQuery === jQuery) {
        // 这里的window仅仅是个参数,参考最上面的代码结构,修改它并不会修改jQuery变量
        window.jQuery = _jQuery;
    }
    // 返回本身 被外部变量接收
    return jQuery;
};

if (typeof noGlobal === strundefined) {
    window.jQuery = window.$ = jQuery;
}


数据缓存

数据缓存模块,会被内部大量使用,其实就是 $.data 或者 $(dom).data 的使用,这里面逻辑不复杂,大致代码结构:

jQuery.extend({
    ...
    data: function( elem, name, data ) {
        // 这里的方法来自 data/Data.js 中,逻辑简单,就是 set get delete 操作
        return dataUser.access( elem, name, data );
    },
    ...
});

jQuery.fn.extend( {
    data: function( key, value ) {
        ...
        // 这个方法也是 set 和 get 操作,内部中使用的比较多,封装为一个抽象方法
        // 同时选择多个 calss 帮助批量执行回调逻辑
        // 比如:attr(),prop(),text(),html(),css(),data(),scrollLeft(),scrollTop()
        return access( this, function( value ) {
            ...
            // 这里来自 Data.js 的方法,设置值,取值等操作
            ...
        }, null, value, arguments.length > 1, null, true );
    },
    ...
} );


回调和异步

回调方法 callbacks 就是一个发布订阅。

异步的机制是借鉴的 promise 的东西,jq中的源码可以简单了解下,具体的模拟实现可以直接看 promise原理


页面加载

$(function) 和 $(document).ready() 是一样的效果,调用 DOMContentLoaded 监听,采用 load 兜底处理:

function completed() {
	document.removeEventListener( "DOMContentLoaded", completed, false );
	window.removeEventListener( "load", completed, false );
	jQuery.ready();
}

jQuery.ready.promise = function( obj ) {
    if ( !readyList ) {
        readyList = jQuery.Deferred();
        // Catch cases where $(document).ready() is called after the browser event has already occurred.
	// we once tried to use readyState "interactive" here, but it caused issues like the one
	// discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
	if ( document.readyState === "complete" ) {
	    // Handle it asynchronously to allow scripts the opportunity to delay ready
	    setTimeout( jQuery.ready );
        } else {
	    // Use the handy event callback
	    document.addEventListener( "DOMContentLoaded", completed, false );
	    // A fallback to window.onload, that will always work
	    window.addEventListener( "load", completed, false );
	}
    }
    return readyList.promise( obj );
};

3人赞

分享到: