引言:2019年,react hooks成功上位,vue3.0发布alpha版,TS使用率的飞速增加,以及大量前端开发工具使用体验的大幅优化和提升等等让愈来愈多的开发者吐槽前端学不动了的时候,最好的应对方式即是对基础概念的掌握。内功足够强大,才能作到不被别人牵着鼻子走。阅读开源代码是一个很好的方式,首先率选择了jQuery即是里面的内容没有太多足够抽象的设计思想。更多的是对于基础内容的覆盖。同时也包含一些不错但设计模式在里面,所以具备不错的性价比。
jQuery是早期前端开发中占比很重的一个库。在手动操做DOM和浏览器差别较大的时代,jQuery经过统一和简化不一样浏览器之间的API,为程序开发带了极大的便利。因此jQuery的设计思路也是围绕这两点展开的。css
ps: 不作特殊说明,$
在源码示例中等效jQuery
。前端
API设计的特色 ————> 函数重载vue
jQuery实际采用面向对象的方式进行程序开发。jQuery
自己是构造函数。react
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
,生成的实例都是等效的。jquery
(new jQuery('body')).constructor === jQuery('body').constructor // true (new jQuery('body')).__proto__ === jQuery('body').__proto__ // true // 这就证明了 有无new操做符,返回的结果是等效的。
这样设计有一个好处是让构造jQuery对象更加方便。ajax
那它的实现方式呢, 看一眼jQuery
函数的定义:编程
jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context ); };
咱们发现jQuery方法返回的实际是 jQuery.fn.init
的实例。同时,咱们为了让生成的实例继承jQuery.prototype上的方法,还须要添加一行代码:json
jQuery.fn.init.prototype = jQuery.prototype;
关于js中构造函数和prototype的更多内容能够查阅其它资料。segmentfault
关于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
。
$.ajax容许接收dataType:jsonp
,可是咱们知道jsonp
是经过<script>
脚本实现的跨域请求,它不能经过XMLHttpRequest发送。那么$.ajax有什么特别的处理么?
prefilters和transports。
这也是ajax能够自定义dataType的关键点,原理跟event.special 相似。
jQuery本身实现了一个Callbacks方法,用于管理回调,主要是为了提供给本身的defferred、ajax和animation使用。
实现基于观察者模式,对外暴露 add,remove,fire这几个API方法。 除了这三个方法,是没法在外部直接修改回调list和执行状态firing等数据的,经过闭包来实现。
同时提供了回调函数上下文的设置接口(fireWith)。
jQuery的设计思路就是找到页面上的一些元素并执行一些操做。其中负责“找”的即是selector。而这一部分最终成为一个独立项目Sizzle。
Sizzle做为查找器引擎,基于函数式编程的思路进行开发。基本的思路是将输入(selector字符串)转化为输出结果(与selector match的元素),不对输入数据作任何变动,经过不一样的输入数据生成不一样的函数而后执行最终函数得到目标数据。
Sizzle在转换selector的中间过程当中,还对生成的函数进行缓存,进而在下次遇到相同的输入时,能够直接返回以前已经生成过的函数,从而得到性能的提高。
Sizzle自己实现了一个小型的compilor。为何这么说,在早先浏览器不支持querySelector/querySelectorAll
的时代,想想':first', " p ~ p"等之类的元素查找。这种写法暗含了上下文相关。传统的getElementsByTagName
方法必然包含了大量的回溯操做。这对于开发者是极为不便利的,jQuery封装了这些操做。这可能也是为何当时能够快速流行并成为js中最流行的库的缘由。
在经过查找引擎Sizzle找到目标元素后,就能够对元素执行一些操做。
在jQuery中,咱们都知道进行DOM操做能够采用链式写法,好比像下面这样对document.body进行操做:
$('body').addClass('foo').find('div').remove().end().addClass('bar')
那么若是不采用链式写法呢,会有什么样的结果,看下面
$('body').addClass('foo'); $('body').find('div').remove(); $('body').addClass('bar');
因此,一目了然~~~
这样在进行DOM操做时,手写代码带来的便利性是显而易见的。实现这种写法的机制也很简单,就是在每一次操做以后,都返回对象自身return this
。
可是,若是某个方法须要返回操做结果或者其它数据,那么这时候链式操做就没法知足了。
jQuery中存在许多函数重载。咱们知道函数重载是在函数名相同的前提下,根据参数类型或个数来区分不一样的处理。那么函数重载在jQuery中有什么意义呢?
$('body').css('width') $('body').css('width', '800px') $('body').css({ 'color': 'red', 'border': '10px solid blue' })
很明显,css()是方法是被重载的函数。那若是不对css进行重载呢,想像一下,若是实现上面的功能应该怎么设计程序。可能须要设计get/set方法或者针对每种参数类型都写一个方法。那么对外暴露的API就不只仅是一个css()了。API的繁可能是会增长使用者的学习成本的。
可是函数重载也不是只好不坏,增长了程序的复杂性。在jQuery中,存在一些单纯的normalize参数的方法。这样让开发者没法第一眼就知道最终调用的是哪一个方法。这是对开发者而言,对于计算机,函数重载也可能会增长程序的消费。
关于函数重载,在jQuery的event处理中,获得了更明显的使用。绑定实践最终会调用jQuery.event.add方法,可是在这以前,会先走on()方法,这个方法主要的做用就是规范函数的参数。
函数重载最终的效果的经过参数个数或者参数类型,类区分不一样的处理方案, 减小了对外暴露的API数量。可是函数重载的基础是在不一样方案之间概念相近的状况下,才建议采用函数重载。这样对于使用者而言,也是清晰明确的。若是你把jQuery的find的方法重载到jQuery.css中,那谁能够一眼看出find方法在哪一个API中呢。
举几个例子:
style()
方法;css()
方法;animate()
方法;domManip()
方法;那有个问题:何时该收,何时该放? 答:找到业务中的关键节点,而后在关键节点上作好覆盖面比较广的把控。
动画都会以队列的形式执行,默认队列是fx
,那么fx是如何实现的? 一个队列应该具备自执行的特色,将处理方法以数组的形式存储,而后再执行出栈时,给每一个方法添加一个钩子,钩住下一个要执行的方法,在执行完后调用下一个方法。
这个模式跟compose很像,将多个函数合并成一个函数执行。Koa和Redux的核心概念实现即是基于函数的compose。
jQuery从2005年发行至今(2019年12月),仍然在生产环境中占据一席之地的缘由?
运行时负载是如今React/Vue等框架随着业务功能的逐渐强大,也难以免,最终总会有一个天花板存在。也所以,有人搞出了无运行时负载的框架Svelte。Vue3更是强调本身的运行时性能是2.x的一倍,一部分提高得益于用Proxy替换了Object.defineProperty,另外一部分则是静态编译时作的性能优化。因此对于框架或者库的设计,这也是应该考虑的一方面问题。
jQuery.event实现的基本原理demo special-events
jquery-edge-new-special-event-hooks
还需继续努力~~
完!!!
本文参与了 SegmentFault思否征文「2019 总结」,欢迎正在阅读的你也加入。