How to implement a simplified version of jQuery

Posted by coool on Sun, 19 May 2019 04:28:48 +0200

JQuery is a very convenient library for operating DOM. Although jQuery is used less and less with the popularity of frameworks such as React and Vue nowadays, many of these ideas are worth learning. This article will introduce how to implement a simplified version of jQuery from scratch.

Here, I name this library Clus(class homophonic) The following is replaced by the dollar sign.

First, you need to declare a constructor and do some initialization:

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

$.fn = $.prototype = {
    contructor: $,
    init,
};

As you can see, the constructor returns an instance of $.fn.init. The advantage of this is that you can create a new instance without having to use a new constructor every time. As you can see, the whole core is on the init function:

function init(selector) {
    const fragmentRE = /^\s*<(\w+|!)[^>]*>/;
    const selectorType = $.type(selector);
    const elementTypes = [1, 9, 11];

    let dom;

    if (!selector) {
        dom = [],
        dom.selector = selector;
    } else if (elementTypes.indexOf(selector.nodeType) !== -1 || selector === window) {
        dom = [selector],
        selector = null;
    } else if (selectorType === 'function') {
        return $(document).ready(selector);
    } else if (selectorType === 'array') {
        dom = selector;
    } else if (selectorType === 'object') {
        dom = [selector],
        selector = null;
    } else if (selectorType === 'string') {
        if (selector[0] === '<' && fragmentRE.test(selector)) {
            dom = $.parseHTML(selector),
            selector = null;
        } else {
            dom = [].slice.call(document.querySelectorAll(selector));
        }
    }

    dom = dom || [];
    $.extend(dom, $.fn);
    dom.selector = selector;

    return dom;
}

It can be clearly seen that different operations are performed according to the type of parameters that are passed in, for example, if the input function is a function, then all operations in the function are after DOM Ready; and then, if the input is a string, and if it is a tag, the tag string will be parsed into DOM Fragment, and if it is a normal string, it will be parsed into DOM Fragment. Call the document.querySelectorAll() method to find the DOM.

I believe you can easily see the above code, but one thing worth mentioning is $. extend(dom, $.fn); this code means that all methods on the instance are added to the DOM array object, so that the method of an instance, such as $('. clus'). add ('hello'), can be called directly in the chain. It's implemented in $. fn. So all methods implemented in $. FN can be invoked in the form of $(selector).method().

As for the extend() method, I think it's the core method in the whole library besides the init() method. The code is as follows:

export default function extend() {
    let options, name, clone, copy, source, copyIsArray,
        target = arguments[0] || {},
        i = 1,
        length = arguments.length,
        deep = false;

    if (typeof target === 'boolean') {
        deep = target;
        target = arguments[i] || {};
        i++;
    }

    if (typeof target !== 'object' && $.type(target) !== 'function') {
        target = {};
    }

    if (i === length) {
        target = this;
        i--;
    }

    for (; i < length; i++) {
        //
        if ((options = arguments[i]) !== null) {
            // for in source object
            for (name in options) {

                source = target[name];
                copy = options[name];

                if (target == copy) {
                    continue;
                }

                // deep clone
                if (deep && copy && ($.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) {
                    // if copy is array
                    if (copyIsArray) {
                        copyIsArray = false;
                        // if is not array, set it to array
                        clone = source && Array.isArray(source) ? source : [];
                    } else {
                        // if copy is not a object, set it to object
                        clone = source && $.isPlainObject(source) ? source : {};
                    }

                    target[name] = extend(deep, clone, copy);
                } else if (copy !== undefined) {
                    target[name] = copy;
                }
            }
        }
    }

    return target;
}

As you can see, just like jQuery's implementation, that's exactly where copy ing comes from.

Following is an example of addClass() method to illustrate how to operate DOM:

function addClass(cls) {
    let classes, clazz, el, cur, curValue, finalValue, j, i = 0;

    if (typeof cls === 'string' && cls) {
        classes = cls.match(rnotwhite) || [];

        while((el = this[i++])) {
            curValue = getClass(el);
            cur = (el.nodeType === 1) && ` ${curValue} `.replace(rclass, ' ');

            if (cur) {
                j = 0;

                while((clazz = classes[j++])) {
                    // to determine whether the class that to add has already existed
                    if (cur.indexOf(` ${clazz} `) == -1) {
                        cur += clazz + ' ';
                    }
                    finalValue = $.trim(cur);
                    if ( curValue !== finalValue ) {
                        el.setAttribute('class', finalValue);
                    }
                }
            }
        }
    }

    return this;
}

$.fn.addClass = addClass;

It is worth mentioning that in the instance method, this keyword can access the set of all elements queried by the selector, where each element is operated on through a while loop. To implement a chain operation like $(selector).addClass().removeClass(), you only need to return one this in each instance method. The implementation of other instance methods such as hasClass() is similar.

In fact, each instance method obtains the queried elements through this keyword, and then traverses these elements to perform specific operations on each element. In this case, a chestnut is given:

function append(DOMString) {
    let el, i = 0,
        fregmentCollection = $.parseHTML(DOMString),
        fregments = Array.prototype.slice.apply(fregmentCollection);

    while((el = this[i++])) {
        fregments.map(fregment => {
            el.appendChild(fregment);
        });
    }

    return this;
}

$.fn.append = append;

Above is the implementation of append(). First, DOMString is parsed as a fregment, then the queried elements (through this keyword) are traversed and appendChild() is operated on for each element, thus inserting DOM into all matched elements.

Other example methods are implemented in a similar way. I will not elaborate on them here. If you want to see the implementation of other methods in more detail, you can go directly to them. Clus View the source code.

Topics: Javascript JQuery less Fragment React