引言:2019年,react hooks成功上位,vue3.0发布alpha版,TS使用率的飞速增长,以及大量前端开发工具使用体验的大幅优化和提高等等让越来越多的开发者吐槽前端学不动了的时候,最好的应对方式便是对基础概念的掌握。内功足够强大,才能做到不被别人牵着鼻子走。阅读开源代码是一个很好的方式,首先率选择了jQuery便是里面的内容没有太多足够抽象的设计思想。更多的是对于基础内容的覆盖。同时也包含一些不错但设计模式在里面,因此具有不错的性价比。
jQuery是早期前端开发中占比很重的一个库。在手动操作DOM和浏览器差异较大的时代,jQuery通过统一和简化不同浏览器之间的API,为程序开发带了极大的便利。所以jQuery的设计思路也是围绕这两点展开的。
ps: 不做特殊说明,$
在源码示例中等效jQuery
。
jQuery实际采用面向对象的方式进行程序开发。jQuery
本身是构造函数。
jQuery('body').constructor === jQuery // true
jQuery('body').addClass === jQuery.prototype.addClass // true
// 因为 jQuery('body')的constructor和 addClass方法分别指向 jQuery本身和jQuery.prototype上的addClass方法,
// 所以jQuery('body')返回的对象实际上就是jQuery构造函数生成的实例
但是在js中生成实例一般使用new
操作符,而jQuery一般的写法是$()
。这里其实是通过某种技巧省略了new
操作符。首先有无new
,生成的实例都是等效的。
(new jQuery('body')).constructor === jQuery('body').constructor // true
(new jQuery('body')).__proto__ === jQuery('body').__proto__ // true
// 这就证实了 有无new操作符,返回的结果是等效的。
这样设计有一个好处是让构造jQuery对象更加方便。
那它的实现方式呢, 看一眼jQuery
函数的定义:
jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context );
};
我们发现jQuery方法返回的实际是 jQuery.fn.init
的实例。同时,我们为了让生成的实例继承jQuery.prototype上的方法,还需要添加一行代码:
jQuery.fn.init.prototype = jQuery.prototype;
关于js中构造函数和prototype的更多内容可以查阅其它资料。
关于new
操作符,我们都知道在构造 函数没有指定return对象的时候,会返回this
本身。如果我们在无new
时,显式指定return对象为this(return this;
),是不是也等效于new呢?
答案: 不是。
这里和函数中this的指向有关。一个函数或方法在执行时,内部的this指向分为四个来源
已经确定jQuery的开发采用面向对象的方式。而面向对象的两个基本要素: 封装和继承。
封装定义一个实例如何组装完成,继承定义多个实例间会共享的内容(行为)。
$.fn.init
方法做了jQuery对象的封装工作,通过一个简单的$()
工厂函数调用。在init方法中,将一切可能的输入源封装为jQuery对象。
有个需要特别说明的地方是,$()
方法除了接收普通的DOM对象或HTML字符串作为输入源返回一个jQuery对象外,还支持接收函数。这也是个语法糖,意思是在document ready的时候,调用这个函数。这么没有什么特别的目的,就是为了非常方便地定义一些在document ready执行的逻辑。因为在实际业务中,你的代码执行的时候可能还有很多元素未加载。
jQuery对象的继承基于js的原型对象完成。所有的jQuery对象都共享$.prototype对象上的方法。同时jQuery给自身添加了extend()
方法用于对象的扩展。那么,也同样可以用于扩展自身的prototype对象,从而实现功能的扩展。这也是jQuery插件实现的基本原理。
需要预先明确的点: jQuery.prototype === jQuery.fn
。 这有什么用? 手敲代码的时候快一些。
jQuery中许多地方用到了钩子思想,主要是用于处理浏览器的兼容性问题。在事件处理和css样式设置中的体现尤为明显。
事件处理包含绑定,分发和删除三部分业务。jQuery中所有的事件(包括自定义事件)都会通过这三个方法进行处理。如果遇到自定义事件或者需要兼容性处理等特殊情况,会通过jQuery.event.special处理。
jQuery.event.special实现的基础是jQuery对浏览器的事件做了代理,所有在业务上需要绑定到元素事件的逻辑,最终都会交给一个统一的方法。这个方法通过原生API绑定到元素上,然后在事件被触发时,此方法根据事件的上下文进行业务逻辑的分发。
jQuery对事件的绑定最终都收缩到jQuery.event.add
方法中,不论对外暴露的API是on()
或者one()
。 同时在模块内部也有一个on
方法,这个方法同样起到函数重载的作用,将参数处理成规范形式然后提交给jQuery.event.add
方法进行事件绑定操作。
jQuery.event.add
这个方法很有意思,它并没有直接把处理方法直接通过原生绑定方法绑定处理事件到元素上面。而是将EventHandler作为数据存到元素本身(存储的实现参考Data.js),如果元素对同一类型绑定了多个事件,这些事件会以数组的形式存在。如果没有把handler直接帮到元素的事件上面,那么如何在事件触发时,调起这些逻辑?其实是绑定了一个调度器,这个调度器会在事件触发时,将存储元素本身的方法逐一取出执行。
这是对于浏览器支持的普通事件的处理方式,如果是自定义事件呢?
答案就是 jQuery.event.special。
假如在执行自定义事件customEvent
绑定的逻辑时,jQuery首先检查jQuery.event.special.customEvent
是否存在。如果存在的话,会走jQuery.event.special.customEvent
中定义的逻辑。 这个对象一般包含四个方法: setup
, add
, teardown
, ‘remove’。作用于事件处理中不同生命周期。通过special对事件处理逻辑做拦截,在此基础上可以实现对原生事件行为的重写或者添加自定义事件。
如果不使用special,那么如何处理兼容性问题。if…else ? 写出来的逻辑成了面条式的代码。在事件处理中,包含三个基本要素: 绑定,解绑和分发。针对同一个事件兼容性处理,可能需要在这三个处理方法中分别添加兼容性业务的处理。这样一来写出来的逻辑必然十分繁杂。如果我们以事件为单位,定义各自的三种逻辑,然后交给程序在合适的时间调起。这样一来,业务会清晰很多。
setup: 给该元素第一次绑定该事件时调用;
teardown: 给该元素解绑该事件最后一个handler时调用;
add: 给该元素添加handler;
remove: 给该元素移除handler;
handler: 当dispatch该事件的时候调用;
_default: 给该事件添加默认行为;
如果setup/teardown 返回false,那么会执行jQuery的bind/unbind方法(通过DOM native API)
关于css样式相关方法的hooks是以jQuery.cssHooks
存在的,分为 get
和 set
。
. a j a x 允 许 接 收 ‘ d a t a T y p e : j s o n p ‘ , 但 是 我 们 知 道 ‘ j s o n p ‘ 是 通 过 ‘ < s c r i p t > ‘ 脚 本 实 现 的 跨 域 请 求 , 它 不 能 通 过 X M L H t t p R e q u e s t 发 送 。 那 么 .ajax允许接收`dataType:jsonp`,但是我们知道`jsonp`是通过`