jq源码 | 2020-12-02 20:35:56 295次 3次
诶...朋友们好啊。 刚才有个朋友问我,他说你这看 jq 源码也没用。 我说我这个有用,这是化劲儿,传统代码是讲化劲儿的,四两拨千金。
文章大致划分为如下几个模块:
- 总体分析
- 选择器sizzle
- dom操作
- 样式操作
- 事件
- 动画
传统武术来说,一般都是先站桩,后练单招,然后才是练套路,接着开始拆单招,最后是进行对抗训练,写代码也一样,练完单招(使用各种框架),需要开始从源码中学习套路,最后再集大成于一体灵活运用与工作之中。任何需求来了都可以“不招不架就是一下”!
首先要练好内功,先练功后练拳,先分析下 jq 中的内功,在内功的支撑下打出各种组合拳。
代码结构
代码调试的时候可以直接使用打包后的代码,查看源码是使用 github 上的目录会更清晰一些。打包后的代码是一个自执行函数,采用严格模式:
打包后代码结构: ( function( global, factory ) { "use strict"; // 模块化处理 支持 commonjs if ( typeof module === "object" && typeof module.exports === "object" ) { module.exports = global.document ? // 只有 document 对象 factory( global, true ) : function( w ) { // node 中传入了 window 对象 // 使用 var jQuery = require("jquery")(window); if ( !w.document ) { throw new Error( "jQuery requires a window with a document" ); } return factory( w ); }; } else { factory( global ); } } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { // 正常浏览器使用时将 $ jQuery 挂载到 window 上,可以直接使用 if ( typeof noGlobal === "undefined" ) { window.jQuery = window.$ = jQuery; } return jQuery; });
无 new 构建
一般在使用 jq 的时候不需要 new 这个过程,因为自身帮我们做了这件事,接、化、发一气呵成!
大体结构: ( function (window) { // ... jQuery = function (selector, context) { return new jQuery.fn.init(selector, context, rootjQuery); } jQuery.fn = jQuery.prototype = { init: function (selector, context, rootjQuery) { // ... return this } } jQuery.fn.init.prototype = jQuery.fn; })(window);
接:先将 init 看成一个单独的函数,jQuery 做的事就是返回 init 方法的实例,并且 init 的原型 prototype 指向了 jq 的原型;
化:将 init 放在 jQuery.prototype 上,因为要用化劲融入到敌方内部,偷取 this,你打我的劲化为我的力量;
发:浑元形意太极中第二层级是后动手的快,也叫“慢打快”的快,表面你先用的 $,我 init 其实早就出手,将this劲发出去;
通过“接手、化手、发手”,即“接劲、化劲、发劲”完成无 new 构建的过程,对方一下子便飞出去了,还不知道是怎么回事就飞出去了,如果理解不了接化发,需要武德内径图修炼下。
实战,请分析如下四种写法区别:
console.log(new $()) console.log($()) console.log(new $) ---------------------------------------- console.log($)
上面三种写法的结果是一样的,都是 jQuery.fn.init,它的原型上是 jQuery.fn,这里面提供一些常用的 api。其中第三种写法是对手的一个虚劲,new 的时候可以省略括号。
最后一个写法就是返回的 jQuery 函数本身。
extend 插件
jq 中提供了可扩展插件的方式,jQuery.extend = jQuery.fn.extend,这两者虽然是相等操作,但是使用方式是不同的:
1)jQuery.extend(): 把两个或者更多的对象合并到第一个当中,$.xxx 方式使用
2)jQuery.fn.extend():把对象挂载到 jQuery 的 prototype 属性,来扩展一个新的 jQuery 实例方法。通过 $(dom).xxx 使用
原因是这个方法中最终返回的是 this,前者的 this 指向 jq 本身,扩展的是类的静态方法;
后者 this 指向 jq.fn(jQuery.fn = jQuery.prototype),扩展的是对象实例上的方法。
jQuery.extend = jQuery.fn.extend = function() { ... // 注意点:只有一个参数时,说明是扩展的自身静态方法或者对象的实例方法,否则就是对象的合并 if ( i === length ) { target = this; i--; } ... return target; };
同时这个方法中注意 isPlainObject 的方法实现,有很多版本实现:
var class2type = {}; var hasOwn = class2type.hasOwnProperty; var fnToString = hasOwn.toString; var ObjectFunctionString = fnToString.call( Object ); function isPlainObject( obj ) { var proto, Ctor; // 如果是浏览器原生的一些对象,直接不通过 if ( !obj || toString.call( obj ) !== "[object Object]" ) { return false; } // 获取 __proto__ proto = Object.getPrototypeOf( obj ); // Object.create( null )创建的对象没有原型 if ( !proto ) { return true; } Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; // 最后一个判断确保 [object Object] 类型,比如 new function(){} 到这里就不通过 return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; }
jq 内部也是通过插件的机制扩展各种功能。
链式调用
jq 中可以使用链式调用的方式,因为在 jQuery.fn.extend 扩展中,每个方法最终都会返回 this 自身。同时不同的 api 通过扩展的方式最终都集中到 jQuery.fn.init 的原型上。
变量冲突
在使用 jq 的时候,有些其他的库可能占用了 $ 这个变量,jq 中可以通过一个变更的手法解决这个冲突:
// 其他的库占用这两个名称,缓存记录下,如果没有占用则为 undefined var _jQuery = window.jQuery, _$ = window.$; // 这个方法执行时 window.jQuery = window.$ = jQuery 这些变量才有值 jQuery.noConflict = function(deep) { // 解除 $ 占用 if (window.$ === jQuery) { window.$ = _$; } // 次参数代表 jQuery 的占用也解除 if (deep && window.jQuery === jQuery) { // 这里的window仅仅是个参数,参考最上面的代码结构,修改它并不会修改jQuery变量 window.jQuery = _jQuery; } // 返回本身 被外部变量接收 return jQuery; }; if (typeof noGlobal === strundefined) { window.jQuery = window.$ = jQuery; }
数据缓存
数据缓存模块,会被内部大量使用,其实就是 $.data 或者 $(dom).data 的使用,这里面逻辑不复杂,大致代码结构:
jQuery.extend({ ... data: function( elem, name, data ) { // 这里的方法来自 data/Data.js 中,逻辑简单,就是 set get delete 操作 return dataUser.access( elem, name, data ); }, ... }); jQuery.fn.extend( { data: function( key, value ) { ... // 这个方法也是 set 和 get 操作,内部中使用的比较多,封装为一个抽象方法 // 同时选择多个 calss 帮助批量执行回调逻辑 // 比如:attr(),prop(),text(),html(),css(),data(),scrollLeft(),scrollTop() return access( this, function( value ) { ... // 这里来自 Data.js 的方法,设置值,取值等操作 ... }, null, value, arguments.length > 1, null, true ); }, ... } );
回调和异步
回调方法 callbacks 就是一个发布订阅。
异步的机制是借鉴的 promise 的东西,jq中的源码可以简单了解下,具体的模拟实现可以直接看 promise原理。
页面加载
$(function) 和 $(document).ready() 是一样的效果,调用 DOMContentLoaded 监听,采用 load 兜底处理:
function completed() { document.removeEventListener( "DOMContentLoaded", completed, false ); window.removeEventListener( "load", completed, false ); jQuery.ready(); } jQuery.ready.promise = function( obj ) { if ( !readyList ) { readyList = jQuery.Deferred(); // Catch cases where $(document).ready() is called after the browser event has already occurred. // we once tried to use readyState "interactive" here, but it caused issues like the one // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 if ( document.readyState === "complete" ) { // Handle it asynchronously to allow scripts the opportunity to delay ready setTimeout( jQuery.ready ); } else { // Use the handy event callback document.addEventListener( "DOMContentLoaded", completed, false ); // A fallback to window.onload, that will always work window.addEventListener( "load", completed, false ); } } return readyList.promise( obj ); };
3人赞