阅读 329

jQuery源码阅读系列——核心结构分析&手写each/extend方法

前几篇文章记录了导出问题和其中一点的数据类型判断的封装问题。

这次对jQuery核心架构简单分析,以此学习面向对象和插件封装的知识。

使用其他原型上的方法

我们想使用其他类原型上的方法,例如数组 push ,有两种方式,:

  • 类似 [].push.call(实例) ,使用 call 改变内部 this 指向
  • 或者 jQuery.prototype.push = arr.push

jQuery就是这样,取到数组原型上的对应方法,然后放到自己的原型上,这样就可以使用别的原型上提供的公用方法。

这样实例通过原型链找到所属类原型上为其提供的公共属性和方法的时候,就可以使用到别的类的方法,我们把这种借用的方式称作鸭子类型,如下:


(function () {
    "use strict";
    var arr = [];
    var slice = arr.slice;//取到数组原型上的对应方法
    var push = arr.push;
    var indexOf = arr.indexOf;

    // =======
    var version = "3.5.1",
        jQuery = function jQuery(selector, context) {
            return new jQuery.fn.init(selector, context);
        };
    jQuery.fn = jQuery.prototype = {
        constructor: jQuery,//原型重定向,防止没有constructor
        jquery: version,
        length: 0,
        // 鸭子类型把数组原型上的这些方法放到jQuery的原型上,这样就可以使用别的原型上提供的方法,
        //类似[].slice.call(实例),或者像下面这种方法jQuery.push = arr.push ,这样也可以用
        push: push,
        sort: arr.sort,
        splice: arr.splice,
       
    window.jQuery = window.$ = jQuery;
})();
复制代码

面向对象

jQuery在使用的时候,基本都是 $('.box) 这样使用,那么他是如何将构造函数当做普通函数执行,却又返回了其实例对象的呢?我们来分析一下

代码如下:

(function () {
    "use strict";
    var version = "3.5.1",
        jQuery = function jQuery(selector, context) {
            return new jQuery.fn.init(selector, context);
        };
    jQuery.fn = jQuery.prototype = {
        constructor: jQuery,
        jquery: version,
        length: 0,
    };
    
    var init = jQuery.fn.init = function init(selector, context, root) {
        //...
        };
    init.prototype = jQuery.fn;
    //...
    window.jQuery = window.$ = jQuery;
})();

复制代码

执行 $([selector]) 方法,过程如下

  1. 相当于执行 new jQuery.fn.init(selector, context); ,返回的是 init 方法(类)的实例,假设这个实例是 A
  2. 那么: 可以得出 A.__proto__===init.prototype
  3. 又由代码可以得知 init.prototype => jQuery.fn => jQuery.prototype ,让 init 的原型重定向为jQuery的原型,所以最终执行 new init 相当于执行了 new jQuery
  4. 所以 A.__proto__===jQuery.prototype

总结:基于JQ选择器 $(...)jQuery(...) 获取的是 jQuery 类的实例

  • 目的:让用户使用的时候把 jQuery / $ 当做普通函数执行,但是最后的结果是创造其类的一个实例,用户使用起来方便
  • 这种处理模式的思想其实就是工厂设计模式

image。png

有一个面试题:不使用 new 操作符,是否可以创造当前函数的实例?上面就是例子,核心原理就是使用 jQuery.fn 做一个原型的中转

额外补充另外一种不基于 new 执行函数,也可以创在构造函数实例的情况:

function* fn() {}
fn.prototype.x = 100;
let f = fn();
// f.__proto__===fn.prototype  f也是fn的一个实例(这也是另外一种不基于new执行函数,也可以创在构造函数实例的情况) 
复制代码

init 构造函数中的逻辑

函数 init 传入三个参数,其中 selector 可以传入不同的参数种类,我们分类讨论

jQuery.fn.init 中传入了 selectorcontext 两个参数,对应着jQuery的用法

  • $('.box') 在整个文档中找到拥有 'box' 样式类的
  • $('#commHead .box') 在ID为 'commHead' 的容器中,找到拥有box样式类的(后代查找)
  • $('.box',document.getElementById('commHead')) 和上面一个意思

如果传入的是DOM

(function () {
    "use strict";
    //...

    var version = "3.5.1",
        jQuery = function jQuery(selector, context) {
            return new jQuery.fn.init(selector, context);
        };
    jQuery.fn = jQuery.prototype = {
       //...
        length: 0,
    };
    var rootjQuery = jQuery(document),
        init = jQuery.fn.init = function init(selector, context, root) {
            var match, elem;
            // 处理: $(""), $(null), $(undefined), $(false)  返回空的JQ对象
            if (!selector) return this;
            root = root || rootjQuery;
            if (typeof selector === "string") {
               //...
            } else if (selector.nodeType) {
                // 传递的是一个原生的DOM/JS对象:把DOM对象转换为JQ对象“这样就可以使用JQ原型上提供的方法”
                this[0] = selector;
                this.length = 1;
                return this;
            } else if (isFunction(selector)) {
             //...
            }
        };
    init.prototype = jQuery.fn;

    window.jQuery = window.$ = jQuery;
})();
复制代码
  1. if (!selector) return this; 处理为: $("")$(null)$(undefined)$(false) 返回空的JQ对象,仅有 __proto__ 上有公共方法

  2. typeof selector === "string" 是一种关于传入字符串选择器的处理,这里逻辑比较复杂,先跳过这一块

  3. selector.nodeType 处理:当传入的是DOM对象时,把DOM对象放到属性 [0] 上,相当于在DOM对象上包裹一层,并返回一个jQ对象的伪数组,每一个数字属性,就是一个DOM对象。

    image。png

这里区分一下“JQ对象” 和 “DOM对象” “JQ对象” :JQ实例对象“也就是基于选择器获取的结果” ,一般返回的是一个类数组集合,拥有索引和 length 等属性, “DOM/JS对象”: 基于浏览器内置的方法获取的元素对象“他们是浏览器内置类的相关实例对象”

  • “DOM对象”转化为“JQ对象” : $(“DOM对象”)
  • “JQ对象”获取“DOM对象” : 使用 JQ对象[索引]JQ对象.get(索引) 使用内置类原型上的方法

原型上方法 get VS eq

get 是拿到其DOM对象, eq 返回的是jQuery对象

get 原理是,如果什么都不传,先把“JQ对象”类数组集合转化为数组集合( slice.call(this) ),然后如果传了索引就根据索引获取其DOM值,具体逻辑可以看代码的注视

eq 也是基于索引查找JQ对象集合中的某一项,但是最后返回的不是“DOM对象”,而是一个新的“JQ对象”具体如下,一点点分析

  1. 先执行 eq() ,支持负数索引
  2. eq返回 pushStack() 的执行结果,这个执行结果是空的JQ对象,并把 eq() 函数传入的索引的那个DOM对象作为0属性,相当于重新返回一个新的jq对象,仅包含eq的索引那个DOM值
  3. pushStack() 具体实现是基于 merge 方法,拼接伪数组,将空的JQ对象( length 为0)和长度为1的DOM对象组合在一起然后返回
  4. merge 要注意的是,他传递两个集合,把第二个集合中的每一项全部放置到第一个集合的末尾,既合并两个集合,返回的是第一个集合。类似于数组的 concat ,但是 concat 只能数组使用, merge 方法可以支持类数组集合的处理
  5. 注意 pushStack 还返回了 prevObject 属性,代码中可以看到,这是使用eq后,原来的根JQ对象,在链式调用中,可以快速回到原始操作的JQ对象。例如 $('body').prev().addClass('xxx').prevObject.addClass('clearfix')
(function () {
    "use strict";
    var arr = [];
    var slice = arr.slice;//取到数组原型上的对应方法
    var push = arr.push;

    // =======
    var version = "3.5.1",
        jQuery = function jQuery(selector, context) {
            return new jQuery.fn.init(selector, context);
        };
    jQuery.fn = jQuery.prototype = {
        constructor: jQuery,
        jquery: version,
        length: 0,
        push: push,
        // 基于索引把“JQ对象”转换为“DOM对象”
        get: function (num) {
            // 把“JQ对象”类数组集合转化为数组集合
            if (num == null) return slice.call(this);
            // 支持负数作为索引
            return num < 0 ? this[num + this.length] : this[num];
        },
        // 也是基于索引查找JQ对象集合中的某一项,但是最后返回的不是“DOM对象”,而是一个新的“JQ对象”
        eq: function (i) {
            // 支持负数作为索引
            var len = this.length,
                j = +i + (i < 0 ? len : 0);
            return this.pushStack(j >= 0 && j < len ? [this[j]] : []);
        },
        pushStack: function (elems) {
            // this.constructor->jQuery => jQuery() => 空的JQ对象
            var ret = jQuery.merge(this.constructor(), elems);
            // prevObject:在链式调用中,可以快速回到原始操作的JQ对象(根JQ对象)
            ret.prevObject = this;
            return ret;
        },
    };
    
    // 传递两个集合,把第二个集合中的每一项全部放置到第一个集合的末尾“合并两个集合”,返回的是第一个集合
    //   + 类似于数组的concat,但是这个只能数组使用
    //   + merge方法可以支持类数组集合的处理
    jQuery.merge = function merge(first, second) {
        var len = +second.length,
            j = 0,
            i = first.length;
        for (; j < len; j++) {
            first[i++] = second[j];
        }
        first.length = i;
        return first;
    };

    // =======
      var  init = jQuery.fn.init = function init(selector, context, root) {
            var match, elem;
            // 处理: $(""), $(null), $(undefined), $(false)  返回空的JQ对象
            if (!selector) return this;
            if (typeof selector === "string") {
               /...
            } else if (selector.nodeType) {
                // 传递的是一个原生的DOM/JS对象:把DOM对象转换为JQ对象“这样就可以使用JQ原型上提供的方法”
                this[0] = selector;
                this.length = 1;
                return this;
            } else if (isFunction(selector)) {
              //...
            }
            //...
        };
    init.prototype = jQuery.fn;
    window.jQuery = window.$ = jQuery;
})();
复制代码

image。png

如果传入的是函数

如果传入函数,即 $(function (){}) ,通过以下代码可以看出

var rootjQuery = jQuery(document) => root = root || rootjQuery; 再到下面返回值

return root.ready !== undefined ?
                    root.ready(selector) :
                    selector(jQuery);
复制代码

$(function (){}) 相当于执行了 $(document).ready(function (){})

ready 函数操作为: readyList 返回一个 promiseresolve 之后,才执行传进去的函数

readyList 做了什么呢?因为这部分代码过于分散和跳跃,就不粘贴了,说一下原理:

readyList 里,监听 'DOMContentLoaded' 事件,在此事件触发时, resolve promise。

'DOMContentLoaded' 是等待页面中的DOM结构全部都加载完成,就会会触发的事件,触发后会执行回调函数

注意区别与 window.addEventListener('load',function(){})load 事件指的是等待页面中的所有资源都加载完成,含DOM结构加载完成 与其他资源加载完成。

上面的原理正对应着我们日常使用 $(function (){})$(document).ready(function (){})

(function () {
    "use strict";
    var version = "3.5.1",
        jQuery = function jQuery(selector, context) {
            return new jQuery.fn.init(selector, context);
        };
    jQuery.fn = jQuery.prototype = {};
    var rootjQuery = jQuery(document),
        init = jQuery.fn.init = function init(selector, context, root) {
            var match, elem;
            if (!selector) return this;
            root = root || rootjQuery;
            if (typeof selector === "string") {
              //...
            } else if (selector.nodeType) {
              //...
            } else if (isFunction(selector)) {
                return root.ready !== undefined ?
                    root.ready(selector) :
                    selector(jQuery);
            }
            //...
        };
    init.prototype = jQuery.fn;
    // =======
    var readyList = jQuery.Deferred();
    jQuery.fn.ready = function (fn) {
        readyList
            .then(fn)
            .catch(function (error) {
                jQuery.readyException(error);
            });
        return this;
    };
    window.jQuery = window.$ = jQuery;
})();
复制代码

如果传入的是字符串

根据判断,分为 $('.box') 选择器类型和 $('<div>xxx</div>') html字符串类型处理,然后根据不同逻辑,使用不同的正则分支判断分别去处理,这里代码过多,就不仔细分析了

如果传入其他

例如传入一个数组

image。png 我们可以看到jQ将数组的每一项变为了伪数组的

(function () {
    "use strict";
    var push = arr.push;
    var version = "3.5.1",
        jQuery = function jQuery(selector, context) { return new jQuery.fn.init(selector, context);};
    jQuery.fn = jQuery.prototype = {
        jquery: version,
        length: 0,
        push: push
    };
    jQuery.merge = function merge(first, second) {
        var len = +second.length,
            j = 0,
            i = first.length;
        for (; j < len; j++) {
            first[i++] = second[j];
        }
        first.length = i;
        return first;
    };
    var init = jQuery.fn.init = function init(selector, context, root) {
            var match, elem;
            if (!selector) return this;
            root = root || rootjQuery;
            if (typeof selector === "string") {...} 
            else if (selector.nodeType) {...} 
            else if (isFunction(selector)) {...}
            return jQuery.makeArray(selector, this);
        };
    init.prototype = jQuery.fn;
    jQuery.makeArray = function makeArray(arr, results) {
        var ret = results || [];
        if (arr != null) {
            if (isArrayLike(Object(arr))) {
                jQuery.merge(ret,
                    typeof arr === "string" ? [arr] : arr
                );
            } else {
                push.call(ret, arr);
            }
        }
        return ret;
    };
    window.jQuery = window.$ = jQuery;
})();
复制代码

我们可以看到 return jQuery.makeArray(selector, this); ,如果传入数组,jquery对象,就经过 isArrayLike 的检测,然后使用 merge 将空的jQ对象和数组合并。如果是其他类型的值,就调用 push ,将其放在jQ对象的伪数组的第一个,然后最终返回的还是jQuery对象

手写更强的each方法

jquery中的 each 方法可以遍历数组/类数组/对象,我们在它源码的基础上再进行更高要求的封装:

要求:支持回调函数返回值处理:传入的回调函数返回 false 则结束循环。 这是内置方法 forEach / map 不具备的

在其中加入传入参数检测的逻辑和结束循环的逻辑

// 遍历数组/类数组/对象「支持回调函数返回值处理:返回false则结束循环,这是内置方法forEach/map不具备的」
    var each = function each(obj, callback) {
        //'Function.prototype'返回一个匿名空函数,什么都不做,为了兼容下面传入的不是函数的情况
        typeof callback !== "function" ? callback = Function.prototype : null;
        var length,
            i = 0,
            keys = [];
        if (isArrayLike(obj)) {
            // 数组或者类数组
            length = obj.length;
            for (; i < length; i++) {
                var item = obj[i],
                //让其中的this指向元素本身,(和forEach一样)
                    result = callback.call(item, item, i);
                    //回调函数返回false,结合素循环
                if (result === false) break
            }
        } else {
            // 对象
            //为了避免for in循环的问题,我们这里用keys+循环来遍历对象
            keys = Object.keys(obj);
            typeof Symbol !== "undefined" ? keys = keys.concat(Object.getOwnPropertySymbols(obj)) : null;//包含Symbol属性
            i = 0;
            length = keys.length;
            for (; i < length; i++) {
                var key = keys[i],
                    value = obj[key];
                    //这样既执行了,又返回了结果
                if (callback.call(value, value, key) === false) break;
            }
        }
        return obj;
    };
复制代码

$.extend/$.fn.extend

关于对象的深浅合并

对象的浅合并:只把对象第一级的内容进行合并

对象的深合并:如果第一级内容以后也是对象,那个继续对下一级的对象进行合并,而不是完全地属性覆盖

浅合并

Object.assign()

Object.assign() 可以实现对象的浅合并

let obj1 = {
    url: '/api/list',
    method: 'GET',
    headers: {
        'Content-Type': 'application/json'
    },
    cache: false
};

let obj2 = {
    params: {
        lx: 0
    },
    method: 'POST',
    headers: {
        'X-Token': 'AFED4567FG'
    }
};
console.log(Object.assign(obj1, obj2));
复制代码

Object.assign() 浅合并:把 obj2 合并到 obj1 中,让 obj2 中的内容替换 obj1 中的内容,最后返回 obj1obj1 会发生改变)

image。png

如果想返回新的对象,可以这样处理,这样 obj1obj2 都没有改变:

console.log(Object.assign({}, obj1, obj2));
复制代码

深合并

可以看到上面 headers 属性直接被覆盖,那我们如何进行对象的深合并呢? 可以看一下JjQuery的 extend 方法,他就可以进行对象的深合并。

先来看一下 extend 方法的用法,它有几种不同的用法

$.extend 方法的用法

把用户自定义的方法扩展到 jQuery / jQuery.fn
  1. 把jQuery当做对象,扩展到其上面的私有的属性和方法(完善类库)

         $.extend({
                xxx: function () {
                    console.log('xxx');
                }
            });
            $.xxx();
    复制代码
  2. 向其原型上扩展属性和方法,供其实例“基于JQ选择器获取的结果”调取使用,写JQ插件使用方法

    $.fn.extend({
        xxx() {
            console.log('xxx');
        }
    });
    $('.box').xxx(); 
    复制代码

注意:当仅有一个对象时, $.extend(true,{...}) 这种写法,不管第一个属性是否是传了 true 或者没传,都是将对象扩展到jquery上

实现两个及多个对象的合并

包括浅比较进行浅合并,深比较进行深度合并。

  1. 浅合并:类似于 Object.assign$.extend( obj1, obj2) ,将两个对象浅合并到第一个对象,注意第一个对象会改变并且返回第一个对象,

  2. 深合并: $.extend(true, {}, obj1, obj2) ,第一个参数一定要是 true ,后面对象多于两个。那就是深合并,会逐层得将对象合并

源码分析

下面逐行解析其源码

jQuery.extend = jQuery.fn.extend = function () {
    var options, name, src, copy, copyIsArray, clone,
        target = arguments[0] || {},//第一个值没传或者传的是false,就让target变为空对象
        i = 1,
        length = arguments.length,//存储传了多少个值
        deep = false;
    //如果第一个是布尔类型的值,说明想要深合并.(这里一定是true,因为传入false的话,target上面已经被赋值为{})
    if (typeof target === "boolean") {
        deep = target;//deep赋值为 true
        target = arguments[i] || {};//target变为第一个传进来的对象
        i++;//i为2
        // deep->布尔,是否想要深合并.走到这里的话,值一定为 true
        // target->传递的第一个对象(如果没有deep,就是第一个参数,有deep就是第二个参数)
        // i->对应的是第二个传递进来对象的索引「可能不存在」
    }
    //进行类型判断,如果传进来的不可进行属性操作,就让target直接赋值为空对象,确保target是一个对象
    if (typeof target !== "object" && !isFunction(target)) target = {};
    // 如果没有传递第二个对象:只是想把第一个传递对象中的内容合并到$/$.fn上,对应第一种使用方法
    if (i === length) {//代表没有传递第二个对象,只传递了一个对象(不管传没传deep),这样就把传过来的对象和$/$.fn进行合并
        target = this; //-> target=$/$.fn
        i--; //->此时i索引对应的是传递的那个对象
    }

    //以上逻辑就做了这几件事
    // 1. 将target换为传递过来的第一个对象,或者$/$.fn
    // 2. i变为要开始合并的第一个对象的索引,用来开始按顺序和target进行合并

    // target代表最终要被替换的对象,最后返回的是target,
    // 接下来的循环就是拿到剩余传递的对象「可能是一个也可能是多个」,拿他们依次替换target
    for (; i < length; i++) {
        // options:每一轮循环拿到的剩余的对象的其中一个替换target的对象
        options = arguments[i];
        if (options == null) continue;
        for (name in options) {
            // copy获取对象中的每一项(我们把这些项替换target中同名这一项)
            copy = options[name];
            copyIsArray = Array.isArray(copy);
            // 防止对象套娃,会形成死递归(防止循环引用),即自己引用自己的上一层
            if (target === copy) continue;
            if (deep && copy && (jQuery.isPlainObject(copy) || copyIsArray)) {
                // 深合并 options中的这一项需要是纯粹的对象或者是数组,我们才有必要和target中对应这一项进行深度对比,从而实现深度合并,否则直接用options中的这一项替换target中这一项即可
                //   options中的这一项copy  对象/数组
                //   src代表的是target中的这一项「clone」
                src = target[name];
                //下面是让传进来的对象和被替换的对象,类型保持一致
                if (copyIsArray && !Array.isArray(src)) {
                    // 如果copy是一个数组,但是src不是,clone是一个数组
                    clone = [];
                } else if (!copyIsArray && !jQuery.isPlainObject(src)) {//copy只能是数组或者纯粹对象
                    // 如果copy是一个纯粹对象,但是src不是,让clone等于空对象
                    clone = {};
                } else {
                    clone = src;
                }
                copyIsArray = false;
                // 基于递归的方式实现当前项的深度比较和深度合并
                target[name] = jQuery.extend(deep, clone, copy);
            } else if (copy !== undefined) {
                // 浅合并
                target[name] = copy;
            }
        }
    }
    return target;
};
复制代码

注意遇到循环引用需要特殊处理 image。png

文章分类
前端
文章标签