1

jQuery诞生记-原理与机制

 3 years ago
source link: https://www.zhangxinxu.com/wordpress/2013/07/jquery-%e5%8e%9f%e7%90%86-%e6%9c%ba%e5%88%b6/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

by zhangxinxu from http://www.zhangxinxu.com
本文地址:http://www.zhangxinxu.com/wordpress/?p=3520

一、看似偶然的东西实际是必然会发生的

我大学时候在图书馆翻过一本很破旧的书,讲生物理论的,主要内容就是探讨生命的产生是偶然还是必然。里面很多亚里士多德都看不懂的公式计算什么的,还有模拟原始地球环境出现了有机物的实验什么的 183.gif。总之,书论述的观点是:“在当时的地球环境下,生命的产生是必然的!” 无数次机会的偶然条件、无数次化合物的相遇反应等必定会产生有机物,再有N多偶然,有机物必然形成了有机体……

这种理论类似于,你是个过马路非常小心的人,且你万寿无疆,除了怕被汽车撞。给你100万年的寿命,你最后必然还是被车撞死。

如果以这种理论来看jQuery的出现,结论也应该是必然的

二、需求、动力、发展、事物产生与jQuery的诞生

刀削面机器人

一个成熟的东西显然不是一口气就出来的,所谓“一铲子挖不了一口井”,我想jQuery的原作者再天才,也是循序渐进过来的,如何个循序渐进法,我想,很有可能就是需求驱动而产生的,好比上图刀削面机器人,据说现在已经第八代了!

1. gelElementById太长了
页面上有个按钮,还有个图片,我想点击按钮图片隐藏,如下HTML:

<button id="button">点击我</button>
<img id="image" src="xxx.jpg">

于是,我的脚本可能就这样:

var button = document.getElementById("button")
    , image = document.getElementById("image")

button.onclick = function() {
    image.style.display = "none";
};

有何问题?人几乎都是天生的“懒惰者”,document.getElementById名称长且重复出现,好像到了公司发现卡没带又回家重新拿卡的感觉,我希望越简单越好。恩,sk_thumb.gif 我很喜欢钱,$这个符号我很喜欢,我打算改造一番,简化我的工作:

var $ = function(id) {
    return document.getElementById(id);
};

$("button").onclick = function() {
    $("image").style.display = "none";
};

这里的$()就是最简单的包装器,只是返回的是原生的DOM对象。

2. 我需要一个简洁的暗号,就像“芝麻开门”
后来页面复杂了,点击一个按钮,有2张图片要隐藏。

$("button").onclick = function() {
    $("image1").style.display = "none";
    $("image2").style.display = "none";
};

好像又看见长长的重复的东西,xxx.style.display = "none", 为什么每次开门都要从包里找到钥匙、对准插口插进去、还要左扭扭右扭扭呢?一次还好,天天经常老是这样怎受得了。设想,要是有个芝麻开门的暗号就好了,“open开”,声音识别,门自动开了,多省心。

这里每次隐藏都要xxx.style.display = "none", 比每天拿钥匙开门还要烦,我希望有一个快捷的方式,例如,“hide隐”,语句识别,元素自动隐藏,多省心。

就是要变成下面的效果:

$("button").onclick = function() {
    $("image1").hide();
    $("image2").hide();
};

3. 如何识别“芝麻开门”的暗号
$("image1")本质是个DOM元素,$("image1").hide()也就是在DOM元素上扩展一个hide方法,调用即隐藏。

哦,扩展,立马想到了JS中的prototype原型。//zxx: 老板,现在满大街什么菜最便宜。老板:原型啊,都泛滥了!

HTMLElement.prototype.hide = function() {
    this.style.display = "none";
};

上面代码的demo地址应该不会被人看到吧……

虽然在身体上钻了个窟窿插进入了一个方法,毕竟浏览器有有效果啊,切肤之痛就不算什么了。但是,我们是在泱泱天朝,很多IE6~IE8老顽固,这些老东西不认识HTMLElement,对于HTMLElement自残扩展之法根本理解不了,而这些老家伙掌管了半壁江山。唉,面对现实,元素直接扩展是行不通了。

因此,由于兼容性,我们需要想其他扩展方法。

4. 条条大路通罗马,此处不留爷,自有留爷处
虽IE6~IE8不识HTMLElement原型扩展,但是,Function的原型扩展其认识啊。管不管用,暂时不得而知,先随便搞个简单的试试呗~

var F = function() {};
F.prototype.hide = function() {
    this?.style.display = "none";
};

new F().hide();    // 这个实现隐藏?

本文至少还有一半的内容,但是,全文的最难点就在这里的,对new F()的认识和理解。

上面的代码,new F()您可以看做是this?.style这里的this. 您可能会跳起来抢答道:“那new F()return值 = DOM元素不就完事OK啦!—— this.style.hide = new F().style.hide = DOM.style.hide”!

很傻很天真

只要new表达式之后的constructor返回(return)一个引用对象(数组,对象,函数等),都将覆盖new创建的匿名对象,如果返回(return)一个原始类型(无return时其实为return原始类型undefined),那么就返回new创建的匿名对象。

上面的引用来自这里。什么意思呢?说白了就是,new F()如果没有返回值(Undefined类型),或返回值是5种基本型(Undefined类型、Null类型、Boolean类型、Number类型、String类型)之一,则new F()我们可以看成是原型扩展方法中的this; 如果返回是是数组啊、对象啊什么的,则返回值就是这些对象本身,此时new F()this

举例说明:

var F = function(id) {
    return document.getElementById(id);
};

new F("image1") == document.getElementById("image1");    // true 说明看上去返回DOM对象,实际确实就是DOM对象
var F = function(id) {
    return id;
};

new F("image1") == "image1";    // false 说明看上去返回字符串值,实际并不是字符串

回到上面天真的想法。要想使用prototype.hide方法中的this, 偶们就不能让F函数有乱七八糟的返回值。

因此,new F()直接返回DOM是不可取的,但我们可以借助this间接调用。比方说:

var F = function(id) {
    this.element = document.getElementById(id);
};
F.prototype.hide = function() {
    this.element.style.display = "none";
};

new F("image").hide();    // 看你还不隐藏

上面代码的demo地址应该不会被人看到吧……

5. 暴露与重用元素获取方法
上面的方法,元素的获取直接在F方法中,但是,实际情况,考虑到兼容性实现,元素获取可能会相当复杂,同时方法私有,不能重利用。因此,可以把元素获取方法放在原型上,便于管理和重用。代码如下:

var F = function(id) {
    return this.getElementById(id);
};
F.prototype.getElementById = function(id) {
    this.element = document.getElementById(id);
    return this;
};
F.prototype.hide = function() {
    this.element.style.display = "none";
};

new F("image").hide();    // 看你还不隐藏

元素获取方法放在prototype上,通过F()执行。你可能会奇怪了,你刚明明说“new F()直接返回DOM是不可取的”,怎么现在又有return呢?大家务必擦亮眼睛,F.prototype.getElementById的返回值是this,也就是new F()的返回值是this. 形象点就是new F("image")出了一拳,又反弹到自己脸上了。

上面代码的demo地址应该不会被人看到吧……

6. 我不喜欢new, 我喜欢$
new F("image")这种写法我好不喜欢,我喜欢$, 我就是喜欢$, 我要换掉。

好吧,把new什么什么藏在$方法中把~

var $ = function(id) {
    return new F(id);
};

于是,上面的图片隐藏的直接执行代码就是:

$("image").hide();

上面代码的demo地址应该不会被人看到吧……

IE6浏览器也是支持的哦!是不是已经有些jQuery的样子啦!

7. 你怎么就一种姿势啊,人家都腻了诶
循序渐进到现在,都是拿id来举例的,实际应用,我们可能要使用类名啊,标签名啊什么的,现在,为了接下来的继续,有必要支持多个“姿势”。

在IE8+浏览器中,我们有选择器API,document.querySelectordocument.querySelectorAll,前者返回唯一Node,后者为NodeList集合。大统一起见,我们使用后者。于是,就有:

var F = function(selector, context) {
    return this.getNodeList(selector, context);
};
F.prototype.getNodeList = function(selector, context) {
    context = context || document;
    this.element = context.querySelectorAll(selector);
    return this;
};

var $ = function(selector, context) {
    return new F(selector, context);
};

此时,我们就可以使用各种选择器了,例如,$("body #image"), this.element就是选择的元素们。

8. IE6/IE7肿么办?
IE6/IE7不认识querySelectorAll,咋办?
怎么办?

jQuery就使用了一个比较强大的选择器框架-Sizzle. 知道就好,重在演示原理,因此,下面还是使用原生的选择器API示意,故demo效果需要IE8+浏览器下查看。

8. 遍历是个麻烦事
this.element此时类型是NodeList, 因此,直接this.element.style.xxx的做法一定是报错,看来有必要循环下:

F.prototype.hide = function() {
    var i=0, length = this.element.length;
    for (; i<length; i+=1) {
        this.element[i].style.display = "none";
    }    
};
$("img").hide();  // 页面所有图片都隐藏啦!

上面代码的demo地址应该不会被人看到吧……

单纯一个hide方法还可以应付,再来个show方法,岂不是还要循环遍历一次,岂不是要烦死~

因此,急需一个遍历包装器元素的方法,姑且叫做each吧~

F.prototype.each = function(fn) {
    var i=0, length = this.element.length;
    for (; i<length; i+=1) {
        fn.call(this.element[i], i, this.element[i]);
    }
    return this;
};
F.prototype.hide = function() {
    this.each(function() {
       this.style.display = "none";
    });
};

$("img").hide();  // 页面所有图片都隐藏啦!

上面代码的demo地址应该不会被人看到吧……

9. 我不喜欢this.element, 可以去掉吗?
现在包装器对象结构类似这样:

F.prototype = {
    element: [NodeList],
    each: function() {},
    hide: function() {}
}

element看上去好碍眼,就不能去掉吗?可以啊,宝贝,NodeList是个类数组结构,我们把它以数值索引形式分配到对象中就好啦!一来去除冗余element属性,二来让原型对象成为类数组结构,可以有一些特殊的功能。

于是,F.prototype.getNodeList需要换一个名字了,比方说初始化init, 于是有:

F.prototype.init = function(selector, context) {
    var nodeList = (context || document).querySelectorAll(selector);
    this.length = nodeList.length;
    for (var i=0; i<this.length; i+=1) {
        this[i] = nodeList[i];    
    }
    return this;
};

此时,each方法中,就没有烦人碍眼的this.element[i]出现了,而是直接的this[i].

F.prototype.each = function(fn) {
    var i=0, length = this.length;
    for (; i<length; i+=1) {
        fn.call(this[i], i, this[i]);
    }
    return this;
};

我们也可以直接使用索引访问包装器中的DOM元素。例如:$("img")[0]就是第一张图片啦!

上面代码的demo地址应该不会被人看到吧……

10. 我是完美主义者,我特不喜欢F名称,可以换掉吗?
F这个名称从头到尾出现,我好不喜欢的来,我要换成$, 我就是要换成$符号……

这个……$已经用了啊,再用冲突的吧。再说,你又不是狐后,耍无赖也没用啊……

狐后耍无赖

御姐发飙了

好吧,想想其他办法吧。一步一步来,那我把所有的F换成$.fn.

就有:

所有的F换成$.fn之后~

上图代码的demo地址应该不会被人看到吧……

显然,运行是OK的。似乎也非常有jQuery的模样了,但是,实际上,跟jQuery比还是有差别的,有个较大的差别。如果是上图代码所示的JS结构,则包装器对象要扩展新方法,每个都需要再写一个原型的。例如,扩展一个attr方法,则要写成:

$.fn.prototype.attr = function() {
    // ...
};

又看到prototype了,高级的东西应该要隐藏住,否则会给人难以上手的感觉。那该怎么办呢?御姐不是好惹的。

脑子动一下就知道了,把F.prototype换成$.fn不久好了。这样,扩展新方法的时候,直接就是

$.fn.attr = function() {
    // ...
};

至此,就使用上讲,与jQuery非常接近了。

但是,还有几个F怎么办呢,总不能就像下面这样放着吧:

var $ = function(selector, context) {
    return new F(selector, context);
};
var F = function(selector, context) {
    return this.init(selector, context);
};

$.fn = F.prototype;

$.fn.init = function(selector, context) {
    // ...
    return this;
};
$.fn.each = function(fn) {
   // ...
};
$.fn.hide = function() {
   // ...
};

数学中,我们都学过合并同类项。仔细观察上面的的代码:
$()返回的是new F(),而new F()又是返回的对象的引用。擦,这返回来返回去的,参数又是一样的,我们是不是可以一次性返回,然后再做些手脚,让$.fn.init返回的this依然能够正确指向。

于是,一番调整有:

var $ = function(selector, context) {
    return new $.fn.init(selector, context);
};
var F = function() { };

$.fn = F.prototype;
$.fn.init = function(selector, context) {
    // ...
    return this;
};

// ...

上面代码显然是有问题的,new的是$.fn.init, $.fn.init的返回值是this. 也就是$()的返回值是$.fn.init的原型对象,尼玛$.fn.initprototype原型现在就是个光杆司令啊,哟,正好,$.fn对应的原型方法,除了init没用外,其他hide(), each()就是我们需要的。因此,我们需要加上这么一行:

$.fn.init.prototype = $.fn

于是,$()的返回值从$.fn.init.prototype一下子变成$.fn,正好就是我们一开始的扩展方法。

于是乎,大功告成。慢着……

慢着……

上面明明还有残留的F呢!

哦,那个啊。F是任意函数,$本身就是函数,因此,直接使用$替换就可以了:

var $ = function(selector, context) {
    return new $.fn.init(selector, context);
};
var F = function() { };   // 这个直接删除
$.fn = $.prototype;
$.fn.init = function(selector, context) {
    // ...
    return this;
};

// ...

上图代码的demo地址应该不会被人看到吧……

实际上,如果你不是非得一个$行便天下的话,到了上面进阶第9步就足够了。jQuery在第10步的处理是为了彰显其$用得如此的出神入化,代码完美,令人惊叹!

至此,jQuery大核心已经一步一步走完了,可以看到,所有的这些进阶都是根据需求、实际开发需要来的,慢慢完善,慢慢扩充的!

11. 每个扩展方法都要$.fn.xxxx, 好闹心的来

$.fn.css = function() {}
$.fn.attr = function() {}
$.fn.data = function() {}
// ...

每个扩展前面都有个$.fn, 好讨厌的感觉,就不能合并吗?

于是,jQuery搞了个extend方法。

$.fn.extend({
    css: function() {},
    attr: function() {},
    data: function() {},
    // ...
});

12. $()不仅可以是选择器字符串,还可以是DOM
init方法中,判断第一个参数,如果是节点,直接this[0] = this_node. over!

以下13~?都是完善啊,补充啊,兼容性处理啊什么的,没有价值,到此为止!

三、排了很长队的结束语

网上也有其他一些介绍jQuery原理或机制的文章,可能当事人自己理解,而阅读者本来就不懂,说来说去,越说越绕,可能更不懂了。

jQuery是很优秀,好比身为灵长类的人类。但是,其诞生显然是从简单开始的。因此,要了解人类,可以通过追溯其起源。如果你是上帝,要让你造一个人,你会怎么造,是一口气出来?女娲造人还要捏泥人呢!不妨从单细胞生物开始,随着自然进化,淘汰,自然而然,就会出现人类,上帝他就是这么干的。

jQuery的诞生也大致如此,要想了解jQuery,可以试试踏着本文jQuery的成长足迹,一点一点逐步深入,您就会了解为何jQuery要这么设计,它是如何设计的等。

虽然,内容由浅及深,但是,其中涉及的原型以及new构造函数的一些特性,对于新人而言,还是有一些理解门槛的,希望我的描述与解释可以让你有一丝豁然开朗,那就再好不过了。

感谢您的阅读至此,欢迎指出文章可能书写不准确的地方,再次感谢!

月底在百姓网有个小分享,演示文档连个肉渣子还没准备呢。因此,未来一周休文。

(本篇完)1f44d.svg 是不是学到了很多?可以分享到微信
1f44a.svg 有话要说?点击这里


Recommend

  • 2

    《浏览器工作原理与实践》笔记之HTTP诞生到HTTP 1.1如果您发现本文排版有问题,可以先点击...

  • 30

    因为ZK有watch机制,可以随时发现一些数据的变化,从而达到数据的及时性。 ZK的所有读操作都可以设置watch监视点: getData, getChildren, exists. 写操作则是不能设置监视点的。 监视有两种类型:数据监视点和子节点监视点...

  • 9

    前言 首先看一个Android界面的布局层次结构,最直观的看一下: 我们能清晰看到,这个界面分成了3部分:顶部状态栏...

  • 9
    • zhuanlan.zhihu.com 3 years ago
    • Cache

    Martian-cloud 传染机制的原理 - 知乎

  • 4
    • segmentfault.com 3 years ago
    • Cache

    深入React合成事件机制原理

    点击进入React源码调试仓库。为什么要自己实现一套事件机制由于fiber机制的特点,生成一个fiber节点时,它对应的dom节点有可能还未挂载,onClick这样的事件处理函数作为...

  • 7

    本书部分摘自《Java 并发编程的艺术》 概述 相信大家都很熟悉如何使用 Java 编写处理并发的代码,也知道 Java 代码在编译后变成 Class 字节码,字节码被类加载器加载到 JVM 里,JVM 执行字节码,最终需要转化为汇编指令在 CPU...

  • 8

    了解HTTP/3 协议的原理、机制、优势、配置和实测 2 HTTP 协议经过发展,目前 HTTP2.0 作为主流 HTTP 协议,已经得到一定普及,虽然国内仍然有很多连 HTTPS 都没上的网站,但不影响 HTTP 协议的发展。 HTTP2....

  • 5
    • segmentfault.com 3 years ago
    • Cache

    Flutter的渲染机制和原理

    1. Flutter渲染机制之三棵树在Flutter中和Widgets一起协同工作的还有另外两个伙伴:Elements和RenderObjects;由于它们都是有着树形结构,所以经常会称它们为三棵树。Widget:Widget是Flutter的核心部分,是用户界面的不可变描述。做Flutt...

  • 6

    白话比特币和它的原理机制 比特币的通俗小故事 从前,有个古老的村落,里面住着一群古老的村民,这个村庄没有银行为大家存钱、记账。没有一个让所有村民都信赖的村长来维护和记录村民之间的账务往来,也就是没有任何中间机构或个人来记账。于...

  • 7

    从内存管理原理,窥探OS内存管理机制 推荐 原创 华为云开发者社区 2022-01-11 09:54:52...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK