jQuery设计思想及其简单实现

239 阅读3分钟

前言

 jQuery的实现都是基于DOM API的,但是相比原生JavaScript的DOM操作更加便利,而且可以直接链式操作。它的基本设计思想是声明一个全局函数jQuery,使用别名$,对接受参数进行重载,获取对应的元素,之后便返回一个对象,称为jQuery构造出来的对象,再用这个对象去操作获取到的元素。  

选择网页元素

jQuery使用$(selector)的方法实现获取网页元素的操作,

我们这里用DOM API的querySelector去实现它。

window.$ = window.jQuery = function(selector){
    let elements = document.querySelectorAll(selector)
    return {//返回一个操作elements的对象}
}

查找元素

获取了网页元素之后,怎么查找元素里面的内容呢?封装一个find函数,此时我们需要使用for循环来遍历里面的元素。

find(selector) {
    let array = [];
    for(let i=0;i<elements.length;i++){
        array = array.concat(Array.from(elements[i].querySelectorAll(selector)))
    }
    return this
}

遍历函数

事实上,很多地方我们都需要去遍历$(selector)获取的元素,为何不直接封装一个each函数来直接调用呢?

each(fn) {
    for(let i=0;i<elements.length;i++){
        fn.call(null,elements[i])
    }
}

当调用each函数后,就会传递elements的每一项给fn函数并调用。上面的函数可以用更简洁的代码实现

find(selector) {
    let array = [];
    each((item)=>array.concat(Array.from(item.querySelectorAll(selector))))
    return this
}

jQuery实现重载

可以看到上面的函数返回的是当前执行环境的对象,也就是jQuery构造出来的对象。

如果我们继续执行链式操作的话,例如$('#id').find('.child').parent(),

此时parent操作的elements是$('#id')中的elements而不是find中的array,怎么办呢?

这里我们需要对$('#id')进行重载

window.$ = window.jQuery = function(selectorOrArray){
    let elements
    if(typeof selectorOrArray === 'string'){
        elements = document.querySelectorAll(selectorOrArray)
    }else if(selectorOrArray instanceof Array){
        elements = selectorOrArray
    }
    return {//返回一个操作elements的对象}
}

现在我们把函数的返回值改为return jQuery(array)

就可以把find产生的array传给jQuery函数了,返回的是一个新的对象,

elements保存的是find函数产生的array,可以按预期地继续执行链式操作了。

返回上一个执行环境

进行jQuery链式操作的时候,我们若是要返回上一个执行环境怎么办?

例如$('#id').find('.child').parent().addClass()

我想addClass操作的是$('#id')里的elements要怎么做呢?

我们可以在进入下一个执行环境之前,保存当前执行环境的对象。

以find函数为例

find(selector) {
    let array = [];
    for(let i=0;i<elements.length;i++){
        array = array.concat(Array.from(elements[i].querySelectorAll(selector)))
    }
    //把当前执行环境的对象保存到array里面
    array.oldApi = this;
    return jQuery(array);
}

当执行find函数进入下一个执行环境的时候,elements保存的是find返回的数组。

这个数组里面的oldApi属性保存的就是上一个执行环境的对象,

可是jQuery(array)返回的对象里并没有保存到这个对象的引用,因此,我们需要添加一个属性以保存对象引用。

oldApi:selectorOrArray.oldApi

现在,我们只要调用一个函数,让他返回oldApi对象就可以了。这个函数在jQuery是end()

end() {
    return this.oldApi
}

在jQuery中使用原型

在执行链式操作$('#id').find('.child').parent().addClass()的时候,我们可以看到,

每一步都会创建一个新的API,每一个新的API都创建自己对应的find、Each等函数,

因此会引起内存使用暴涨,我们需要用原型来解决这个问题。

const api = Object.create(jQuery.prototype)

把jQuery函数的API放到它的prototype属性里面,在jQuery函数中创建一个对象api,这个对象的__proto__属性保存jQuery.prototype的引用。

给这个对象添加属性,分别保存elements和oldApi。

  Object.assign(api, {
    elements: elements,
    oldApi: selectorOrArrayOrTemplate.oldApi
  })

修改jQuery函数的返回值return api,这个时候我们每次调用jQuery函数的时候,都会去调用它的原型上的API而不会创建大量的对象了。