阅读 8069

源码篇(四):手写jQuery版mini源码分析jQuery的优势。附送简版jQuery源码

适合人群

本文适合0.5~3年的前端开发人员,以及想了解jQuery是什么的小伙伴们。

前言

谈谈个人对jQuery的看法,无兴趣可直接看源码分析。

如果你是一个五年以上的开发人员,相信你一定认识了解jQuery。这好比你十年前就已经有手机,那你肯定认识了解诺基亚。

当今的jQuery的确是没落了,逊色于三大框架,他的确落寞了。落寞到什么层度?

落寞到,你面试时你提起他,面试官会觉得你没有跟上时代;

落寞到,你写项目的用上jQuery,别人会觉得你的项目非常的low;

落寞到,掘金都懒得给他一个专题了(要是十年前肯定是个特殊的专区)。

其实不然,jQuery还是有很多可以学习且可以借鉴的地方,这也是笔者考虑再三,把他补上源码篇之一的原因。如果你是追求短期面试高薪,明显jQuery跟本文都不适合你。如果你注重基础,不凡了解一下本文。

如果你入门前端时,已经是三大框架的时代,那可以简单的理解为:jQuery是一个JavaScript函数库,让你的项目相比原生的js, “写的少,做的多”。是的,document.getelementbyid('id').innerhtml = "你好" 用 $('#id').html("你好") 即可实现,这对比我们原生js的来写项目,是带来多大的方便。他有很多优势,具体的优势,下边分析完源码会有汇总。但是不得不先提一下,他的缺陷是什么?淘汰的根本是什么?

笔者的观点是:虚拟dom的出现是淘汰jQuery的根本。jQuery的简写,还是用法是无脑操作dom。当多次需要渲染同一个dom时,你直接操作的dom(因为渲染了多次),他的效率就远远比不上"直接操作js,算好结果再同步dom"(因为只渲染了最终的一次)。虚拟dom的出现,让我们的前端,直接从mvc结构转换成mvvm结构,三大框架都是数据驱动,而jQuery依然是操作dom的写法,不得不给淘汰。这好比诺基亚想要在其他方面拯救自己,如果不抛弃塞班更换流行系统,将有心无力,最后给无情的抛弃。

手写mini_jquery

基本页面

我们新建一个html,写好jQuery的常见写法:

<html>
    <head>
        <title>zQuery源码</title>
        <meta name="keywords" content="shopInfo.shopName?if_exists" />
        <meta name="description" content="shopInfo.shopName?if_exists" />
        <meta http-equiv="X-UA-Compatible" content="IE=8">
        </meta>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    </head>
    <body>
        <script src="src/zQuery.js"></script>
    </body>
</html>
复制代码

再新建zQuery.js引入。

全局注册

用过jquery的小伙伴们,都知道我们的?全局哪里都可以使用。引入jQuery后,$是在全局上注册了。我们看看官方是如何处理:

缩略代码,即为上图。我们可以看到,引入jQuery后即调用了,实际上就是利用理解调用函数 (function(){})(),将jQuery暴露在全局中。

那么有没有人思考过?为什么截图中,会有一个module的判断呢?其实那是后续有了node环境,也有部分人需要在node用到jQuery,node是没有dom页面的,添加的判断。我们的案例暂时不需要考虑这些。我们手写一个注册:

(function (window) {
    var zQuery = function () {
        return new Object();
    }
    window.$ = zQuery;
})(window);
复制代码

获取对象

即完成引入注册,全局注册。但是返回的对象到底是个啥?我们都知道$("")返回的是个dom对象,可以根据id获取,也可以根据标签名称,根据类名等等。

  • 首先他要根据()里的值,获取dom对象,
  • 支持id, class, 标签的获取。

我们根据两个要点,我们给他定义一个初始化方法,且新建时候立即调用,修改一下他的返回值:

(function (window) {
    var zQuery = function () {
        return new zQuery.fn.init(selector);
    }
    window.$ = zQuery;
    
     zQuery.fn = {
        init: function (selector) {
            this.dom = [];
            const childNodes = document.childNodes;
            var rs = null;
            if (typeof (selector) != 'undefined') {
                if (selector.substr(0, 1) == "#") {//id选择器
                    rs = document.getElementById(selector.slice(1));
                    this.dom[0] = rs;
                } else if (selector.substr(0, 1) == ".") { //样式选择器
                    rs = document.getElementsByClassName(selector.slice(1));
                    for (var i = 0; i < rs.length; i++) {
                        this.dom[i] = rs[i];
                    }
                } else {//标签选择器
                    rs = document.getElementssByTagName();
                    for (var i = 0; i < rs.length; i++) {
                        this.dom[i] = rs[i];
                    }
                }
            }
            return this;
        },
    }
})(window);

这样,即可完成$()的对象获取。
复制代码

操作对象

再拿到我们的dom对象之后,接下来就是我们如何改变他们的问题。常见的是:

  • html改变文本值
  • css改变颜色
  • hide/show隐藏或者显示

以上方法,用原生js实现的话,很简单吧?我们嵌入zQuery.fn中,来实现代码:

    zQuery.fn = {
        ...,
        html: function (value) {
            if (this.dom.length == 1) {
                this.dom[0].innerHTML = value;
            } else if (this.length > 1) {
                for (var i = 0; i < this.dom.length; i++) {
                    this.dom[i].style[attr] = value;
                }
            }
        },
        css: function (attr, value) {
            if (this.dom.length == 1) {
                this.dom[0].style[attr] = value;
            } else if (this.dom.length > 1) {
                for (var i = 0; i < this.dom.length; i++) {
                    this.dom[i].style[attr] = value;
                }
            }
        },
        show: function (attr, value) {
            if (this.dom.length == 1) {
                this.dom[0].style.display = 'block';
            } else if (this.dom.length > 1) {
                for (var i = 0; i < this.dom.length; i++) {
                    this.dom[i].style.display = 'block';
                }
            }
        },
        hide: function (attr, value) {
            if (this.dom.length == 1) {
                this.dom[0].style.display = 'none';
            } else if (this.dom.length > 1) {
                for (var i = 0; i < this.dom.length; i++) {
                    this.dom[i].style.display = 'none';
                }
            }
        },
    }
复制代码

到此,即可完成我们的dom操作。案例如下:

html:
    <div id="htmlId">点击我重新赋值</div>
    <div id="cssId" style="color: black;">点击我变成红色</div>
    <div id="showId">点击我变成隐藏</div>
    <div id="changeId">点击我改变样式选择器</div>
    <div class="cssSelector">样式选择器</div>
    <div class="cssSelector">样式选择器</div>
    <div class="cssSelector">样式选择器</div>
    <div class="cssSelector">样式选择器</div>
    
js:

btnList.addEventListener('click', function (e) {
    var id = e.target.id;
    switch (id) {
        case 'htmlId':
            $('#htmlId').html("html赋值成功");
            break;
        case 'cssId':
            $('#cssId').css("color", "red");
            break;
        case 'showId':
            $("#showId").hide();
            break;
        case 'changeId':
            $(".cssSelector").css("color", "#0099dd");
            break;
        }
    }
});
复制代码

完成链式

jquery的优势之一,就是方便支持链式。那么我们如何来实现他呢?

其实也很好理解,我们只需要将我们找到的dom对象,接下去再寻找下一层dom对象,直到找到结果为止。 我们把第一次的dom对象保存,第二次拿去dom对象,基于第一次的条件去下获取,然后重新初始化即可。 且这个过程,所有的层次关系,多级的dom,内置方法是一致的,我们需要完成原型链的继承。

三个步骤:

  • 1.拿到上次的dom

  • 2.基于上次的dom,获取下一次dom。

  • 3.完成原型链继承

      第一个步骤,我们可以新建一个全局的myDocument表示保存的dom结构。
      zQuery.fn = {
              init: function (selector, myDocument) {
                  this.dom = this.dom ? this.dom : [];
                  this.myDocument = myDocument ? myDocument : document;
                  const childNodes = this.myDocument.childNodes;
                  var rs = null;
                  if (typeof (selector) != 'undefined') {
                      if (selector.substr(0, 1) == "#") {//id选择器
                          rs = this.myDocument.getElementById(selector.slice(1));
                          this.dom[0] = rs;
                          console.log("rs===" + rs.innerText + rs.innerHTML);
                      } else if (selector.substr(0, 1) == ".") { //样式选择器
                          rs = this.myDocument.getElementsByClassName(selector.slice(1));
                          for (var i = 0; i < rs.length; i++) {
                              this.dom[i] = rs[i];
                          }
                      }
                  }
                  return this;
              },
              ...
      }
    
      基于上次的dom,获取下一次dom。
      zQuery.fn = {
          ...
          find: function (selector) {
              if (this.dom.length == 1) {
                  this.init(selector, this.dom[0]);
              } else if (this.dom.length > 1) {
                  for (var i = 0; i < this.dom.length; i++) {
                      this.init(selector, this.dom[i]);
                  }
              }
              return this;
          },
      }
        
      //完成原型链继承
      zQuery.fn.init.prototype = zQuery.fn;
    复制代码

实现案例测试:

html:
<div id="findId">
    点击我触发链式变化:<div class="f_div">我是子内容</div>
</div>

js:
$("#findId").find('.f_div').css("color", "red");
复制代码

网络请求

最后简单的实现jQuery的$.ajax神器。

ajax: function ({ url, dataType, success }) {
        var xhr = new XMLHttpRequest();
        xhr.open(dataType, url);
        xhr.send(null);
        xhr.onreadystatechange = function () {
            if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
                success(xhr.responseText);
            };
        }
},


$().ajax({
    url: 'http://....,
    dataType: 'GET',
        success: function (res) {
            alert("接口返回数据:" + res);
        }
})
复制代码

这样一个简版的jQuery以及实现了。看完简版,我们来分析一下jq的优势吧。

jQuery的优势

jQuery虽然在dom的处理上,成为要害。但是他曾经的火热,说明身上还是很多优点的。我们分析一下:

  • 简洁的api

无论上在选择器上,还是在网络请求上,jQuery都用了几个字母,就实现了我们的长篇代码。链式操作使页面更加简洁。

  • dom操作的封装

jQuery封装了大量常用的DOM操作,使开发者在编写DOM操作相关程序的时候能够得心应手。jQuery轻松地完成各种原本非常复杂的操作,让JavaScript新手也能写出出色的程序。jQuery在操作dom上本身是有优势的,只是我们需要每次都去操作他,不像单页面架构,只更新自己需要更新的部分

  • 小且不污

jQuery本身的引入非常小,最低差不多15K就解决,而且作者的封装,值暴露了$,不会污染全局的变量。

  • 浏览器兼容性

单页的兼容,哪个支持ie10?没记错的话,官方都是声明支持IE11以上吧。

但是jQuery却不需要考虑这样的问题,能够支持IE 6.0+、FF 2+、Safari 2.0+和Opera 9.0+下

  • 丰富的插件支持

在笔者的眼中,jQuery目前为止的生态库,是比其他框架要多,且引入即用。特别是一些门户需要的动画效果的,游戏的效果。他的生态圈比较历史渊源。

  • 自定义插件

jQuery自定义插件,$.fn.extend即可扩展,十分方便。

框架建议

技能上允许时,不要一味的追求框架。三大框架固然是个不错的框架,但是jQuery还是有适合自己存在的地方,例如笔者写门户时,除了服务端渲染外,剩下的喜欢用简单的jQuery去实现。不单单时候门户,jQuery的劣势是什么?就是dom操作无法减少。但是我们本身的需求,就是不需要操作dom,或者少之又少,那就没有缺陷了。所以没有太多交互效果的网站中,依然还是可以使用jQuery。

框架源码

可到github下载完整实例:https://github.com/zhuangweizhan/codeShare

(function (window) {
    var zQuery = function (selector) {
        return new zQuery.fn.init(selector);
    }
    zQuery.fn = {
        init: function (selector, myDocument) {
            this.dom = this.dom ? this.dom : [];
            this.myDocument = myDocument ? myDocument : document;
            const childNodes = this.myDocument.childNodes;
            var rs = null;
            if (typeof (selector) != 'undefined') {
                if (selector.substr(0, 1) == "#") {//id选择器
                    rs = this.myDocument.getElementById(selector.slice(1));
                    this.dom[0] = rs;
                    console.log("rs===" + rs.innerText + rs.innerHTML);
                } else if (selector.substr(0, 1) == ".") { //样式选择器
                    rs = this.myDocument.getElementsByClassName(selector.slice(1));
                    for (var i = 0; i < rs.length; i++) {
                        this.dom[i] = rs[i];
                    }
                }
            }
            return this;
        },
        find: function (selector) {
            if (this.dom.length == 1) {
                this.init(selector, this.dom[0]);
            } else if (this.dom.length > 1) {
                for (var i = 0; i < this.dom.length; i++) {
                    this.init(selector, this.dom[i]);
                }
            }
            return this;
        },
        html: function (value) {
            if (this.dom.length == 1) {
                this.dom[0].innerHTML = value;
            } else if (this.length > 1) {
                for (var i = 0; i < this.dom.length; i++) {
                    this.dom[i].style[attr] = value;
                }
            }
        },
        css: function (attr, value) {
            if (this.dom.length == 1) {
                this.dom[0].style[attr] = value;
            } else if (this.dom.length > 1) {
                for (var i = 0; i < this.dom.length; i++) {
                    this.dom[i].style[attr] = value;
                }
            }
        },
        show: function (attr, value) {
            if (this.dom.length == 1) {
                this.dom[0].style.display = 'block';
            } else if (this.dom.length > 1) {
                for (var i = 0; i < this.dom.length; i++) {
                    this.dom[i].style.display = 'block';
                }
            }
        },
        hide: function (attr, value) {
            if (this.dom.length == 1) {
                this.dom[0].style.display = 'none';
            } else if (this.dom.length > 1) {
                for (var i = 0; i < this.dom.length; i++) {
                    this.dom[i].style.display = 'none';
                }
            }
        },
        //异常请求
        ajax: function ({ url, dataType, success }) {
            var xhr = new XMLHttpRequest();
            xhr.open(dataType, url);
            xhr.send(null);
            xhr.onreadystatechange = function () {
                if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
                    success(xhr.responseText);
                };
            }
        },
    }

    zQuery.fn.init.prototype = zQuery.fn;
    window.$ = zQuery;
})(window);
复制代码

博客进度

| 序号 | 博客主题 | 相关链接 | |-----|------|------------|- | 1 | 手写vue_mini源码解析 | juejin.im/post/684790… | | 2 | 手写react_mini源码解析 | juejin.im/post/685457… | | 3 | 手写webpack_mini源码解析 | juejin.im/post/685457… | | 4 | 手写jquery_mini源码解析(即本文) | juejin.im/post/685457… | | 5 | 手写vuex_mini源码解析 | juejin.im/post/685705… | | 6 | 手写vue_router源码解析 | 预计8月 | | 7 | 手写diff算法源码解析 | 预计8月 | | 8 | 手写promise源码解析 | 预计8月 | | 9 | 手写原生js源码解析(手动实现常见api) | 预计8月 | | 10 | 手写react_redux,fiberd源码解析等 | 待定,本计划先出该文,整理有些难度 | | 11 | 手写koa2_mini | 预计9月,前端优先 |

期间除了除了源码篇,可能会写一两篇优化篇,基础篇等。有兴趣的欢迎持续关注。

下一篇将写vuex的实现。