Skip to content

如何写一个你自己的jQuery库? #10

Description

@Lucifier129

如何写一个你自己的jQuery库?

前言

本文面向的读者群如下:

  • 前端交互重度依赖jQuery库
  • 具备一定的原生js基础知识
  • 开始阅读某一版本的jQuery源码

本文采用的写作与编码手法如下:

  • 以标准实现为主,不考虑兼容性
  • 函数实现以新手直觉式为主,而非jQuery源码中经过千锤百炼的版本
  • 粗略搭建类jQuery式骨架,最终产物不以投入生产为目的,仅供参考
  • 用最终实现的库,实现一个在现代浏览器中正常工作的轮播图效果

注意:代码大致仿照jQuery,不代表跟它的内部实现以及api用法完全一样。

全部代码打包:猛击这里

轮播图实现DEMO:猛击这里

轮播图jsbin地址:猛击这里

立即执行的匿名函数

不管是模块化开发还是非模块化开发,建立一个工具库时,一般都得用到匿名函数。

差别在于:模块化开发依赖模块加载器提供的define函数,匿名函数作为参数传入,会自动getValue求值。而非模块化开发,则用特殊的格式写立即执行的匿名函数,其中一种格式如下:

;(function(window) {
    //这里写你所有的内容
}(window));

开头的分号,意在防止与其他js文件合并压缩时,由于上一个文件没有用分号结尾而产生问题。最末尾的分号则是防止与下一个js文件发生合并冲突。

将全局对象window作为参数传入,则可以使之在匿名函数内部作为局部变量访问,提供访问速度。

保存常用函数为局部变量

有一些数组或对象的方法经常能使用到,应将它们保存为局部变量以提高访问速度。

;(function(window){
    var obj = {},
                toStr = obj.toString,
                hasOwnProp = obj.hasOwnProperty,
                arr = [],
                slice = arr.slice,
                push = arr.push;

            var isObject = function(obj) {
                    return obj == null ? obj.toString() : toStr.call(obj) === '[object Object]';
                },
                isArray = Array.isArray || function(obj) {
                    return toStr.call(obj) === '[object Array]';
                },
                isFunction = function(obj) {
                    return typeof obj === 'function';
                },
                isString = function(obj) {
                    return typeof obj === 'string';
                },
                isBoolean = function(obj) {
                    return typeof obj === 'boolean';
                },
                inArray = function(arr, item) {
                    return arr.indexOf(item) !== -1;
                };

}(window));

定义一个迭代器each

本来嘛,不考虑兼容性,以标准实现为主,数组的迭代器可以用其原生的forEach,而对象的迭代则用for in加hasOwnProperty。但是为了一致性,定义一个数组与对象通用的迭代器,还是很有用的。

更重要的是,这个each函数还可以设计为能迭代类数组(对象以0、1、2……为属性名并且具有length属性);

    function each(obj, callback) {
                //如果既不是对象也不是数组,不迭代,直接返回
                if (!isObject(obj) && !isArray(obj)) {
                    return obj;
                }
                var len = obj.length;
                //要求obj.length类型为数字且不小于0
                //符合该条件就当做数组来迭代
                if (typeof len === 'number' && len >= 0) {
                    //如果有用户滥用length属性,导致错误是其活该
                    for (var i = 0; i < len; i += 1) {
                        //callback函数的this值为数组中的每一项
                        if (callback.call(obj[i], i, obj[i]) === false) {
                            //如果callback函数的返回值等价于false
                            //终止迭代
                            break;
                        }
                    }
                } else {
                    //程序运行到这一步,说明该迭代的Obj类型为object
                    for (var prop in obj) {
                        if (hasOwnProp.call(obj, prop)) {
                            if (callback.call(obj[prop], prop, obj[prop]) === false) {
                                break;
                            }
                        }
                    }
                }
                //返回被迭代的数组或对象
                return obj;
            }
            //好用的函数,输出为静态方法
            jQuery.each = each;

定义一个对象扩展函数extend

我们不能每次都手动将一个对象的属性或方法拷贝到另一个对象中,所以要定义一个批量拷贝的函数extend。作为粗糙模拟,不考虑深度克隆。

    function extend(target) {
                //将除target之外的所有参数保存为sources
                var sources = slice.call(arguments, 1);
                //迭代器each派上了用场
                each(sources, function() {
                    each(this, function(key, value) {
                         target[key] = value;
                    });
                });
                //返回被扩展的对象
                return target;
            }

             //好用的函数,输出为静态方法
             jQuery.extend = extend;

定义一个压入栈函数pushStack

我们已有迭代器each,遍历数组与对象很方便;但是,数组的长度length是可以增减的,增加时一个个赋值进去太麻烦,我们需要一个批量添加项目的工具函数。

这个方法类似于数组的extend方法。

    function pushStack(target) {
                var len = target.length;

                if (typeof len !== 'number' || len < 0) {
                    //没有length属性,或者它小于0,直接返回target
                    return target;
                }

                //保存除target外的所有参数并转化为数组形式
                var sources = slice.call(arguments, 1);

                each(sources, function() {
                    //this值就是sources数组中的每一项
                    var len = this.length;

                    //同样判断是否为数组或类数组
                    if (typeof len !== 'number' || len < 0) {
                        return;
                    }

                    //之前保存的常用函数之一push,派上用场
                    //与apply结合,批量push进target中
                    push.apply(target, this);
                });
                return target;
            }

定义一个作为局部函数的jQuery

有些库的做法是一开始就定义一个全局变量,而另一些的做法是先定义成局部变量,最后再赋值给一个全局变量。这里采用的是后者。

;(function(window){
    //请将这里当做对上面那块常用函数的折叠部分,下同

    function jQuery(selector, context) {
        return new jQuery.init(selector, context);
    }
}(window));

目前都提倡采用无new操作符的做法,所以让jQuery.init来代理jQuery函数,由它来处理参数与返回值。

本文做法其实是zepto式的,jQuery源码里采取了更复杂的方法,此处为简便,没有沿用。jQuery.init函数内部实现如下

    //函数也是对象,也可以添加属性和方法
            jQuery.init = function(selector, context) {

                //定义length属性,成为类数组
                //可以用each迭代,用pushStack批量增加元素
                this.length = 0;

                if (selector && selector.nodeName) {
                    //如果第一个参数为dom节点
                    //保存为this[0]
                    this[0] = selector;
                    this.length = 1;
                } else if (isFunction(selector)) {
                    //如果第一个参数为函数,则为DOM ready 事件
                    return document.addEventListener('DOMContentLoaded', selector, false);
                } else {
                    //原生方法处理selector选择器,如果有context上下文,则以它为起点搜索,否则以document为始
                    //要求context必须是DOM,即便是jQuery的实例也不行,因为我们只是粗糙模拟
                    var items = (context && context.nodeName ? context : document).querySelectorAll(selector);

                    pushStack(this, items);
                }


            };

jQuery的$函数的参数范围非常广,我们这里只取最常用的一种。

处理jQuery.init与jQuery的原型继承关系

这方面,懂的人觉得简单,不懂的人一头雾水,多说无益,看代码为上。

    //将jQuery的原型对象放到它的fn属性中
            jQuery.fn = jQuery.prototype;
            //本来这句与上句并做一起,为了便于理解,分拆开来
            //jQuery.init是函数,函数都有prototype原型对象
            //而且是可改变的,让它指向jQuery.prototype
            jQuery.init.prototype = jQuery.prototype;

            //设置一个jQuery的简写
            var $ = jQuery;

            //添加extend方法给jQuery的原型
            //专门用来拓展它自己
            $.fn.extend = function() {
                return extend.apply(window, [this].concat(slice.call(arguments)));
            };

给jQuery的原型添加方法

至此,基本骨架搭建好了,添加一些原型方法让这个库更加羽翼丰满吧。

        $.fn.extend({
            //each在这里很简单
            each: function(callback) {
                return each(this, callback);
            },
            //以下为搜索dom节点的操作
            find: function(selector) {
                //什么参数都不传,在这里返回空的jQ实例, 下同
                var ret = $();
                var nodes = [];
                //其实可以用each(this, callback)
                //但既然已经有$.fn.each,那就直接使用
                this.each(function() {
                    //注意:这里的this值,是上一个this值的项
                    //请看each函数的源码
                    var items = this.querySelectorAll(selector);

                    //搜索到的新元素先推入一个数组
                    //毕竟push为数组原生方法,比类数组更快
                    //items里有元素才推进去
                    items.length && push.apply(nodes, items);
                });
                //最后才推进新jQ实例
                return pushStack(ret, nodes);
            },
            eq: function(index) {
                index = index >= 0 ? index : index + this.length;
                return $(this[index]);
            },
            children: function(selector) {
                //返回新jQ实例
                var ret = $();
                var nodes = [];
                //如果有选择器参数,则新建一个jQuery专用的随机id
                var id = selector || 'jQuery' + Math.random().toString(36).substr(2);

                this.each(function() {
                    var items;
                    if (selector) {
                        //设置id,如果有就用其以前的id
                        this.id = this.id || id;
                        //构造选择器'#id>.target',选中所需元素
                        items = document.querySelectorAll(this.id + '>' + selector);
                        //拿到之后删除添加的随机id
                        if (this.id === id) {
                            this.removeAttribute('id');
                        }
                    } else {
                        //没选择器,直接获取children属性中的节点
                        items = this.children;
                    }

                    items.length && push.apply(nodes, items);
                });
                //推入栈
                return pushStack(ret, nodes);
            },
            first: function() {
                //新实例,下同
                var ret = $();
                var nodes = [];
                this.each(function() {
                    var item = this.firstElementChild;
                    //存在第一个非文本非注释的元素节点,才推进去
                    item && nodes.push(item);
                });
                //最后才全部压入栈,pushStack直接返回第一个参数,恰好是我们要返回的
                return pushStack(ret, nodes);
            },
            last: function() {
                var ret = $();
                var nodes = [];
                this.each(function() {
                    var item = this.lastElementChild;
                    item && nodes.push(item);
                });
                return pushStack(ret, nodes);
            },
            siblings: function(selector) {
                var ret = $();
                var nodes = [];
                //如果有选择器参数,则新建一个jQuery专用的随机id
                var id = selector || 'jQuery' + Math.random().toString(36).substr(2);
                this.each(function() {
                    //找到父节点
                    var parent = this.parentNode,
                        items;
                    if (selector) {
                        //设置id
                        parent.id = parent.id || id;
                        items = document.querySelectorAll(parent.id + '>' + selector);
                        //删除id
                        if (parent.id === id) {
                            parent.removeAttribute('id');
                        }
                        items.length && push.apply(nodes, items);
                    } else {
                        push.apply(nodes, parent.children);
                    }
                    //从数组中删除该节点,则余下全部兄弟节点
                    nodes.splice(nodes.indexOf(this), 1);
                });

                return pushStack(ret, nodes);
            },
            parent: function() {
                var ret = $();
                var nodes = [];

                this.each(function() {
                    var parent = this.parentNode;
                    parent && nodes.push(parent);
                });

                return pushStack(ret, nodes);
            },
            index: function() {
                var target = this[0];
                return slice.call(target.parentNode.children).indexOf(target);
            },
            append: function(node) {
                var len = this.length;
                this[0].appendChild(node);
                return this;
            },
            prepend: function(node) {
                var len = this.length;
                var first = this[0].firstElementChild;
                this[0].insertBefore(node, first);
                return this;
            }
        });

如上,添加了一些dom遍历的方法,下面添加css类的方法,分批书写,便于理解和阅读。

    $.fn.extend({
                css: function() {
                    //css方法既是getter又是setter,参数不定
                    //干脆不写形参,将实参转为数组形式
                    var args = slice.call(arguments);
                    var len = args.length;

                    //如果参数数量为1,且其为字符串
                    //就是 $(elem).css('color')
                    if (len === 1 && typeof args[0] === 'string') {
                        //get one
                        return getComputedStyle(this[0], null).getPropertyValue(args[0]);
                    } else if (len === 2) {
                        //形式为:$(elem).css('color', '#333');
                        //set all
                        return this.each(function() {
                            this.style[args[0]] = args[1];
                        });
                    }
                },
                addClass: function(classNames) {
                    //如果不是字符串参数,直接返回this
                    if (typeof classNames !== 'string') {
                        return this;
                    }
                    //去掉两端空白符后,返回数组形式
                    classNames = classNames.trim().split(' ');
                    return this.each(function() {
                        var classList = this.classList;
                        //再遍历一遍,添加所有class名
                        each(classNames, function(key, value) {
                            classList.add(value);
                        });
                    });
                },
                removeClass: function(classNames) {
                    //如果不是字符串参数,直接返回this
                    if (typeof classNames !== 'string') {
                        return this;
                    }
                    //去掉两端空白符后,返回数组形式
                    classNames = classNames.trim().split(' ');
                    return this.each(function() {
                        var classList = this.classList;
                        //再遍历一遍,删除所有class名
                        each(classNames, function(key, value) {
                            //与addClass只有一个差别
                            //许多可以合并函数来优化都没有做,只为直观
                            classList.remove(value);
                        });
                    });
                }
            });

要做轮播图不需要太多东西,这会儿轮到动画animate了

    //动画组件要定义一些工具方法
            var nextTick = requestAnimationFrame || function(fn) {
                return setTimeout(fn, 1000 / 60);
            };

            function getStyle(elem, prop) {
                return parseFloat(getComputedStyle(elem, null).getPropertyValue(prop), 10);
            }

            function animate(elem, propObj, duration, callback) {
                var start = +new Date();
                var oldValue = {};
                var diff = {};
                var ratio;
                for (var prop in propObj) {
                    diff[prop] = propObj[prop] - (oldValue[prop] = getStyle(elem, prop));
                }


                function move() {
                    ratio = (+new Date() - start) / duration;
                    if (ratio < 1) {
                        each(diff, function(prop) {
                            elem.style[prop] = oldValue[prop] + this * ratio + 'px';
                        });
                        nextTick(move);
                    } else {
                        each(diff, function(prop) {
                            elem.style[prop] = propObj[prop] + 'px';
                        });
                        callback();
                    }
                }

                move();
            }

        function noop() {};

        $.fn.animate = function(propObj, duration, callback) {
            var self = this,
                len = self.length,
                count = 0;
            return self.each(function() {
                animate(this, propObj, duration || 400, typeof callback === 'function' ? function() {
                    ++count === len && callback.call(self);
                } : noop);
            });
        };

由于轮播图中用到的事件函数比较简单,故这里不再添加事件模块,就用目前的产物,尝试写一个轮播图吧。

先将这个工具库输出到全局对象;

    window.jQuery = window.$ = jQuery;

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions