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

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

这次对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;
    };
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享