jQuery

The Write Less, Do More, JavaScript Library

https://github.com/jquery/jquery

Cube - Page - Concave - Zoom - Linear - None - Default
Sky - Beige - Simple - Serif - Night - Default

节选自

《jQuery 技术内幕》

深入解析jQuery架构设计与实现原理

作者:高云

审校邀请 http://nuysoft.com/book.html

版权保护,谢绝以任何形式转载、出版、集结。

总体架构

入口模块、底层模块、功能模块

设计理念

  • 写更少的代码,做更多的事
  • 提升 JavaScript 开发体验
  • 以用为本

源码评价

精致、优雅、风骚

核心特性

  • 兼容主流浏览器,支持 IE 6.0+, FF 3.6+, Safari 5.0+, Opera, Chrome
  • 独特的链式语法、短小清晰的多功能接口
  • 高效灵活的CSS选择器、以及对CSS选择器的扩展
  • 便捷的插件扩展机制和丰富的插件

源码结构


(function( window, undefined ) {
    // 构造jQuery对象
    var jQuery = (function() {
        var jQuery = function( selector, context ) {
                return new jQuery.fn.init( selector, context, rootjQuery );
            }
        return jQuery;
    })();
    // 工具函数 Utilities
    // 回调函数对象 Callbacks Object
    // 异步队列对象 Deferred Object
    // 浏览器测试 Support
    // 数据缓存 Data
    // 队列 Queue
    // 属性操作 Attributes
    // 事件处理 Events
    // 选择器 Sizzle
    // DOM遍历 Traversing
    // DOM操作 Manipulation
    // 样式操作 CSS
    // 异步请求 AJAX
    // 动画 Effects
    // 坐标 Offset 和 大小 Dimensions
    window.jQuery = window.$ = jQuery;
})(window);

		

入口模块、底层模块、功能模块

自调用匿名函数


(function( window, undefined ) {
    // jQuery的所有代码
})(window);
		
  1. 为什么要创建这样一个自调用匿名函数呢?
  2. 为什么要传入 window 对象呢?
  3. 为什么要在在参数列表中增加 undefined 呢?
  4. 注意到源码最后的分号了吗?

构造 jQuery 对象

jQuery.fn.init()

构造函数 jQuery()

总体结构


  16 (function( window, undefined ) {
         // 构造 jQuery 对象
  22     var jQuery = (function() {
  25         var jQuery = function( selector, context ) {
  27                 return new jQuery.fn.init( selector, context, rootjQuery );
  28             },
                 // 一堆局部变量声明
  97         jQuery.fn = jQuery.prototype = {
  98             constructor: jQuery,
  99             init: function( selector, context, rootjQuery ) { ... },
                 // 一堆原型属性和方法
 319         };
 322         jQuery.fn.init.prototype = jQuery.fn;
 324         jQuery.extend = jQuery.fn.extend = function() { ... };
 388         jQuery.extend({
                 // 一堆静态属性和方法
 892         });
 955         return jQuery;
 957     })();
          // 省略其他模块的代码 ...
9246     window.jQuery = window.$ = jQuery;
9266 })( window );
		

为什么

为什么要在调用构造函数 jQuery() 内部用运算符 new 创建并返回另一个构造函数的实例?

为什么

为什么在第97行执行 jQuery.fn = jQuery.prototype,设置 jQuery.fn 指向构造函数 jQuery() 的原型对象 jQuery.prototype?

为什么

既然调用构造函数 jQuery() 返回的 jQuery 对象实际上是构造函数 jQuery.fn.init() 的实例,为什么能在构造函数 jQuery.fn.init() 的实例上调用构造函数 jQuery() 的原型方法和属性,例如: $('#id').length 和 $('#id').size()?

为什么

为什么要把第25~955行的代码包裹在一个自调用匿名函数中,然后把第25行定义的构造函数 jQuery() 作为返回值赋值给为第22行的 jQuery 变量?去掉这个自调用匿名函数,直接在第25行定义构造函数 jQuery() 不也可以吗?去掉了不是更容易阅读和理解吗?

为什么

为什么要覆盖构造函数 jQuery() 的原型对象 jQuery.prototype?

jQuery.fn.init( selector, context, rootjQuery )

原型属性和方法


  97 jQuery.fn = jQuery.prototype = {
  98     constructor: jQuery,
  99     init: function( selector, context, rootjQuery ) {}
 210     selector: "",
 213     jquery: "1.7.1",
 216     length: 0,
 219     size: function() {},
 223     toArray: function() {},
 229     get: function( num ) {},
 241     pushStack: function( elems, name, selector ) {},
 270     each: function( callback, args ) {},
 274     ready: function( fn ) {}, //
 284     eq: function( i ) {},
 291     first: function() {},
 295     last: function() {},
 299     slice: function() {},
 304     map: function( callback ) {},
 310     end: function() {},
 316     push: push,
 317     sort: [].sort,
 318     splice: [].splice
 319 };
		

静态属性和方法


 388 jQuery.extend({
 389     noConflict: function( deep ) {},
 402     isReady: false,
 406     readyWait: 1,
 409     holdReady: function( hold ) {},
 418     ready: function( wait ) {},
 444     bindReady: function() {},
 492     isFunction: function( obj ) {},
 496     isArray: Array.isArray || function( obj ) {},
 501     isWindow: function( obj ) {},
 505     isNumeric: function( obj ) {},
 509     type: function( obj ) {},
 515     isPlainObject: function( obj ) {},
 544     isEmptyObject: function( obj ) {},
 551     error: function( msg ) {},
 555     parseJSON: function( data ) {},
 581     parseXML: function( data ) {},
 601     noop: function() {},
 606     globalEval: function( data ) {},
 619     camelCase: function( string ) {},
 623     nodeName: function( elem, name ) {},
 628     each: function( object, callback, args ) {},
 669     trim: trim ? function( text ) {} : function( text ) {},
 684     makeArray: function( array, results ) {},
 702     inArray: function( elem, array, i ) {},
 724     merge: function( first, second ) {},
 744     grep: function( elems, callback, inv ) {},
 761     map: function( elems, callback, arg ) {},
 794     guid: 1,
 798     proxy: function( fn, context ) {},
 825     access: function( elems, key, value, exec, fn, pass ) {},
 852     now: function() {},
 858     uaMatch: function( ua ) {},
 870     sub: function() {},
 891     browser: {}
 892 });
		

总结

  • 构造函数的 7 种用法
  • 源码总体结构
  • 5 个为什么
  • 构造函数 jQuery.fn.init() 的 12 个分支
  • 原型属性和方法
  • 静态属性和方法

CSS选择器引擎 Sizzle

分割器、块查找、块内过滤、块间关系过滤

设计思路

div > p

  1. 解析块表达式
  2. 执行块表达式 div.red
  3. 处理块表达式之间的关系

设计思路

  1. 分割器 chunker正则
  2. 块查找 Sizzle.find( expr, context, isXML )
    块内过滤 Sizzle.filter( expr, set, inplace, not )
  3. 块间关系过滤 Expr.relative

实现原理

  • 从左向右:不断缩小上下文
  • 从右向左:先查找再过滤

异步队列 Deferred

解耦异步任务和回调函数

为 ajax 模块、队列模块、ready 事件提供基础功能。

jQuery 1.5 之前


$.ajax({
    url: 'ajax/test.html',
    success: function(data, status, xhr) { ... },
    error: function(xhr, status, errMsg) { ... },
    complete: function(xhr, status) { ... }
});
		

jQuery 1.5+


$.ajax( 'ajax/test.html' )
   // 对应 success
   .done(function(data, textStatus, jqXHR) { ... })
   .done(function(data, textStatus, jqXHR) { ... })
   // 对应 error
   .fail(function(jqXHR, textStatus, errorThrown) { ... })
   .fail(function(jqXHR, textStatus, errorThrown) { ... })
   // 对应 complete
   .complete(function(jqXHR, statusText){ ... }) 
   .complete(function(jqXHR, statusText){ ... });
		

源码结构


 996 jQuery.Callbacks = function( flags ) {
1181 };
1189 jQuery.extend({
1191     Deferred: function( func ) {
1282     },
1285     when: function( firstParam ) {
1325     }
1326 });
		

调用关系

jQuery.Callbacks( flags )

返回一个多功能链式对象,提供强大的回调函数列表管理功能,支持添加、移除、触发、锁定、禁用回调函数,为 jQuery.ajax()、jQuery.Deferred() 和 ready 事件提供基础功能,也可以基于它编写新的组件。

返回的多功能对象称为为“回调函数列表 Callbacks Object”,代码中用 callbacks 表示。

jQuery.Callbacks ( flags ) 支持的标记

  • once
    确保回调函数列表只能被触发一次。
  • memory
    记录上一次触发回调函数列表时的参数,之后添加的任何回调函数都将用记录的参数值立即调用。
  • unique
    确保一个回调函数只能被添加一次,回调函数列表中没有重复值。
  • stopOnFalse
    当某个回调函数返回 false 时中断执行。

回调函数列表的方法列表

  • 添加 callbacks.add( callbacks )
  • 移除 callbacks.remove( callbacks )
  • 清空 callbacks.empty()
  • 禁用 callbacks.disable()
  • 锁定 callbacks.lock()
  • 调用 callbacks.fireWith( [context] [, args] )
  • 调用 callbacks.fire( arguments )

jQuery.Callbacks( flags ) 的实现原理

内部通过一个数组保存回调函数,其他方法则围绕这个数组进行操作和检测。

jQuery.Callbacks( flags ) 的源码结构


// 工具函数,将字符串格式的标记转换为对象格式
function createFlags( flags ) { ... }

jQuery.Callbacks = function( flags ) {
    // 解析字符串标记 flags 为对象
    flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {};
    // 声明局部变量,通过闭包机制引用
    var list = [], // 存放回调函数的数组
        stack = [], // 在可重复触发、正在执行的列表上,重复触发时,将上下文和参数放入数组 stack 中
        memory, // memory 的可能值和用途有些复杂,在分析工具方法 fire( context, args ) 时详细分析
        firing, // 是否正在执行中
        firingStart, // 执行回调函数时的起始下标
        firingLength, // 执行回调函数时的结束下标
        firingIndex, // 正在执行的回调函数的下标
        add = function( args ) { ... }, // 实际添加回调函数的工具函数
        fire = function( context, args ) { ... }, // 实际触发回调函数的工具函数
        // 回调函数列表,方法 jQuery.Callbacks( flags ) 的返回值
        self = {
            add: function() {}, // 添加回调函数
            remove: function() { ... }, // 移除回调函数
            has: function( fn ) { ...}, // 回调函数是否在列表中
            empty: function() { ... }, // 清空列表
            disable: function() { ... }, // 禁用列表
            disabled: function() { ... }, // 是否已禁用列表
            lock: function() { ... }, // 锁定列表
            locked: function() { ... }, // 是否已锁定列表
            fireWith: function( context, args ) { ... }, // 使用指定的上下文和参数调用回调函数
            fire: function() { ... }, // 使用指定的参数调用回调函数,上下文为 self
            fired: function() { ... } // 回调函数列表是否至少执行过一次
        };
    return self; // 返回回调函数列表 self
};
		

jQuery.Deferred( func )

基于 jQuery.Callbacks( flags ) 实现 ,返回一个链式工具对象,支持添加多个回调函数到回调函数列表、触发回调函数列表、传播任意同步或异步任务的成功或失败状态。

返回的链式工具对象称为“异步队列 Deferred Object”,代码中用 deferred 表示。

jQuery.Deferred( func ) 的用法

  • 三种状态
    待定 pending、成功 resolved、失败 rejected。
  • deferred.done/fail/progress/then/allway()
    添加成功回调函数、失败回调函数、消息回调函数到对应的成功、失败、消息回调函数列表
  • deferred.resolve/resolveWith/reject/rejectWith/notify/notifyWith()
    触发成功、失败、消息回调函数列表

jQuery.Deferred( func ) 的实现原理

内部维护了三个回调函数列表:成功回调函数列表、失败回调函数列表、消息回调函数列表,其他方法则围绕这三个列表进行操作和检测。

jQuery.Deferred( func ) 的源码结构


jQuery.extend({

    Deferred: function( func ) {
            // 成功回调函数列表
        var doneList = jQuery.Callbacks( "once memory" ),
            // 失败回调函数列表
            failList = jQuery.Callbacks( "once memory" ),
            // 消息回调函数列表
            progressList = jQuery.Callbacks( "memory" ),
            // 初始状态
            state = "pending",
            // 异步队列的只读副本
            promise = {
                // done, fail, progress
                // state, isResolved, isRejected
                // then, always
                // pipe
                // promise           
            },
            // 异步队列
            deferred = promise.promise({}),
            key;
        // 添加触发成功、失败、消息回调函列表的方法
        for ( key in lists ) {
            deferred[ key ] = lists[ key ].fire;
            deferred[ key + "With" ] = lists[ key ].fireWith;
        }
        // 添加设置状态的回调函数
        deferred.done( function() {
            state = "resolved";
        }, failList.disable, progressList.lock )
        .fail( function() {
            state = "rejected";
        }, doneList.disable, progressList.lock );
        // 如果传入函数参数 func,则执行。
        if ( func ) {
            func.call( deferred, deferred );
        }

        // 返回异步队列 deferred
        return deferred;
    },
}
		

jQuery.when( deferreds )

提供了基于一个或多个对象的状态来执行回调函数的功能,通常是基于具有异步事件的异步队列。

jQuery.when( deferreds ) 的用法

如果传入多个异步队列对象,方法 jQuery.when() 返回一个新的主异步队列对象的只读副本,只读副本将跟踪所传入的异步队列的最终状态。

一旦所有异步队列都变为成功状态,“主“异步队列的成功回调函数被调用;

如果其中一个异步队列变为失败状态,主异步队列的失败回调函数被调用。


/*
请求 '/when.do?method=when1' 返回 {"when":1}
请求 '/when.do?method=when2' 返回 {"when":2}
请求 '/when.do?method=when3' 返回 {"when":3}
*/
var whenDone = function(){ console.log( 'done', arguments ); },
    whenFail = function(){ console.log( 'fail', arguments ); };
$.when(
    $.ajax( '/when.do?method=when1', { dataType: "json" } ),
    $.ajax( '/when.do?method=when2', { dataType: "json" } ),
    $.ajax( '/when.do?method=when3', { dataType: "json" } )
).done( whenDone ).fail( whenFail );
		

jQuery.when( deferreds ) 的实现原理

  • 内部创建一个“主”异步队列,并维护一个计数器。传入的异步队列参数称为“子”异步队列。
  • 为了监听子异步队列的成功状态,在每个异步队列上添加会把计数器减一的成功状态监听函数。
  • 为了监听传入的异步队列的失败状态,把主异步队列的方法 reject() 添加到每个子异步队列的失败队列上。
  • 为了监听传入的异步队列的消息调用,在每个子异步队列上添加会记录消息参数的消息监听函数。

数据缓存 Data

为 DOM 元素和 JavaScript 对象附加任意类型的数据,避免内存泄漏

为队列模块、动画模块、样式模式、事件模块提供基础功能,维护这些模块运行时的内部数据。

数据缓存的用法

  • jQuery.hasData( elem )
    是否有关联的数据。
  • jQuery.data( elem, name, data, pvt )
    设置任意类型的数据,或返回指定名称的数据,或返回关联的数据缓存对象。
  • jQuery.removeData( elem, name, pvt )
    移除设置的数据。
  • .data( key, value )
    设置或读取自定义数据,解析 HTML5 属性 data-,并触发自定义事件 getData、setData、changeData。
  • .removeData( key )
    移除设置的自定义数据。

数据缓存的实现原理

为 DOM 元素附加数据

对于 DOM 元素,通过分配一个唯一的关联 id 把 DOM 元素和该 DOM 元素的数据缓存对象关联起来,关联 id 被附加到以 jQuery.expando 的值命名的的属性上,数据存储在全局缓存对象 jQuery.cache 中。

读取、设置、移除数据时,通过关联 id 从全局缓存对象 jQuery.cache 中找到该 DOM 元素的数据缓存对象,然后读取或设置指定属性的值,或移除指定属性。

数据缓存的实现原理

为 JavaScript 对象附加数据

对于 JavaScript 对象,数据直接存储在该 JavaScript 对象的属性 jQuery.expando 上。

读取、设置、移除数据时,直接从 JavaScript 对象的数据缓存对象中,读取或设置指定属性的值,或移除指定属性。

数据缓存的实现原理

内部数据 vs 自定义数据

为了避免 jQuery 内部使用的数据和用户自定义的数据发生冲突,内部数据存储在数据缓存对象上,自定义数据则存储在数据缓存对象的属性 data 上。

数据缓存的总体结构


// 数据缓存 Data
jQuery.extend({
    // 全局缓存对象
    cache: {},
    // 唯一 id 种子
    uuid: 0,
    // 页面中 jQuery 的每个副本的唯一标识
    expando: ...,
    // 是否有关联的数据
    hasData: function( elem )  { ... },
    // 设置、读取自定义数据或内部数据
    data: function( elem, name, data, pvt )  { ... },
    // 移除自定义数据或内部数据
    removeData: function( elem, name, pvt)  { ... },
    // 设置、读取内部数据
    _data: function( elem, name, data )  { ... },
    // 是否可以设置数据
    acceptData: function( elem )  { ... }
});
jQuery.fn.extend({
    // 设置、读取自定义数据,解析 HTML5 属性 data-
    data: function( key, value )  { ... },
    // 移除自定义数据
    removeData: function( key )  { ... }
});
// 解析 HTML5 属性 data- 
function dataAttr( elem, key, data )  { ... }
// 检查数据缓存对象是否为空
function isEmptyDataObject( obj )  { ... }
jQuery.extend({
    // 清空数据缓存对象
    cleanData: function( elems )  { ... }
});
		

数据缓存方法的调用关系

队列 Queue

顺序执行函数

队列 Queue

队头出队,队尾入队,先进先出。

为动画模块提供基础功能,负责存储动画函数、自动出队并执行动画函数、并确保动画函数的顺序执行。

可以应用于任何需要顺序执行函数的场景。

队列的实现原理

  • 队列模块基于数据缓存模块和数组实现。
  • 在队列模块中,用数组存储函数,通过数组方法 .push() 和 .shift() 实现入队和出队操作,通过函数方法 .call() 执行函数。
  • 数组作为内部数据存储在关联的数据缓存对象上,数据名称为队列名称加字符串“queue”。对于 DOM 元素,存储在全局缓存对象 jQuery.cache 中;对于 JavaScript 对象,直接存储在该对象上。

队列的总体结构


// 队列 Queue
// 检测函数队列和计数器是否完成
function handleQueueMarkDefer( elem, type, src ) { ... }
jQuery.extend({
    // 计数器 type+mark 加一
    _mark: function( elem, type )  { ... },
    // 计数器 type+mark 减一
    _unmark: function( force, elem, type )  { ... },
    // 函数入队,并返回队列
    queue: function( elem, type, data )  { ... },
    // 函数出队,并执行
    dequeue: function( elem, type )  { ... }
});
jQuery.fn.extend({
    // 取出函数队列,或函数入队
    queue: function( type, data )  { ... },
    // 函数出队,并执行
    dequeue: function( type )  { ... },
    // 延迟函数出队执行
    delay: function( time, type )  { ... },
    // 清空函数队列
    clearQueue: function( type )  { ... },
    // 观察函数队列和计数器是否完成,返回异步队列只读副本
    promise: function( type, object )  { ... }
});
		

队列的方法调用关系

.delay( time, type )

设置一个定时器,使得匹配元素关联的函数队列中后续的函数延迟出队和执行。

通过调用方法 .queue( type, data ) 向关联的函数队列中插入一个新函数,在该函数内通过 setTimeout 延迟下一个函数的出队时间。

.promise( type, object )

返回一个异步队列的只读副本,观察每个匹配元素关联的某个类型的队列和计数器是否完成。

当每个匹配元素关联的某个类型的函数队列(type +“queue”)变为空队列,计数器(type +“mark”)变为 0,则触发异步队列的成功回调函数,上下文和参数是调用方法 .promise() 的集合(即 jQuery 对象);

如果队列和计数器不存在,则立即触发成功回调函数。

实现原理

  • 创建一个异步队列来存储成功回调函数,创建一个计数器来记录匹配元素关联的队列和计数器的状态,计数器的初始值为需要观察的元素的个数,创建一个会使计数器减一的特殊回调函数,并把该特殊回调函数添加到需要观察的元素所关联的回调函数列表中。

实现原理

  • 当某个元素关联的函数队列成为空队列,关联的计数器变为 0,则触发关联的回调函数列表,特殊回调函数被执行,方法 .promise( type, object ) 的计数器减一。该功能由函数 handleQueueMarkDefer( elem, type, src ) 提供。

实现原理

  • 当方法 .promise( type, object ) 的计数器变为 0,表示所有需要观察的元素关联的函数队列和计数器都已经完成,则触发异步队列的成功回调函数。

浏览器测试 Support

提供了 34 个针对不同浏览器功能和 bug 的测试项

把测试结果封装在对象 jQuery.support 中

34 个测试项

含义? 测试结果? 实现原理? 源码分析?

属性操作 Attributes

实现原理

  • HTML 属性操作
    原生方法 getAttrbiute() 和setAttribute() 的封装和简化,并解决了大量浏览器兼容性问题。
  • DOM 属性操作
    DOM 属性原生读取、设置操作的封装和简化,并解决了少量浏览器兼容性问题。
  • 类样式操作
    通过修改 DOM 属性 className 来修改类样式,核心技巧:类样式前后分别加空格 + indexOf/replace()。
  • 值操作 .val( value )
    对 DOM 属性 value 原生读取、设置操作的封装和扩展,并解决了少量浏览器兼容问题。

事件处理 Event

要求:兼容、正确、高效、便捷

源码:清晰、优雅

DOM遍历 Traversing

功能:父元素、祖先元素、兄弟元素、子元素

实现:遍历函数、工具函数、模版函数

总体结构


// DOM遍历
jQuery.each({
    // 遍历函数
    parent: function( elem )  { ... },
    parents: function( elem )  { ... },
    parentsUntil: function( elem, i, until )  { ... },
    next: function( elem )  { ... },
    prev: function( elem )  { ... },
    nextAll: function( elem )  { ... },
    prevAll: function( elem )  { ... },
    nextUntil: function( elem, i, until )  { ... },
    prevUntil: function( elem, i, until )  { ... },
    siblings: function( elem )  { ... },
    children: function( elem )  { ... },
    contents: function( elem )  { ... }
}, function( name, fn ) {
    // 公开方法,模板函数
    jQuery.fn[ name ] = function( until, selector )  { ... };
});

jQuery.extend({
    // 工具函数
    filter: function( expr, elems, not )  { ... },
    dir: function( elem, dir, until )  { ... },
    nth: function( cur, result, dir, elem )  { ... },
    sibling: function( n, elem )  { ... }
});
		

DOM操作 Manipulation

CSS操作 CSS

异步请求 AJAX

动画 Effects

THE END

BY @nuysoft 高云 一淘-UX-墨智

mozhi.gyy@taobao.com nuysoft@gmail.com

http://nuysoft.com/

http://nuysoft.iteye.com/ http://cnblogs.com/nuysoft