美文网首页让前端飞
学习 jQuery 源码整体架构,打造属于自己的 js 类库

学习 jQuery 源码整体架构,打造属于自己的 js 类库

作者: 若川i | 来源:发表于2019-07-29 20:49 被阅读14次

    虽然现在基本不怎么使用jQuery了,但jQuery流行10多年JS库,还是有必要学习它的源码的。也可以学着打造属于自己的js类库,求职面试时可以增色不少。

    本文章学习的是v3.4.1版本。
    unpkg.com源码地址:https://unpkg.com/jquery@3.4.1/dist/jquery.js

    jQuery github仓库

    自执行匿名函数

    (function(global, factory){
    
    })(typeof window !== "underfined" ? window: this, function(window, noGlobal){
    
    });
    

    外界访问不到里面的变量和函数,里面可以访问到外界的变量,但里面定义了自己的变量,则不会访问外界的变量。
    匿名函数将代码包裹在里面,防止与其他代码冲突和污染全局环境。
    关于自执行函数不是很了解的读者可以参看这篇文章。
    [译] JavaScript:立即执行函数表达式(IIFE)

    浏览器环境下,最后把$jQuery函数挂载到window上,所以在外界就可以访问到$jQuery了。

    if ( !noGlobal ) {
        window.jQuery = window.$ = jQuery;
    }
    // 其中`noGlobal`参数只有在这里用到。
    

    支持多种环境下使用 比如 commonjs、amd规范

    commonjs 规范支持

    commonjs实现 主要代表 nodejs

    // global是全局变量,factory 是函数
    ( function( global, factory ) {
    
        //  使用严格模式
        "use strict";
        // Commonjs 或者 CommonJS-like  环境
        if ( typeof module === "object" && typeof module.exports === "object" ) {
            // 如果存在global.document 则返回factory(global, true);
            module.exports = global.document ?
                factory( global, true ) :
                function( w ) {
                    if ( !w.document ) {
                        throw new Error( "jQuery requires a window with a document" );
                    }
                    return factory( w );
                };
        } else {
            factory( global );
        }
    
    // Pass this if window is not defined yet
    // 第一个参数判断window,存在返回window,不存在返回this
    } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {});
    

    amd 规范 主要代表 requirejs

    if ( typeof define === "function" && define.amd ) {
        define( "jquery", [], function() {
            return jQuery;
        } );
    }
    

    cmd 规范 主要代表 seajs

    很遗憾,jQuery源码里没有暴露对seajs的支持。但网上也有一些方案。这里就不具体提了。毕竟现在基本不用seajs了。

    无 new 构造

    实际上也是可以 new的,因为jQuery是函数。而且和不用new效果是一样的。
    new显示返回对象,所以和直接调用jQuery函数作用效果是一样的。
    如果对new操作符具体做了什么不明白。可以参看我之前写的文章。

    面试官问:能否模拟实现JS的new操作符

    源码:

     var
        version = "3.4.1",
    
        // Define a local copy of jQuery
        jQuery = function( selector, context ) {
            // 返回new之后的对象
            return new jQuery.fn.init( selector, context );
        };
    jQuery.fn = jQuery.prototype = {
        // jQuery当前版本
        jquery: version,
        // 修正构造器为jQuery
        constructor: jQuery,
        length: 0,
    };
    init = jQuery.fn.init = function( selector, context, root ) {
        // ...
        if ( !selector ) {
            return this;
        }
        // ...
    };
    init.prototype = jQuery.fn;
    
    jQuery.fn === jQuery.prototype;     // true
    init = jQuery.fn.init;
    init.prototype = jQuery.fn;
    // 也就是
    jQuery.fn.init.prototype === jQuery.fn;  // true
    jQuery.fn.init.prototype === jQuery.prototype;  // true
    

    关于这个笔者画了一张jQuery原型关系图,所谓一图胜千言。

    jQuery-v3.4.1原型关系图.png
    <sciprt src="https://unpkg.com/jquery@3.4.1/dist/jquery.js">
    </script>
    console.log({jQuery});
    // 在谷歌浏览器控制台,可以看到jQuery函数下挂载了很多静态属性和方法,在jQuery.fn 上也挂着很多属性和方法。
    

    Vue源码中,也跟jQuery类似,执行的是Vue.prototype._init方法。

    function Vue (options) {
        if (!(this instanceof Vue)
        ) {
            warn('Vue is a constructor and should be called with the `new` keyword');
        }
        this._init(options);
    }
    initMixin(Vue);
    function initMixin (Vue) {
        Vue.prototype._init = function (options) {};
    };
    

    核心函数之一 extend

    用法:

    jQuery.extend( target [, object1 ] [, objectN ] )        Returns: Object
    
    jQuery.extend( [deep ], target, object1 [, objectN ] )
    

    jQuery.extend API
    jQuery.fn.extend API

    看几个例子:
    (例子可以我放到在线编辑代码的jQuery.extend例子codepen了,可以直接运行)。

    // 1. jQuery.extend( target)
    var result1 = $.extend({
        job: '前端开发工程师',
    });
    
    console.log(result1, 'result1', result1.job); // $函数 加了一个属性 job  // 前端开发工程师
    
    // 2. jQuery.extend( target, object1)
    var result2 = $.extend({
        name: '若川',
    },
    {
        job: '前端开发工程师',
    });
    
    console.log(result2, 'result2'); // { name: '若川', job: '前端开发工程师' }
    
    // deep 深拷贝
    // 3. jQuery.extend( [deep ], target, object1 [, objectN ] )
    var result3 = $.extend(true,  {
        name: '若川',
        other: {
            mac: 0,
            ubuntu: 1,
            windows: 1,
        },
    }, {
        job: '前端开发工程师',
        other: {
            mac: 1,
            linux: 1,
            windows: 0,
        }
    });
    console.log(result3, 'result3');
    // deep true
    // {
    //     "name": "若川",
    //     "other": {
    //         "mac": 1,
    //         "ubuntu": 1,
    //         "windows": 0,
    //         "linux": 1
    //     },
    //     "job": "前端开发工程师"
    // }
    // deep false
    // {
    //     "name": "若川",
    //     "other": {
    //         "mac": 1,
    //         "linux": 1,
    //         "windows": 0
    //     },
    //     "job": "前端开发工程师"
    // }
    

    结论:extend函数既可以实现给jQuery函数可以实现浅拷贝、也可以实现深拷贝。可以给jQuery上添加静态方法和属性,也可以像jQuery.fn(也就是jQuery.prototype)上添加属性和方法,这个功能归功于thisjQuery.extend调用时this指向是jQueryjQuery.fn.extend调用时this指向则是jQuery.fn

    浅拷贝实现

    知道这些,其实实现浅拷贝还是比较容易的:

    // 浅拷贝实现
    jQuery.extend = function(){
        // options 是扩展的对象object1,object2...
        var options,
        // object对象上的键
        name,
        // copy object对象上的值,也就是是需要拷贝的值
        copy,
        // 扩展目标对象,可能不是对象,所以或空对象
        target = arguments[0] || {},
        // 定义i为1
        i = 1,
        // 定义实参个数length
        length = arguments.length;
        // 只有一个参数时
        if(i === length){
            target = this;
            i--;
        }
        for(; i < length; i++){
            // 不是underfined 也不是null
            if((options = arguments[i]) !=  null){
                for(name in options){
                    copy = options[name];
                    // 防止死循环,continue 跳出当前此次循环
                    if ( name === "__proto__" || target === copy ) {
                        continue;
                    }
                    if ( copy !== undefined ) {
                        target[ name ] = copy;
                    }
                }
            }
    
        }
        // 最后返回目标对象
        return target;
    }
    

    深拷贝则主要是在以下这段代码做判断。可能是数组和对象引用类型的值,做判断。

    if ( copy !== undefined ) {
        target[ name ] = copy;
    }
    

    为了方便读者调试,代码同样放在jQuery.extend浅拷贝代码实现codepen,可在线运行。

    深拷贝实现

    $.extend = function(){
        // options 是扩展的对象object1,object2...
        var options,
        // object对象上的键
        name,
        // copy object对象上的值,也就是是需要拷贝的值
        copy,
        // 深拷贝新增的四个变量 deep、src、copyIsArray、clone
        deep = false,
        // 源目标,需要往上面赋值的
        src,
        // 需要拷贝的值的类型是函数
        copyIsArray,
        //
        clone,
        // 扩展目标对象,可能不是对象,所以或空对象
        target = arguments[0] || {},
        // 定义i为1
        i = 1,
        // 定义实参个数length
        length = arguments.length;
    
        // 处理深拷贝情况
        if ( typeof target === "boolean" ) {
            deep = target;
    
            // Skip the boolean and the target
            // target目标对象开始后移
            target = arguments[ i ] || {};
            i++;
        }
    
        // Handle case when target is a string or something (possible in deep copy)
        // target不等于对象,且target不是函数的情况下,强制将其赋值为空对象。
        if ( typeof target !== "object" && !isFunction( target ) ) {
            target = {};
        }
    
        // 只有一个参数时
        if(i === length){
            target = this;
            i--;
        }
        for(; i < length; i++){
            // 不是underfined 也不是null
            if((options = arguments[i]) !=  null){
                for(name in options){
                    copy = options[name];
                    // 防止死循环,continue 跳出当前此次循环
                    if ( name === "__proto__" || target === copy ) {
                        continue;
                    }
    
                    // Recurse if we're merging plain objects or arrays
                    // 这里deep为true,并且需要拷贝的值有值,并且是纯粹的对象
                    // 或者需拷贝的值是数组
                    if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
                        ( copyIsArray = Array.isArray( copy ) ) ) ) {
    
                        // 源目标,需要往上面赋值的
                        src = target[ name ];
    
                        // Ensure proper type for the source value
                        // 拷贝的值,并且src不是数组,clone对象改为空数组。
                        if ( copyIsArray && !Array.isArray( src ) ) {
                            clone = [];
                            // 拷贝的值不是数组,对象不是纯粹的对象。
                        } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {
                            // clone 赋值为空对象
                            clone = {};
                        } else {
                            // 否则 clone = src
                            clone = src;
                        }
                        // 把下一次循环时,copyIsArray 需要重新赋值为false
                        copyIsArray = false;
    
                        // Never move original objects, clone them
                        // 递归调用自己
                        target[ name ] = jQuery.extend( deep, clone, copy );
    
                    // Don't bring in undefined values
                    }
                    else if ( copy !== undefined ) {
                        target[ name ] = copy;
                    }
                }
            }
    
        }
        // 最后返回目标对象
        return target;
    };
    

    为了方便读者调试,这段代码同样放在jQuery.extend深拷贝代码实现codepen,可在线运行。

    深拷贝衍生的函数 isFunction

    判断参数是否是函数。

    var isFunction = function isFunction( obj ) {
    
        // Support: Chrome <=57, Firefox <=52
        // In some browsers, typeof returns "function" for HTML <object> elements
        // (i.e., `typeof document.createElement( "object" ) === "function"`).
        // We don't want to classify *any* DOM node as a function.
        return typeof obj === "function" && typeof obj.nodeType !== "number";
    };
    

    深拷贝衍生的函数 jQuery.isPlainObject

    jQuery.isPlainObject(obj)
    测试对象是否是纯粹的对象(通过 "{}" 或者 "new Object" 创建的)。

    jQuery.isPlainObject({}) // true
    jQuery.isPlainObject("test") // false
    
    var getProto = Object.getPrototypeOf;
    var class2type = {};
    var toString = class2type.toString;
    var hasOwn = class2type.hasOwnProperty;
    var fnToString = hasOwn.toString;
    var ObjectFunctionString = fnToString.call( Object );
    
    jQuery.extend( {
        isPlainObject: function( obj ) {
            var proto, Ctor;
    
            // Detect obvious negatives
            // Use toString instead of jQuery.type to catch host objects
            // !obj 为true或者 不为[object Object]
            // 直接返回false
            if ( !obj || toString.call( obj ) !== "[object Object]" ) {
                return false;
            }
    
            proto = getProto( obj );
    
            // Objects with no prototype (e.g., `Object.create( null )`) are plain
            // 原型不存在 比如 Object.create(null) 直接返回 true;
            if ( !proto ) {
                return true;
            }
    
            // Objects with prototype are plain iff they were constructed by a global Object function
            Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
            // 构造器是函数,并且 fnToString.call( Ctor )  === fnToString.call( Object );
            return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
        },
    });
    

    extend函数,也可以自己删掉写一写,算是jQuery中一个比较核心的函数了。而且用途广泛,可以内部使用也可以,外部使用扩展 插件等。

    链式调用

    jQuery能够链式调用是因为一些函数执行结束后 return this
    比如
    jQuery 源码中的addClassremoveClasstoggleClass

    jQuery.fn.extend({
        addClass: function(){
            // ...
            return this;
        },
        removeClass: function(){
            // ...
            return this;
        },
        toggleClass: function(){
            // ...
            return this;
        },
    });
    

    jQuery.noConflict 很多js库都会有的防冲突函数

    jQuery.noConflict API

    用法:

     <script>
        var $ = '我是其他的$,jQuery不要覆盖我';
    </script>
    <script src="./jquery-3.4.1.js">
    </script>
    <script>
        $.noConflict();
        console.log($); // 我是其他的$,jQuery不要覆盖我
    </script>
    

    jQuery.noConflict 源码

    var
    
        // Map over jQuery in case of overwrite
        _jQuery = window.jQuery,
    
        // Map over the $ in case of overwrite
        _$ = window.$;
    
    jQuery.noConflict = function( deep ) {
        // 如果已经存在$ === jQuery;
        // 把已存在的_$赋值给window.$;
        if ( window.$ === jQuery ) {
            window.$ = _$;
        }
    
        // 如果deep为 true, 并且已经存在jQuery === jQuery;
        // 把已存在的_jQuery赋值给window.jQuery;
        if ( deep && window.jQuery === jQuery ) {
            window.jQuery = _jQuery;
        }
    
        // 最后返回jQuery
        return jQuery;
    };
    

    总结

    全文主要通过浅析了jQuery整体结构,自执行匿名函数、无new构造、支持多种规范(如commonjs、amd规范)、核心函数之extend、链式调用、jQuery.noConflict等方面。

    重新梳理下文中学习的源码结构。

    // 源码结构
    ( function( global, factory )
        "use strict";
        if ( typeof module === "object" && typeof module.exports === "object" ) {
            module.exports = global.document ?
                factory( global, true ) :
                function( w ) {
                    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 ) {
        var version = "3.4.1",
    
            // Define a local copy of jQuery
            jQuery = function( selector, context ) {
                return new jQuery.fn.init( selector, context );
            };
    
        jQuery.fn = jQuery.prototype = {
            jquery: version,
            constructor: jQuery,
            length: 0,
            // ...
        };
    
        jQuery.extend = jQuery.fn.extend = function() {};
    
        jQuery.extend( {
            // ...
            isPlainObject: function( obj ) {},
            // ...
        });
    
        init = jQuery.fn.init = function( selector, context, root ) {};
    
        init.prototype = jQuery.fn;
    
        if ( typeof define === "function" && define.amd ) {
            define( "jquery", [], function() {
                return jQuery;
            } );
        }
        jQuery.noConflict = function( deep ) {};
    
        if ( !noGlobal ) {
            window.jQuery = window.$ = jQuery;
        }
    
        return jQuery;
    });
    

    可以学习到jQuery巧妙的设计和架构,为自己所用,打造属于自己的js类库。
    相关代码和资源防止github blog中,需要的读者可以自取。

    下一篇文章可能是学习underscorejs的源码整体架构。

    读者发现有不妥或可改善之处,欢迎评论指出。另外觉得写得不错,可以点赞、评论、转发,也是对笔者的一种支持。

    笔者往期文章

    面试官问:JS的继承

    面试官问:JS的this指向

    面试官问:能否模拟实现JS的call和apply方法

    面试官问:能否模拟实现JS的bind方法

    面试官问:能否模拟实现JS的new操作符

    前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

    扩展阅读

    chokcoco: jQuery- v1.10.2 源码解读

    chokcoco:【深入浅出jQuery】源码浅析--整体架构

    songjz :jQuery 源码系列(一)总体架构

    关于

    作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。

    github blog,相关源码和资源都放在这里,求个star_~

    微信交流群

    加微信 lxchuan12,备注写明来源。拉您进微信群【前端视野交流群】。

    相关文章

      网友评论

        本文标题:学习 jQuery 源码整体架构,打造属于自己的 js 类库

        本文链接:https://www.haomeiwen.com/subject/nvqprctx.html