jQuery 类库的设计

jopen 9年前

目前为止,jquery是js社区中最活跃、用户最多的前端类库,具有链式操作、兼容性、基于数组的操作、强大的插件机制等特点,也是很多前端入门同学最早接触到的库。但是内部如何实现的,一直吸引着我。因此最近三个月读完了jquery1.7版本的设计,之所以选择该版本是因为Sizzle在1.8之后引入了编译函数,代码变动比较大。

1.总体设计

本文对jquery1.7版本进行了阅读学习,将整个jquery源码拆分为11个模块,这些模块相互依赖,构成了一个简单、强大的js类库。jquery是一个基于DOM操作的类库,因此Sizzle选择器引擎的实现就显得尤为重要。针对Sizzle选择器引擎的实现,之前已经做过先关的分析,参见:sizzle选择器引擎介绍 。下面对其中的数据存储、事件处理、异常请求ajax、动画等进行简单的介绍。

jQuery 类库的设计

(function(window, undefined) {      var jQuery = function(selector, context) {          return new jQuery.fn.init(selector, context, rootJquery);      }      jQuery.fn = function() {          // 原型对象          ...      }      // 工具方法      // 异步队列      // 队列queue      // 浏览器测试support      // 属性操作      // 事件系统      // DOM遍历与操作、样式操作      // ajax请求      // 动画  })(window)

2. 数据存储

jQuery 类库的设计

在实际的项目开发中,经常需要把某些信息附加到一个DOM节点中,那么如果管理DOM节点和附加数据的关系,就显得非常重要。很明显,目前有两种思路来解决这个问题:1)直接附加在DOM节点上;2)通过一个id来关联DOM节点、附加数据。这两种方法各有利弊:方法1的好处是DOM节点、附加数据在一起,便于维护;方法2的好处是可以避免相互依赖,从而避免内存泄露问题。

var arr = [];  function createNode() {       return document.createElement('div');  }  function saveNodes() {      for(var i=0; i<100; i++) {          arr.push(createNode());      }  }  // 将上述100个nodes节点渲染到页面,之后在将其从页面中剔除掉,那么因为arr引用着100个nodes节点,这些节点就无法被垃圾回收机制回收,从而引起内存泄露问题。

3. 异步Deferred

jQuery中关于异步的实现,大致上遵循Promise/A规范,为什么说是大致呢?

  1. then的返回:then方法并没有返回新的异步对象,这不符合Promise/A规范中的then要求。这一点在jQuery2.0以后已经被修改
  2. 结果处理:在进行结果处理的时候,jQuery并没有进行结果的异常捕获
  3. 参数的个数:jquery中resolve可以有多个参数,而Promise/A的resolve仅能有一个

内部实现比较曲折抽象,代码晦涩难懂,主要是通过”once”、”memory”两个参数进行控制:”once”决定异步的回调函数只能被执行一次;“memory”决定函数具有记忆动能,也就是异步事件完成以后,再绑定回调函数,回调函数会立即执行。

4. 事件处理

jQuery 类库的设计

jquery内部事件的功能很强大,除了可以处理DOM事件外,还可以自定义事件、触发事件(DOM事件、自定义事件)、定义事件的命名空间等强大功能。并且jquery内部的另外一大亮点就是通过数据存储模块(.data),尽量降低DOM和监听事件之间的依赖,避免DOM、js对象相互依赖造成的内存泄露。在数据缓存模块的数据结构如下:

$('.a').on('click', function() {});  // results is as fellow:  $.cache = {      1: {          events: {                             click: [                //click.delegateCount: 记录代理事件的个数,代理回调函数放在数组的前面                  {                      data: ...,                      guid: ...,                      selector: ...,                      handler: function() {}, // handler.guid = 1  用于定位和移除监听函数                      ....                  }                ]          },          handle: function() {......}    // 主监听函数      }  }

4.1 事件绑定:

当绑定事件时,内部方法的调用链为:bind/delegate/live/one()—>.on()—>$.event.add()—>$.data()/addEventListener/attachEvent()。其实在对于一个DOM元素,所有的事件都对应一个主监听函数($._data(elem).handle),然后通过主监听函数通过事件分发函数($.event.dispatch)来触发相应类型的监听函数。

$('.a').on('click', fn1);  $('.a').on('blur', fn2);  $('.a').on('focus', fn3);  // 其实DOM元素(.a)并没有直接与fn1、fn2、fn3关联起来,而是通过dispatch进行事件分发  // 用伪代码可以表示如下:  $('.a').on('click blur focus', dispatch);  function dispatch(type) {      var fn;      if(type === 'click') {          fn = fn1;        } else if(type === 'blur') {          fn = fn2;      } else if (type === 'focus') {          fn = fn3;      }      return fn;  }

4. 2 事件移除

当移除事件时,内部方法的调用链为:unbind/undelagate/die()—>.off()—>$.event.remove()—>$._data()/removeEventListener/detachEvent()。事件的移除,也就是从数据存储对象$.cache中移除相应的事件对象,当事件对象events为空时,则移除整个数据缓存对象。

4.3 事件手动触发

在jquery中,可以手工触发DOM事件或自定义事件。内部方法的调用链为:.trigger/triggerHandler() —> $.event.trigger() — > $.event.dispatch(主监听函数) — >事件的监听函数。

还有一个需要关注的问题就是,如何事件事件的冒泡呢?方法其实很简单,就是根据DOM的结构向上查询出元素的祖先元素,一直到window对象,这样就构成了元素的冒泡路径,然后触发这个路径上元素的相应事件。这也是jquery可以模拟focus、blur、change、submit进行事件冒泡的关键环节。

// 寻找冒泡路径  var eventPath = [];  for(;cur; cur = cur.parentNode) {      eventPath.push([cur, type]);  }  // 执行路径上的监听函数  for() {      cur = eventPath[i][0];      type =  eventPath[i][1];      hanle = $._data(cur, "events")[type];      hanle.apply(cur);  }

5. 异步请求ajax

jQuery 类库的设计

异步请求是jquery在总体可以分为三部分:核心实现、便捷方法、ajax全局事件。其中该模块依赖于Deferred模块提供的异步编程模块,可以方便进行回调函数的注册,例如:

$(url, options).then(successFn, failFn);

其中核心方法的实现主要包括以下骤:

  1. 参数的设置:在jQuery所有API中,.ajax的参数种类应该是对多的,里面的参数看得人掩护缭乱。但是其中最终的有url、type、dataType、data,尤其是dataType的设置对于结果的应该很长大,所以有大量代码是对这一步的处理。
  2. 前置过滤函数处理:主要是对json、jsonp、script三种数据类型的处理,在请求发送前对其进行过滤处理
  3. 请求发出:这里请求的发出包括两种方式,分别为依赖于XMLHttpRequest和script标签。如果浏览器允许跨域,则仅需使用XMLHttpRequest就足够了。
  4. 回调函数的执行:这里的回调函数包括很多种,请求发出前、开始接受数据、数据接受完成等等。

6. 动画解析

jQuery 类库的设计

在jQuery中,动画show、hide、fadeIn、fadeOut等均要调用Animation方法,也就是说Animation是最基本的入口函数。在上图中可以看到该入口函数包括了三个过程:参数配置、生成动画函数、动画函数执行。下面展开包括以下细节:

  1. 参数配置:主要有三个参数,duration表示动画的执行时间;easing为动画每一帧的变化速度,目前jQuery中仅存在两种帧变化函数:线性(linear)变化、余弦变化(swing),要用其他变化函数只能修改$.fx.easing对象;complete为动画完成之后需要执行的回调函数。
  2. 生成动画函数doAnimation:jQuery会给每一个样式生成一个$.fx对象,该对象用于实现动画效果。默认情况下,每隔13ms会执行一帧动画,然后更新页面的样式。
  3. 动画执行:如果动画不需要排队,在生成完动画函数之后,就立即执行动画函数doAnimation;反之,将doAnimation放入队列queue中进行排队。当所有动画均完成之后,就可以执行回调函数complete了。

当然,jQuery内部实现比较复杂,考虑了函数暂停、样式临时修改(修改inline元素的width/height时,会临时将其display修改为inline-block)、清空动画队列等操作。

参考资料

https://developer.chrome.com/devtools/docs/demos/memory/example6


原文出处: bigbrother1984