【jQuery】浅析jQuery设计原理

134 阅读2分钟

jQuery封装原理技巧,你真的了解吗?

目录

  • jQuery有多牛X
  • jQuery设计模式
  • 术语+命名风格
  • 链式风格
  • 浅析jQuery设计封装原理
  • 总结+github仓库代码

一、jQuery有多牛X

它是目前最长寿的库,2006年发布。

它是世界上使用最广泛的库,全球80%的网站在用


二、jQuery设计模式

  • jQuery用到了那些设计模式?
    1. 不用new的构造函数,这个模式没有专门的名字
    2. $(支持多个参数),这个模式叫做重载
    3. 用闭包隐藏细节,这个模式没有专门的名字
    4. $.text()即可读也可写,getter/setter
    5. $.fn是$.prototypr的别名,这叫别名
    6. jQuery针对不同的浏览器使用不同的代码,这叫适配器

三、术语+命名风格

  • 举例

    1. Object对象表示Object构造出来的对象
    2. jQuery对象表示jQuery构造出来的对象
  • 命名风格

    const div=$('div');
    
    1. 上面这个代码会产生某种歧义,让人误以为是原生DOM的。怎么避免这种误解呢?
    2. 改成这样
    const $div=$('div');
    
    1. 因为他是jQuery对象,所以在前缀加上$以区分。

四、链式风格

  • 也叫jQuery风格
    1. window.jQuery()是我们提供的全局函数
    2. 代码示例
<!DOCTYPE html>
<html lang="en">
<head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Document</title>
</head>
<body>
    <div class="demo">demo</div>
</body>
<script>
     import $ form 'jquery';	//假设导入了jQuery包
     $('demo').addClass('demo2')
              .css("width":"100px","height":"100px")
              .css("border":"1px solid red","font-size":"150%");

   /* 能够一直使用"."语法来操作这个选中的div元素就是链式风格了 */
</script>
</html>
  • 特殊函数jQuery
    1. jQuery(选择器) 用于获取对应的元素
    2. 但是jQuery不返回这些元素
    3. 相反,它返回一个可以操作的api对象,称为jQuery构造出来的对象
    4. 这个api对象可以直接操作对于的元素

五、浅析jQuer设计封装原理

在前面,我实现了自己手动封装一个DOM库,其实功能十分有限,API也仅仅是简单的增删改查。

而今天我将要简单分析jQuery的设计封装原理,它之所以这么长寿,使用这么广泛,因为它是真的非常经典!下面我们通过自己尝试封装jQuery库来一步一步的感受其中的奥妙!

  • 我们不妨先自己尝试下封装jQuery库,从起点出发,探究它的深层代码底蕴。用过jQuery的都知道,jQuery接受两个参数,一个是选择器,另外一个是字符串内容。但是它不返回选择的元素,而是返回一个API对象
    1. 我们先实现返回一个API对象
/* 在window上挂载一个全局函数jQuery */
window.jQuery=function (selector) {
   let elements=document.queryselectorAll(selector);

   /* 既然jQuery是返回的一个API对象,那我们也定义一个API对象然后再返回它,注意这个API可以操作elements,这里就是用到了闭包 */

   const api = {
       addClass(className){
           for(let i=0;i<elements.length;i++){
               elements[i].classList.add(className);
           }
           return ???;
       }
               .
               .
               .(省略的一些函数)
               .
               .
   }
   return api;
}
  1. 每个函数也返回api对象,这样就实现了链式操作!
window.jQuery=function (selector) {
   let elements=document.queryselectorAll(selector);

   const api = {
       addClass(className){
           for(let i=0;i<elements.length;i++){
               elements[i].classList.add(className);
           }
           return api; //每个函数操作完后也同样返回api;
       }
               .
               .
               .(省略的一些函数)
               .
               .
    }
    return api;
}
  1. 再次优化返回的API对象--使用this
window.jQuery=function (selector) {
   let elements=document.queryselectorAll(selector);

   const api = {
       addClass(className){
           for(let i=0;i<elements.length;i++){
               elements[i].classList.add(className);
           }
           return this; //使用this优化
       }
               .
               .
               .(省略的一些函数)
               .
               .
   }
   return api;
}
  1. 有没有觉得定义了一个api然后再返回api有点多余??那就对了,接下来继续改进
window.jQuery=function (selector) {
   let elements=document.queryselectorAll(selector);

   /* 直接return 其实return里面这一坨就是api */

   return {
       addClass(className){
           for(let i=0;i<elements.length;i++){
               elements[i].classList.add(className);
           }
           return this; //使用this优化
       }
               .
               .
               .(省略的一些函数)
               .
               .
      }
}
  1. 实现find()功能查找出响应选择器对应的元素,其实jQuery就是返回使用一次后,就会返回新的api对象,很显然我们在实现find()功能时会出现很多意想不到的错误,那要怎么办呢? 其实答案很明显了,jQuery本来就是重新封装元素对应的api,我们只要在里面添加封装一个函数来返回一个新的api对象即可。那这样刚开始接受的参数selector就有了变化,可能也是数组类型,我们只需要在开头就判断好参数对应的类型就好了。这就是属于重载了。当然了,后面有更多的函数如果也这样,也只需要判断selector类型,重新使用jQuery再次封装一个新的api即可。
window.jQuery=function (selector) {
   let elements;	//将elements变成jQuery里面的全局变量,不然let只有在块作用域里面才有用
   if(typeof selector==='string'){
       elements=document.queryselectorAll('selector')
   }
   else if(selector instanceof Array){
       elements=selector;
   }

   return {
       addClass(className){
           for(let i=0;i<elements.length;i++){
               elements[i].classList.add(className);
           }
           return this; //使用this优化
       },
       find(selector){
           let arr=[];
           for(let i=0;i<elements.length;i++){
               const elements2=Array.from(elements[i].queryselectorAll(selector))
               arr=arr.concat(elements2);
           }
           /*  return this 注意这里不能返回this,否则会出错,原因如开头的解释 */

           const newApi=jQuery(arr);	//重新使用jQuery封装新的api
           return newApi;		
       }
               .
               .
               .(省略的一些函数)
               .
               .
   }
}
  1. 实现end()或者叫做return()函数,就是用户想要返回上一次想要操作的api元素来进行操作。我们其实可以在find()函数身上下手,因为只有find()后才是找到或者保留某个对应元素对应的api,这样以来,我直接在find()函数里面保留上一次的oldApi不就ok了!
window.jQuery=function (selector) {
  let elements;
  if(typeof selector==='string'){
      elements=document.queryselectorAll('selector')
  }
  else if(selector instanceof Array){
      elements=selector;
  }

  return {
      addClass(className){
          for(let i=0;i<elements.length;i++){
              elements[i].classList.add(className);
          }
          return this; //使用this优化
      },
      find(selector){
          let arr=[];
          for(let i=0;i<elements.length;i++){
              const elements2=Array.from(elements[i].queryselectorAll(selector))
              arr=arr.concat(elements2);
          }
          /*  return this 注意这里不能返回this,否则会出错,原因如开头的解释 */

          arr.oldApi=this;	//保存旧的api
          const newApi=jQuery(arr);	//重新使用jQuery封装新的api
          return newApi;		
      },
      end(){
          return this.oldApi;
      }
      oldApi:selector.oldApi,
              .
              .
              .(省略的一些函数)
              .
              .
  }
}
  1. 实现createElement()函数创建单个元素或者多个元素。注意了,能够容纳所有标签的元素为[<template></template>标签](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/template)
window.jQuery=function (selector) {
  let elements;	
  if(typeof selector==='string'){
      if(selector[0]==='<'){
          elements=createElement(selector);
      }
      else{
          elements=document.queryselectorAll('selector')
      }
  }
  else if(selector instanceof Array){
      elements=selector;
  }
  function createElement(string) {
      const container=document.createElement('template');
      container.innerHTML=string.trim();	//去除空格
      return container.content.firstChild;
  }

  return {
      addClass(className){
          for(let i=0;i<elements.length;i++){
              elements[i].classList.add(className);
          }
          return this; //使用this优化
      },
      find(selector){
          let arr=[];
          for(let i=0;i<elements.length;i++){
              const elements2=Array.from(elements[i].queryselectorAll(selector))
              arr=arr.concat(elements2);
          }
          /*  return this 注意这里不能返回this,否则会出错,原因如开头的解释 */

          arr.oldApi=this;	//保存旧的api
          const newApi=jQuery(arr);	//重新使用jQuery封装新的api
          return newApi;		
      },
      end(){
          return this.oldApi;
      }
      oldApi:selector.oldApi,
              .
              .
              .(省略的一些函数)
              .
              .
  }
}
  1. 命名风格上的提升,我觉得jQuery也挺难拼写的,直接就用$代替吧!
winodw.$=window.jQuery=function(selector){
    .
    .
    .(省略的函数)
    .
    .
}
  1. jQuery.prototype共有属性。其实封装好了之后还是有缺陷的,因为这些api函数都是共有属性,若不将这些共有属性抽出来使用,则会浪费非常多的内存空间,每次调用这个jQuery API就会开辟一个新的空间。这样肯定是得不偿失的。我们只需要在原型链上加上这个jQuery的共有属性即可。
window.$=window.jQuery=function(){
    let elements;	//将elements变成jQuery里面的全局变量,不然let只有在块作用域里面才有用
  if(typeof selector==='string'){
      if(selector[0]==='<'){
          elements=createElement(selector);
      }
      else{
          elements=document.queryselectorAll('selector')
      }
  }
  else if(selector instanceof Array){
      elements=selector;
  }
  function createElement(string) {
      const container=document.createElement('template');
      container.innerHTML=string.trim();	//去除空格
      return container.content.firstChild;
  }

  const api=Object.create(jQuery.prototype);	// 规范里的书写使用create来更改原型链的指向 等价于 api.__proto__=jQuery.prototype。

  Object.assign(api,{
      elements:elements,
      oldApi:selector.oldApi
  });	//批量赋值

  return api;
}

<!-- 加上jQuery.fn。这是jQuery源码的一句,我其实没有理解到,可能是为了某些理解或好写避免prototype字太长了 --> 
jQuery.fn=jQuery.prototype={
    constructor:jQuery,
    jQuery:true;
    .
    .
    .(省略的共有函数属性)
    .
    .
}

六、总结

通过上面的例子,我从中收获并且了解了jQuery的经典代码和封装技巧,以及如何使用原型链来节约内存,放置共有属性等等。站在巨人的肩膀上眺望远方,我们不仅能收获很多,更能给我们跟多的思考和启迪,希望以上对jQuery的一些理解也能帮助到你们!

这次浅析jQuery的gitbuh源代码