如何實現一個簡化版的 jQuery

大板慄發表於2019-02-16

對於操作 DOM 來說,jQuery 是非常方便的一個庫,雖然如今隨著 React, Vue 之類框架的流行,jQuery 用得越來越少了,但是其中很多思想還是非常值得我們學習的,這篇文章將介紹如何從零開始實現一個簡化版 jQuery

在這裡,我把這個庫命名為 Clus(class 的諧音),下面以 $ 符號代替。

首先需要宣告一個建構函式並做一些初始化操作:

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

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

可以看到,該建構函式返回一個 $.fn.init 的例項,這樣做的好處就是在使用的時候不要每次都 new 一個建構函式就可以建立一個新的例項了,可以看出來,整個核心都在 init 函式上了:

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;
}

可以很清楚的看到,根據傳入的引數型別的不同進行一些不同的操作,比如傳入的是函式的話,則該函式裡的操作的都是 DOM Ready 之後的操作了;再比如傳入的是字串的話,並且如果是標籤的話,則會把這段標籤字串解析成 DOM Fragment,如果是普通字串,則會呼叫 document.querySelectorAll() 方法來查詢 DOM。

相信大家都能很容易的看明白上面的程式碼,不過有一點值得一提的是 $.extend(dom, $.fn); 這段程式碼,其含義是把例項上的所有方法都新增到 dom 這個陣列物件中,這樣做的目的就是為了可以直接鏈式呼叫某個例項的方法,比如 $(`.clus`).addClass(`hello`),這個 addClass() 方法就是在 $.fn 上實現的。因此所有在 $.fn 實現的方法都可以通過 $(selector).method() 這種方式來呼叫了。

至於 extend() 方法我認為是除了 init() 方法以外,整個庫中最核心的一個方法了,程式碼如下:

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;
}

可以看到,和 jQuery 的實現一毛一樣,沒錯就是從那兒 copy 過來的當然一樣。

下面以 addClass() 方法為例介紹如何操作 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;

值得一提的就是在例項方法中,this 關鍵字可以訪問到根據選擇器所查詢到的所有元素的集合,在這裡是通過 while 迴圈來對每個元素進行操作。要實現類似 $(selector).addClass().removeClass() 這樣的鏈式操作,只需要在每個例項方法中返回一個 this 即可。要實現其他例項方法比如 hasClass() 之類的也是類似的方法。

其實每個例項方法都是通過 this 關鍵字來獲取查詢到的元素,然後遍歷這些元素來針對每個元素進行具體的操作,在舉一個栗子:

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;

上面是 append() 的實現,首先先解析 DOMStringfregment,然後就是遍歷查詢到的元素(通過 this 關鍵字)並針對每個元素去進行 appendChild() 的操作,從而把 DOM 插入到匹配到的所有元素中。

其他例項方法也是通過類似的方式實現的,這裡就不一一細說了,想更詳細的檢視其他方法的實現可以直接到 Clus 中檢視原始碼。

相關文章