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

24次阅读

共计 9437 个字符,预计需要花费 24 分钟才能阅读完成。

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

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

jQuery github仓库

自执行匿名函数

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

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

浏览器环境下,最后把 $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 原型关系图,所谓一图胜千言。

<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 爱好者 | 所知甚少,唯善学。

个人博客

segmentfault前端视野专栏,开通了 前端视野 专栏,欢迎关注~

掘金专栏,欢迎关注~

知乎前端视野专栏,开通了 前端视野 专栏,欢迎关注~

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

正文完
 0