JS库的封装之四:代码整合

参考jQuery,使用JavaScript实现库的封装。本文为第四部分:代码整合。

一、背景

  在之前,我们模仿jQuery源码的实现来尝试自己封装一个JS库,最终完成了入口函数、工具类方法/DOM操作方法、jQuery的事件绑定。
  现在我们需要将之前写的代码进行分类,将每一块功能拆分到不同的文件,类似于以模块为单位,最终可以通过webpack等工具将多个js文件合并成一个js文件。
  在我们未将它们合并之前,如果要调用,需要依次手动引入(注意先后顺序,先引入jquery.core.js、再引入jquery.util.js,最后引入其它的文件)。
  文件分类后的结构如下图所示:

    .
    ├── js
    |   ├── jquery.core.js    // jquery框架基本结构
    |   ├── jquery.util.js    // jquery的工具类方法
    |   ├── jquery.style.js    // jquery的样式操作方法
    |   ├── jquery.event.js    // jquery的事件框架
    |

  下面是文件内的具体代码实现。

二、jquery.core.js

  jquery框架基本结构。

(function (global) {

    function jQuery(selector) {
        return new jQuery.fn.init(selector);
    }

    jQuery.fn = jQuery.prototype = {
        constructor: jQuery,

        init: function (selector) {
            if (jQuery.type(selector) === "string") {  // 选择器
                const elements = document.querySelectorAll(selector)
                for (let i = 0; i < elements.length; i++) {
                    this[i] = elements[i];
                }
                this.length = elements.length;
            } else if (selector.nodeType) {  // DOM元素
                this[0] = selector;
                this.length = 1;
            }
        }
    }

    jQuery.fn.init.prototype = jQuery.fn;


    jQuery.fn.extend = jQuery.extend = function (...args) {
        // 接收数据的对象
        let target;
        // 提供数据的对象
        let sources = [];

        // 参数个数为1:
        if (args.length === 1) {
            target = this;
            sources.push(args[0]);
        } else {
            // 参数个数>1:
            target = args[0];
            sources.push(...args);
            sources.splice(0, 1);
        }

        // 完成拷贝的逻辑
        sources.forEach(function (source) {
            // 获取对象中的每一个属性:
            Object.keys(source).forEach(function (key) {
                target[key] = source[key];
            })
        });

        // 告知用户拷贝的结果
        return target;
    }

    global.$ = global.jQuery = jQuery;
})(window)

  关于 $.extend$.fn.extend 两个方法,比较重要,这里再详细讲下。

1. $.extend

  ① 如果有一个参数,把参数对象里面的属性依次拷贝给$

    $.extend({ name:"abc",age:18 })
    // --> $.name="abc"
    // --> $.age=18

  ② 如果有多个参数,把第二个参数及其后面的所有参数中的属性依次遍历给第一个参数

    var p={}
    $.extend(p,{a:10},{b:20},{c:30})
    // --> p.a=10;
    // --> p.b=20;
    // --> p.c=30

2. $.fn.extend

  ① 如果有一个参数,把参数对象中的属性依次遍历给$.fn

    $.fn.extend({ css:function(){},on:function(){} })
    // --> $.fn.css=function(){}
    // --> $.fn.on=function(){}

  ② 如果有多个参数,功能等价于$.extend的第二个功能

    $.fn.extend(p,{a:10},{b:20},{c:30})
    $.extend(p,{a:10},{b:20},{c:30})
    // --> p.a=10 p.b=20 p.c=30;

3. $.fn.extend与$.extend共同点

  ① $.fn.extend$.extend 多参数功能是完全一样的
  ② $.fn.extend$.extend 一个参数的功能其实都是为了把参数里面的属性依次遍历给 this
  ③ 这2大功能最终的目的都是为了进行对象的拷贝——>实现拷贝继承–>思考:能不能重用拷贝的逻辑(简化代码,代码见上)
    寻找共同点:
    a)都是为了拷贝
    b)拷贝其实只要确定:提供数据的对象、接收数据的对象
    c)第一大功能提供数据的对象:第二个参数及其后面的参数;接收数据的对象是第一个参数
    d)第二大功能提供数据的对象:第一个参数;接收数据的对象:this

三、jquery.util.js

  jquery的工具类方法。

jQuery.extend({
    // 可以遍历数组和对象
    each(obj, callback) {
        // 有2种情况,数组使用for循环,对象使用for...in循环

        // 不仅仅可以遍历数组,也可以遍历伪数组
        // { length:0 }
        // { 0:100,length:1 }
        // { 0:"a",1:"b",2:"c",length:3 }
        // 在这里,由于存在数组、伪数组2种情况,只能使用一种约定俗成的方式来通过他们的特征来进行判断:length属性,并且值>=0
        if ((length in obj) && obj.length >= 0) {
            for (let i = 0; i < obj.length; i++) {

                callback.call(obj[i], i, obj[i])
                // callback.apply(obj[i],[i,obj[i]])

                // 没有必要使用bind,bind的实现相对繁琐
                // callback.bind(obj[i])(i,obj[i])
            }

        } else {
            for (let i in obj) {
                callback.call(obj[i], i, obj[i])
            }
        }
    },

    type(data) {
        // 判断data的数据类型
        // --> Object.prototype.toString.call(1)
        // --> "[object Number]"

        var type = Object.prototype.toString.call(data);
        return type
            .replace("[object ", "")
            .replace("]", "")
            .toLowerCase();
    }
})

jQuery.fn.extend({
    each(callback) {
        //this:jquery对象
        jQuery.each(this, callback)

        return this;
    }
});

四、jquery.style.js

  jquery的样式操作方法。

jQuery.fn.extend({
    // 1、获取样式$("div").css("color")  只能获取到第一个div的颜色
    // 2、设置样式
    //      $("div").css("color","red") 设置每一个div的字体颜色
    //      $("div").css({ color:"red","backgroundColor","blue" })
    css(...args) {
        var arg1 = args[0],
            arg2 = args[1];
        // 参数个数:1
        if (args.length === 1) {
            if (jQuery.type(arg1) === "string") {
                // a、获取样式:只能获取第一个元素的样式
                let firstDom = this[0];
                let domStyleObj = window.getComputedStyle(firstDom, null)
                return domStyleObj[arg1];
            } else {
                // b、设置多个样式  
                // arg1:{ color:"red",fontSize:"20px" }
                var _that = this;
                // 遍历出所有要添加的样式
                jQuery.each(arg1, function (key, value) {
                    // 遍历每一个DOM元素,添加指定的样式
                    _that.css(key, value);
                });
                return _that;
            }
        } else {
            // 参数个数:2  设置单个样式
            // 第一步:遍历每一个DOM
            // 第二步:给DOM添加样式
            return this.each(function (index, dom) {
                //this:表示一个DOM元素  ===   dom
                this.style[arg1] = arg2;
            });
        }
    },
    show() {
        this.css("display", "block");
        return this;
    },
    hide() {
        this.css("display", "none");
        return this;
    },
    toggle() {
        // 判断每一个元素,如果隐藏就显示,如果显示就隐藏
        this.each(function () {
            let $this = jQuery(this);
            $this[$this.css("display") === "none" ? "show" : "hide"]();
        })
    }
});

五、jquery.event.js

  jquery的事件框架。

(function () {
    // 将会保存曾经绑定过的所有的事件处理函数
    // 以DOM元素为区分,
    const events = [
        // { ele:div1,type:"click",callback:function(){} },
        // { ele:div1,type:"click",callback:function(){} },
        // { ele:div1,type:"keydown",callback:functioN(){} },
        // { ele:div3,type:"click",callback:function(){} }
    ];


    jQuery.fn.extend({
        // $("div").on("click",function(){})
        on(type, callback) {
            // 给当前jquery对象中的每一个DOM元素绑定事件
            this.each(function (index, element) {
                element.addEventListener(type, callback);
                events.push({ele: element, type, callback})
            });
            // 实现链式编程
            return this;
        },
        // 解绑绑定:$("div").off("click"):表示解除当前元素的所有的单击事件
        off(type) {
            this.each(function (index, element) {
                // 找到该元素曾经绑定过type类型的事件
                var evts = events.filter(function (evtObj) {
                    // 是否是该元素绑定的该类型的事件
                    var isCurrent = evtObj.ele === element && evtObj.type === type;
                    return isCurrent;
                });
                // 进行事件解绑操作
                evts.forEach(function (evt) {
                    var {callback} = evt;
                    element.removeEventListener(type, callback);
                })
            })
        }
    })
})()

六、调用方式

  调用时,需要注意先后顺序,先引入jquery.core.js、再引入jquery.util.js,最后引入其它的文件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<div id="div1">100</div>
<div id="div2">200</div>
<div id="div3">300</div>
</body>
<script src="./jquery.core.js"></script>
<script src="./jquery.util.js"></script>
<script src="./jquery.event.js"></script>
<script>
    $.fn.extend(p, {aaa: 100}, {bbb: 200}, {ccc: 300})
    console.log(p);


    $("div").on("click", function () {
        console.log('click div');
    });
    $("#div1").on("click", function () {
        console.log('click div1');
    });
    $("#div1").on("mouseover", function () {
        console.log('mouseover div1');
    });

    // 实现解绑div元素的click事件
    $("#div1").off("click");  // 找到该元素该类型的事件总和
</script>
</html>

七、总结

  总用写了四篇博客来学习JS库的封装,这是最后一篇:主要对前面的所有内容进行复盘和整理。

  这个系列的文章,主要是通过仿照jQuery来学习JS库的封装,共写了如下几篇博客:
  1. 《JS库的封装之一:入口函数》
  2. 《JS库的封装之二:添加方法》
  3. 《JS库的封装之三:添加事件》
  4. 《JS库的封装之四:代码整合》


  目录