模拟 jQuery API的实现

2019-01-23  本文已影响12人  夜未央_M

jQuery 是什么?

jQuery实质上是一个构造函数,接受一个参数,这个参数可能是节点,然后返回一个方法对象去操作节点。官方文档是这样说明的:
jQuery是一个快速,小巧,功能丰富的JavaScript库。它通过易于使用的API在大量浏览器中运行,使得HTML文档遍历和操作,事件处理,动画和Ajax变得更加简单。

那么今天我们就来演示一下jQuery API的工作原理

用原生DOM写一个类似jQuery的API

1.写一个带有id的 ul 列表

<!DOCTYPE html>
<html lang="zh">

    <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>模拟 jQuery API的实现</title>
    </head>

    <body>
        <ul>
            <li id="item1">选项1</li>
            <li id="item2">选项2</li>
            <li id="item3">选项3</li>
            <li id="item4">选项4</li>
            <li id="item5">选项5</li>
            <li id="item6">选项6</li>
        </ul>
        <script>
        </script>
    </body>

</html>


2. 以item3为节点,找到其兄弟节点

通过 var allChildren = item3.parentNode.children 获取 item3 父节点的所以子节点,然后遍历所有子节点,得到 item3以外 的所有节点,这样就找到选项3的所以兄弟节点啦。可以 console.log一下。
(由于array是伪数组,不能用push的方法,所以我们用到 array[array.length] = allChildren[i] 的方法)

<script>
      var allChildren = item3.parentNode.children
      var array = {length:0}
      for(let i = 0; i < allChildren.length; i++){
        if(allChildren[i] !== item3){
          array[array.lenth]=allChildren[i]
          array.length += 1
        }
      }
      console.log(array)
</script>


3. 代码封装

封装的好处有很多:给代码一个名字方便调用;形成局部变量可以避免覆盖JS原始变量(立即调用函数)等
给这个函数取个名字,如 getSiblings;把 item3 换成 node,这样输入任意节点都可以使用这个函数了;注意不要忘记 return,这样我们就得到了一个函数 function getSiblings(node){}

<script>
      function getSiblings(node){
        var allChildren = node.parentNode.children
        var array = {length:0}
        for(let i = 0; i < allChildren.length; i++){
          if(allChildren[i] !== node){
            array[array.lenth]=allChildren[i]
            array.length += 1
          }
        }
        return array
      }
</script>


4. 封装函数:function addClass(node, classes){}

现在我们要给 item3 加 class属性
首先我们声明一个 classes 对象,里面有 a、b、c 三个属性;同时分别给它们一个布尔值,方便 add 和 remove;遍历各个属性。

      var classes = {'a':false, 'b':true, 'c':true}
      for(let key in classes){
        var value = classes[key]
        var methodName = value?'add':'remove';
        item3.classList[methodName](key)
      }

可以看到,class b、c已经被添加到 item3 中了
同样我们来封装一下这些代码,如下所示:

function addClass(node, classes){
  for (var key in classes){
    var value = classes[key]
    var methodName = value ? 'add' : 'remove'
    //console.log (methodName )
    //console.log (node.classList)
    //console.log (node.classList.add)
    //console.log (node.classList[methodName])
    node.classList[methodName](key)
  }
}
/*
obj.x()  等同于  obj['x']()
注意一点上述代码不能用点运算符,要用[]运算符,classList['add'] === classList.add
*/
addClass(item3, {a:true, b:false, c:true})

现在,只要你给一个 node 和 classes 于此函数,就可以给 该节点添加 classes所包含的正确属性


5.命名空间:

给封装的函数一个名字,方便其他人使用,同时防止与前人命名的冲突。
假如我们想把这两个封装好好的函数联系到一起,以便以后调用的话,我们可以这样去写。

window.xxdom = {} 
xxdom.getSiblings = function (node) { 
  var allChildren = node.parentNode.children

  var array = {
    length: 0
  }
  for (let i = 0; i < allChildren.length; i++) {
    if (allChildren[i] !== node) {
      array[array.length] = allChildren[i]
      array.length += 1
    }
  }
  return array
}
xxdom.addClass = function (node, classes) {
  classes.forEach( (value) => node.classList.add(value) )
}

xxdom.getSiblings(item3)
xxdom.addClass(item3, ['a','b','c'])


6.能不能把node 放在前面

node.getSiblings()
node.addClass()

我们发现当我们要用到上面的命名空间的时候会非常麻烦,我们总是要用 dom.getSiblings()、xxdom.addClass()、总是要带有别人的一个小尾巴。 item3.getSiblings()、item3.addClass() 好像看起来更清爽一些,有没有办法来实现呢?
其实是有的。为了验证,我们给 Node 的共有属性添加 getSiblings属性,然后我们测试下能否访问到:

      Node.prototype.getSiblings = function(){
        return 1
      }

现在有个问题,我们这里的getSiblings()怎么获取到item3 ?
方法一:扩展 Node 接口
直接在 Node.prototype 上加函数
Node 如何取到 item3?
this
why?把上面写成 .call 的形式,因为this 是call 的第一个参数。


那么 item3 为什么会有 getSiblings属性呢?
因为我们篡改了其 proto 最终指向的 node.prototype 的共有属性,然后添加了一个 getSiblings的方法,然后它里面呢先去获取this 的父节点的所有儿子,那么 this 是谁呢?this 就是你在调用的时候就会帮你把 item3给传递进来,item3会自动的传给 getSiblings() 的 this,所以 this 就是.前面的东西,不管你是什么。然后声明一个伪数组,遍历这个伪数组,如果伪数组里面的第[i]项全不等于 this (这时候的 this 就是 item3),那么不等于 item3的 item放到伪数组里面,伪数组的 length +1,循环后返回这个伪数组,于是我们得到了 item1、item2、item4、item5。

** 自己命名新的接口 Node2**
前面写的内容其实不是很好,为什么呢?
因为我们在改 node 的共用属性。比如 A 同学在上面添加了2个函数,B 同学也在上面添加了2个函数,然后我们也添加了2个函数,所以就存在被覆盖的可能性。
那, 如果不改原型,我们怎么能实现 item3.getSiblings呢?方法也是有的!就是命名新的接口,示例如下:

 function Node2(node){
     return {
         element: node,
         getSiblings: function(){

         },
         addClass: function(){

         }
     }
 }
 let node =document.getElementById('x')
 let node2 = Node2(node)
 node2.getSiblings()
 node2.addClass()



真实代码(这种对 Node 没有产生破坏的方法叫做「无侵入」)如下:

window.Node2 = function (node) {
  return {
    getSiblings: function () {
      var allChildren = node.parentNode.children
      var array = {
        length: 0
      }
      for (let i = 0; i < allChildren.length; i++) {
        if (allChildren[i] !== node) {
          array[array.length] = allChildren[i]
          array.length += 1
        }
      }
      return array
    },
    addClass: function (classes) {
      for (var key in classes) {
        var value = classes[key]
        var methodName = value ? 'add' : 'remove'
        node.classList[methodName](key)
      }
    }
  }
}


7. 把 Node2 改成jQuery

通过上面的操作,我们就在原有 Node 的基础上新扩展了接口,我们这时候就可以给新扩展的起个自己喜欢的名字,就叫jQuery吧

function jQuery(node) {
  return {
    element: node,
    getSiblings: function () {
    },
    addClass: function () {
    }
  }
}
let node = document.getElementById('x')
let node2 = jQuery(node)
node2.getSiblings()
node2.addClass()

实际操作如下:

window.jQuery = function (nodeOrselector) {
  let nodes = {}
  if (typeof nodeOrselector === 'string') {
    let temp = document.querySelectorAll(nodeOrselector) //伪数组
    for (let i = 0; i < temp.length; i++) {
      nodes[i] = temp[i]
    }
    nodes.length = temp.length
  } else if (nodeOrSelector instanceof node) {
    node = {
      0: nodeOrSelector,
      length: 1
    }
  }

  nodes.getSiblings = function () {
    var allChildren = node.parentNode.children
    var array = {
      length: 0
    }
    for (let i = 0; i < allChildren.length; i++) {
      if (allChildren[i] !== node) {
        array[array.length] = allChildren[i]
        array.length += 1
      }
    }
    return array
  }


  nodes.addClass = function (classes) {
    classes.forEach((value) => {
      for (let i = 0; i < nodes.length; i++) {
        node[i].calssList.add(value)
      }
    })
  }

  return nodes

}

window.$ = jQuery   //缩写吧:alias
var $div = $('div')
$div.addClass('red') // 可将所有 div 的 class 添加一个 red
$div.setText('hi') // 可将所有 div 的 textContent 变为 hi


8. $缩写与alias

window.$ = jQuery
即var node2 = $(node)

但是为了防止记混 node2 到底有没有引入 jQuery
大家通常这样写:

var $node2 = $(node)

我们会在引用的 jQuery标签前面加上$符号来区分原生的标签或者接口,看到$我们立马就明白了这是由 jQuery 引用染生出来的。

到这里我们已经知道 jQuery 是个什么了:它就是一个函数,是 JS 原始 DOM 的扩展,便于我们更好得使用JS写代码的加强版 DOM API。

上一篇下一篇

猜你喜欢

热点阅读