最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美。
其结构明晰,高内聚、低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷、渐进增强)优雅的处理能力以及 Ajax 等方面周到而强大的定制功能无不令人惊叹。
另外,阅读源码让我接触到了大量底层的知识。对原生JS 、框架设计、代码优化有了全新的认识,接下来将会写一系列关于 jQuery 解析的文章。
我在 github 上关于 jQuery 源码的全文注解,感兴趣的可以围观一下。 jQuery v1.10.2 源码注解 。
系列第一篇: 【深入浅出jQuery】源码浅析–整体架构
本篇是系列第二篇,标题起得有点大,希望内容对得起这个标题,这篇文章主要总结一下在 jQuery 中一些十分讨巧的 coding 方式,将会由浅及深,可能会有一些基础,但是我希望全面一点,对看文章的人都有所帮助,源码我还一直在阅读,也会不断的更新本文。
即便你不想去阅读源码,看看下面的总结,我想对提高编程能力,转换思维方式都大有裨益,废话少说,进入正题。
短路表达式 与 多重短路表达式
短路表达式这个应该人所皆知了。在 jQuery 中,大量的使用了短路表达式与多重短路表达式。
短路表达式:作为”&&”和”||”操作符的操作数表达式,这些表达式在进行求值时,只要最终的结果已经可以确定是真或假,求值过程便告终止,这称之为短路求值。这是这两个操作符的一个重要属性。
1 | // ||短路表达式 |
当然,上面两个例子是短路表达式最简单是情况,多数情况下,jQuery 是这样使用它们的:
1 | // 选自 jQuery 源码中的 Sizzle 部分 |
嗯,可以看到,diff 的值经历了多重短路表达式配合一些全等判断才得出,这种代码很优雅,但是可读性下降了很多,使用的时候权衡一下,多重短路表达式和简单短路表达式其实一样,只需要先把后面的当成一个整体,依次推进,得出最终值。
1 | var a = 1, b = 0, c = 3; |
这里需要提出一些值得注意的点:
1、在 Javascript 的逻辑运算中,0、””、null、false、undefined、NaN 都会判定为 false ,而其他都为 true
2、因为 Javascript 的内置弱类型域 (weak-typing domain),所以对严格的输入验证这一点不太在意,即便使用 && 或者 || 运算符的运算数不是布尔值,仍然可以将它看作布尔运算。虽然如此,还是建议如下:
1 | if(foo){ ... } //不够严谨 |
注重细节,JavaScript 既不弱也不低等,我们只是需要更努力一点工作以使我们的代码变得真正健壮。
预定义常用方法的入口
在 jQuery 的头几十行,有这么一段有趣的代码:
1 | ( function( window, undefined) { |
不得不说,jQuery 在细节上做的真的很好,这里首先定义了一个对象变量、一个字符串变量、数组变量,要注意这 3 个变量本身在下文是有自己的用途的(可以看到,jQuery 作者惜字如金,真的是去压榨每一个变量的作用,使其作用最大化)。
其次,借用这三个变量,再定义些常用的核心方法,从上往下是数组的 concat、push 、slice 、indexOf 方法,对象的 toString 、hasOwnProperty 方法以及字符串的 trim 方法,core_xxxx 这几个变量事先存储好了这些常用方法的入口,如果下文行文当中需要调用这些方法,将会:
1 | jQuery.fn = jQuery.prototype = { |
可以看到,当需要使用这些预先定义好的方法,只需要借助 call 或者 apply( 戳我详解)进行调用。
那么 jQuery 为什么要这样做呢,我觉得:
以数组对象的 concat 方法为例,如果不预先定义好 core_concat = core_deletedIds.concat 而是调用实例 arr 的方法 concat 时,首先需要辨别当前实例 arr 的类型是 Array,在内存空间中寻找 Array 的 concat 内存入口,把当前对象 arr 的指针和其他参数压入栈,跳转到 concat 地址开始执行,而当保存了 concat 方法的入口 core_concat 时,完全就可以省去前面两个步骤,从而提升一些性能;
另外一点,借助 call 或者 apply 的方式调用,让一些类数组可以直接调用数组的方法。就如上面是示例,jQuery 对象是类数组类型,可以直接调用数组的 slice 方法转换为数组类型。又譬如,将参数 arguments 转换为数组类型:
1
2
3
4
5
6
7
function test( a,b,c){
// 将参数 arguments 转换为数组
// 使之可以调用数组成员方法
var arr = Array.prototype.slice.call( arguments);
...
}
钩子机制(hook)
在 jQuery 2.0.0 之前的版本,对兼容性做了大量的处理,正是这样才让广大开发人员能够忽略不同浏览器的不同特性的专注于业务本身的逻辑。而其中,钩子机制在浏览器兼容方面起了十分巨大的作用。
钩子是编程惯用的一种手法,用来解决一种或多种特殊情况的处理。
简单来说,钩子就是适配器原理,或者说是表驱动原理,我们预先定义了一些钩子,在正常的代码逻辑中使用钩子去适配一些特殊的属性,样式或事件,这样可以让我们少写很多 else if 语句。
如果还是很难懂,看一个简单的例子,举例说明 hook 到底如何使用:
现在考公务员,要么靠实力,要么靠关系,但领导肯定也不会弄的那么明显,一般都是暗箱操作,这个场景用钩子实现再合理不过了。
1 | // 如果不用钩子的情况 |
可以看到,在中间审阅考生这个函数中,运用了很多 else if 来判断是否考生有后门关系,如果现在业务场景发生变化,又多了几名考生,那么 else if 势必越来越复杂,往后维护代码也将越来越麻烦,成本很大,那么这个时候如果使用钩子机制,该如何做呢?
1 | // relationHook 是个钩子函数,用于得到关系得分 |
可以看到,使用钩子去处理特殊情况,可以让代码的逻辑更加清晰,省去大量的条件判断,上面的钩子机制的实现方式,采用的就是表驱动方式,就是我们事先预定好一张表(俗称打表),用这张表去适配特殊情况。
当然 jQuery 的 hook 是一种更为抽象的概念,在不同场景可以用不同方式实现。
看看 jQuery 里的表驱动 hook 实现,$.type 方法:
1 | ( function( window, undefined) { |
这里的 hook 只是 jQuery 大量使用钩子的冰山一角,在对 DOM 元素的操作一块,attr 、val 、prop 、css 方法大量运用了钩子,用于兼容 IE 系列下的一些怪异行为。在遇到钩子函数的时候,要结合具体情境具体分析,这些钩子相对于表驱动而言更加复杂,它们的结构大体如下,只要记住钩子的核心原则,保持代码整体逻辑的流畅性,在特殊的情境下去处理一些特殊的情况:
1 | var someHook = { |
从某种程度上讲,钩子是一系列被设计为以你自己的代码来处理自定义值的回调函数。有了钩子,你可以将差不多任何东西保持在可控范围内。
连贯接口
无论 jQuery 如今的流行趋势是否在下降,它用起来确实让人大呼过瘾,这很大程度归功于它的链式调用,接口的连贯性及易记性。很多人将连贯接口看成链式调用,这并不全面,我觉得连贯接口包含了链式调用且代表更多。而 jQuery 无疑是连贯接口的佼佼者。
链式调用
链式调用的主要思想就是使代码尽可能流畅易读,从而可以更快地被理解。有了链式调用,我们可以将代码组织为类似语句的片段,增强可读性的同时减少干扰。( 链式调用的具体实现上一章有详细讲到)
1 | // 传统写法 |
命令查询同体
这个上一章也讲过了,就是函数重载。正常而言,应该是命令查询分离(Command and Query Separation,CQS),是源于命令式编程的一个概念。那些改变对象的状态(内部的值)的函数称为命令,而那些检索值的函数称为查询。
原则上,查询函数返回数据,命令函数返回状态,各司其职。而 jQuery 将 getter 和 setter 方法压缩到单一方法中创建了一个连贯的接口,使得代码暴露更少的方法,但却以更少的代码实现同样的目标。
参数映射及处理
jQuery 的接口连贯性还体现在了对参数的兼容处理上,方法如何接收数据比让它们具有可链性更为重要。
虽然方法的链式调用是非常普遍的,你可以很容易地在你的代码中实现,但是处理参数却不同,使用者可能传入各种奇怪的参数类型,而 jQuery 作者想的真的很周到,考虑了用户的多种使用场景,提供了多种对参数的处理。
1 | // 传入键值对 |
jQuery 的 on() 方法可以注册事件处理器。和 CSS() 一样它也可以接收一组映射格式的事件,但更进一步地,它允许单一处理器可以被多个事件注册:
1 | // binding events by passing a map |
无 new 构造
怎么访问 jQuery 类原型上的属性与方法,怎么做到做到既能隔离作用域还能使用 jQuery 原型对象的作用域呢?重点在于这一句:
1 | // Give the init function the jQuery prototype for later instantiation |
这里的关键就是通过原型传递解决问题,这一块上一章也讲过了,看过可以跳过了,将文字搬过来。
嘿,回想一下使用 jQuery 的时候,实例化一个 jQuery 对象的方法:
1 | // 无 new 构造 |
大部分人使用 jQuery 的时候都是使用第一种无 new 的构造方式,直接 $(‘’) 进行构造,这也是 jQuery 十分便捷的一个地方。当我们使用第一种无 new 构造方式的时候,其本质就是相当于 new jQuery(),那么在 jQuery 内部是如何实现的呢?看看:
1 | ( function( window, undefined) { |
大部分人初看 jQuery.fn.init.prototype = jQuery.fn 这一句都会被卡主,很是不解。但是这句真的算是 jQuery 的绝妙之处。理解这几句很重要,分点解析一下:
首先要明确,使用 $(‘xxx’) 这种实例化方式,其内部调用的是 return new jQuery.fn.init(selector, context, rootjQuery) 这一句话,也就是构造实例是交给了 jQuery.fn.init() 方法取完成。
将 jQuery.fn.init 的 prototype 属性设置为 jQuery.fn,那么使用 new jQuery.fn.init() 生成的对象的原型对象就是 jQuery.fn ,所以挂载到 jQuery.fn 上面的函数就相当于挂载到 jQuery.fn.init() 生成的 jQuery 对象上,所有使用 new jQuery.fn.init() 生成的对象也能够访问到 jQuery.fn 上的所有原型方法。
也就是实例化方法存在这么一个关系链
- jQuery.fn.init.prototype = jQuery.fn = jQuery.prototype ;
- new jQuery.fn.init() 相当于 new jQuery() ;
- jQuery() 返回的是 new jQuery.fn.init(),而 var obj = new jQuery(),所以这 2 者是相当的,所以我们可以无 new 实例化 jQuery 对象。
setTimeout
写到这里,发现上文的主题有些飘忽,接近于写成了 如何写出更好的 Javascript 代码,下面介绍一些 jQuery 中我觉得很棒的小技巧。
熟悉 jQuery 的人都知道 DOM Ready 事件,传Javascript原生的 window.onload 事件是在页面所有的资源都加载完毕后触发的。
如果页面上有大图片等资源响应缓慢, 会导致 window.onload 事件迟迟无法触发,所以出现了DOM Ready 事件。此事件在 DOM 文档结构准备完毕后触发,即在资源加载前触发。
另外我们需要在 DOM 准备完毕后,再修改DOM结构,比如添加DOM元素等。而为了完美实现 DOM Ready 事件,兼容各浏览器及低版本IE(针对高级的浏览器,可以使用 DOMContentLoaded 事件,省时省力),在 jQuery.ready() 方法里,运用了 setTimeout() 方法的一个特性, 在 setTimeout 中触发的函数, 一定是在 DOM 准备完毕后触发。
1 | jQuery.extend({ |
暂且写这么多吧,技巧还有很多,诸如 $.Deferred() 异步队列的实现,jQuery 事件流机制等,篇幅较长,将会在以后慢慢详述。
原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。
系列第一篇: 【深入浅出jQuery】源码浅析–整体架构
最后,我在 github 上关于 jQuery 源码的全文注解,感兴趣的可以围观一下,给颗星星。 jQuery v1.10.2 源码注解 。