如何读源码
jQuery 整体框架甚是复杂, 也不易读懂. 但是若想要在前端的路上走得更远, 更好, 研究分析前端的框架无疑是进阶路上必经之路. 但是庞大的源码往往让我们不知道从何处开始下手. 在很长的时间里我也被这种问题困扰着, 自己也慢慢摸索到一个比较不错的看源码的 "姿势".
一定不推荐的就是拿到源码直接开始啃, 首先我们一定要对这个框架的整体的架构有一定的了解, 每个模块之间的联系是怎样的;
然后找一找有没有关于源码分析的书籍, 如果有的话那么恭喜你了, 你可以直接跟着书的思路开始看源码;
如果没有框架源码分析相关书籍的话, 那么就只能自己啃源码了, 可以从成熟框架的早期源码开始看起, 这样一开始的代码量不多, 多看几遍还是可以理解的.
看源码时不仅要知道其然, 还要知道其所以然. 即不仅要知道这样写, 还需要知道为什么这样写. 这就要求我们不仅要看源码, 而且要敲源码, 换几种不同的思路来实现源码实现的功能能让我们更好的理解作者为什么这样写.
------------------------------------------------------------------------------------------ 分隔线, 下面介绍 jQuery 框架的实现核心思路.
为方便阅读和理解, 其核心代码只有 70 几行. 源码链接请点击这里, 如果对您有用的话, 欢迎 star. https://github.com/yuliangbin/yQuery
1,jQuery 框架总体架构
- (function(){
- // 替换全局的 $,jQuery 变量
- var
- _jQuery = window.jQuery,
- _$ = window.$,
- //jQuery 实现
- jQuery = window.jQuery = window.$ = function( selector, context ) {
- return new jQuery.fn.init( selector, context );
- };
- //jQuery 原型方法
- jQuery.fn = jQuery.prototype = {
- init: function( selector, context ) {},
- // 一些原型的属性和方法
- };
- // 原型替换
- jQuery.fn.init.prototype = jQuery.fn;
- // 原型扩展
- jQuery.extend = jQuery.fn.extend = function() { ... };
- jQuery.extend({
- // 一堆静态属性和方法
- });
- })();
2,$() 实现细节
我们知道使用 jQuery 的唯一入口就是全局属性 jQuery,$. 我们可以先实现一个 jQuery 类.
- var $ = jQuery = function () {
- };
- jQuery.fn = jQuery.prototype = {
- name : "jQuery",
- size : function () {
- return this.length;
- }
- };
- var my$ = new $();
- console.log(my$.name);
其实直接用 jQuery 生成一个 jQuery 实例, 也可以实现 jQuery 框架相同的效果. 但是 jQuery 框架并没有使用 new 为 jQuery 类创建一个新实例, 而是直接调用 jQuery() 方法, 然后在后面链式调用原型链上的方法. 如下所示:
$().size()
这是怎么实现的呢? 也就是说我们需要把 jQuery 即看作是一个类, 同时又是一个普通的函数. 而这个函数调用返回 jQuery 类的实例.
- var $ = jQuery = function () {
- return new jQuery();// 返回类的实例
- };
- jQuery.fn = jQuery.prototype = {
- name : "xiaoyu",
- size : function () {
- return this.length;
- }
- };
- var my$ = $();
- console.log($().name);
- //Uncaught RangeError: Maximum call stack size exceeded
执行上述代码, 提示内存外溢的错误, 说明执行 $() 时出现了循环引用. 可见执行 $() 不能返回 jQuery 的实例, 而应该返回其它类的实例才不会导致栈溢出. 实际上 jQuery 也是这么做的.
那么如何返回一个类的实例呢?
- var $ = jQuery = function () {
- return new jQuery.fn.init();// 产生一个 init() 的实例
- };
- jQuery.fn = jQuery.prototype = {
- init: function() {
- console.log(this);
- return this;
- },
- name : "xiaoyu",
- size : function () {
- return this.length;
- }
- };
- console.log($().__proto__ === jQuery.fn.init.prototype);//$().__proto__ -> init.prototype
执行上述代码, 执行 $() 返回了一个实例对象, 这已经很接近 jQuery 框架的.
但是还有一个原型指向问题: 在 jQuery 中, 执行 $() 函数返回的实例对象的__proto__指向的是 jQuery() 函数的 prototype 属性, 而我们自己实现的 jQuery 类执行 $() 返回的实例对象的__proto__指向的是 init() 函数的 prototype 属性.
所以我们在执行 $() 函数之前, 还需要手动改变 init() 函数的 prototype 指向, 使其指向 jQuery.prototype.
- var $ = jQuery = function () {
- return new jQuery.fn.init();// 产生一个 init() 的实例
- };
- jQuery.fn = jQuery.prototype = {
- init: function() {
- console.log(this);
- return this;
- },
- name : "xiaoyu",
- size : function () {
- return this.length;
- }
- };
- // 在实例化前, 将 init.prototype 覆盖为 jQuery.prototype
- jQuery.prototype.init.prototype = jQuery.prototype;
- console.log($().__proto__ === jQuery.prototype);//$().__proto__ -> jQuery.prototype
3, 实现一个简易的 DOM 选择器
第二讲我们已经完成了 jQuery 框架的基本的实现: 执行 $() 函数能够返回一个 jQuery 对象.
我们说过 $() 函数包含两个参数 selector 和 context. 其中 selector 表示选择器, context 表示选择器的选择的内容范围.$() 函数执行返回的是一个 jQuery 对象, 是一个类数组对象. 本质上是一个对象, 虽然拥有数组的 length 和 index, 却没有数组的其他方法.
在 jQuery 中, 假如我们需要操作一个 DOM 元素, 我们可以这样选中它.
$('div').html("hello");// 选中 document 下的所有 div 标签, 并设置所有选中的 DOM 元素的 innerHTML 内容
下面我们就实现一个简易的标签选择器的功能.
核心思路是:
通过传入的 selector 参数, 操作原生 JS 来实现 DOM 元素的过滤, 获取我们需要的 DOM 元素集合, 并将 DOM 元素集合作为属性添加到 jQuery 对象中, 并返回 jQuery 对象.
实现链式操作是通过在上一步操作结束时返回 jQuery 对象.
- var $ = jQuery = function (selector,context) { // 定义类
- return new jQuery.fn.init(selector,context); // 返回选择器的实例
- };
- jQuery.fn = jQuery.prototype = { //jQuery 的原型对象
- init: function(selector,context) { // 定义选择器的构造器
- selector = selector || document; // 默认值为 document
- context = context || document; // 默认值为 document
- if (selector.nodeType) { // 如果传入的参数是 DOM 节点
- this[0] = selector; // 把参数节点传递给实例对象的 index
- this.length = 1; // 设置长度为 1
- this.context = selector;
- return this; // 返回 jQuery 对象
- }
- if (typeof selector === 'string') {// 如果传进来的是标签字符串
- let ele = document.getElementsByTagName(selector); // 获取指定名称的元素
- for (let i = 0; i < ele.length; i++) { // 将获取到的元素放入实例对象中
- this[i] = ele[i];
- }
- this.length = ele.length;
- return this;
- } else {
- this.length = 0;
- this.context = context;
- return this;
- }
- },
- name : "jQuery",
- size : function () {
- return this.length;
- }
- };
- jQuery.prototype.init.prototype = jQuery.prototype;
- let div = $('div').size();
如上所述的代码,$() 函数已经基本传入 DOM 元素和元素标签返回一个 jQUery 对象的功能.
通过上面实现的一个简易的 DOM 选择器, 我们知道: jQuery 对象是通过 jQuery 框架包装 DOM 对象后产生的一个新的对象. 框架为 jQuery 对象定义了独立的方法和属性 (定义在 jQUery.prototype 原型属性上), 因此 jQuery 对象无法直接调用 DOM 对象的方法, DOM 对象也无法直接调用 jQuery 对象的方法.
我们也可以很轻易地实现 jQuery 对象和 DOM 对象的相互转换.
jQuery 对象转换为 DOM 对象: 借助 jQuery 对象的类数组下标选择 jQuery 对象中的某个 DOM 元素.
DOM 元素转换为 jQuery 对象: 直接把 DOM 元素当作参数传递给 $() 函数,$() 函数会自动把 DOM 对象包装为 jQuery 对象.
3.1, 实现 $('div').html("hello") 功能
核心思路: 在原型上封装一个 html() 函数, 根据传递进来的参数来判断是获取第一个 DOM 元素的 innerHTML 还是设置每一个 DOM 元素 innerHTML.
- var $ = jQuery = function (selector,context) { // 定义类
- return new jQuery.fn.init(selector,context); // 返回选择器的实例
- };
- jQuery.fn = jQuery.prototype = { //jQuery 的原型对象
- init: function(selector,context) {
- // 定义选择器的构造器
- // 省略初始化构造器的主体代码
- },
- constructor: jQuery,
- // 定义 jQuery 中的 html() 方法
- html: function(val) {
- if (val) {
- for(let i = 0; i < this['length']; i++){
- this[i].innerHTML = val;
- }
- }else {
- return this[0].innerHTML;
- }
- },
- name : "jQuery",
- size : function () {
- return this.length;
- }
- };
- jQuery.prototype.init.prototype = jQuery.prototype;
- let div = $('div').html('hello');
OK! 一个简易的 html() 函数的功能已经实现完成了, 我们可以看一下 jQuery 源码是如何实现的. 以便学习别人的编程思想.
- html: function( value ) {
- return value === undefined ?
- (this[0] ?
- this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") :
- null) :
- this.empty().append( value );
- },
- // 源码使用三目运算符判断参数是否为空, 如果为空, 则返回第一个元素的 innerHTML; 若不为空, 则先清空匹配元素中的内容, 并使用 append 插入值.
4, 功能扩展函数 extend
根据一般的习惯, 如果要为 jQuery 或者 jQuery.prototype 添加函数或方法, 可以直接通过 "." 语法实现, 或者在 jQuery.prototype 对象上添加一个属性即可.
但是分析 jQuery 源码可以知道 jQuery 是通过 extend() 函数来实现扩展功能的, 即插件功能.
这样做有什么好处呢?
extend 能够方便用户快速的扩展 jQuery 框架的功能, 但不会破坏 jQuery 框架的原型结构从而避免后期人工手动添加工具函数或方法时破坏 jQuery 结构的单纯性.
同时也方便管理. 如果不需要某个插件时简单的删除掉即可, 而不需要在 jQuery 框架源代码中去删除.
我们自己也可以实现一个简单的函数扩展功能, 只需把指定对象的方法复制给 jQuery 对象或者 jQuery.prototype 对象.
- // 接受一个对象作为参数 (实现批量的扩展)
- jQuery.extend = jQuery.prototype.extend = function (obj) {
- for (let key in obj) {
- if (obj.hasOwnProperty(key)) {
- this[key] = obj[key];
- }
- }
- return this;
- }
5, 命名空间问题
但还需要考虑的一个问题就是命名空间的问题: 当一个页面中存在多个框架或者众多代码时, 我们是很难确保代码不发生冲突的.
所以难免会出现命名冲突或代码覆盖的现象. 我们必须把 jQuery 代码封装在一个孤立的环境中, 避免其他代码的干扰.
我们可以通过匿名函数执行, 形成闭包, 将代码封装在一个封闭的环境中, 只通过唯一的入口 window.jQuery 访问.
- (function(){
- var jQuery = window.jQuery = window.$ = function( selector, context ) {
- // The jQuery object is actually just the init constructor 'enhanced'
- return new jQuery.fn.init( selector, context );
- };
- })(window);
5.1, 命名冲突
同时, 为了防止同其他框架协作时发生 $ 简写的冲突, 我们可以封装一个 noConflictl() 方法解决 $ 简写冲突.
思路分析: 在匿名执行 jQuery 框架的最前面, 先用_$,_jQuery 两个变量存储外部的 $,jQuery 的值. 执行 noConflict() 函数时再恢复外部变量 $,jQuery 的值.
- (function(){
- var
- window = this,
- _jQuery = window.jQuery,// 存储外部 jQuery 变量
- _$ = window.$,// 存储外部 $ 变量
- jQuery = window.jQuery = window.$ = function( selector, context ) {
- return new jQuery.fn.init( selector, context );
- };
- jQuery.noConflict = function( deep ) {
- window.$ = _$;// 将外部变量又重新赋值给 $
- if ( deep )
- window.jQuery = _jQuery;// 将外部变量又重新赋值给 jQuery
- return jQuery;
- },
- })();
至此, 我们已经模拟实现了一个简单的 jQuery 框架. 以后就可以根据 x 项目需要不断的扩展 jQUery 的方法即可.
PS: 写文章不宜, 如果这篇文章对您有帮助的话, 希望您多多点击推荐哦!
来源: https://www.cnblogs.com/yuliangbin/p/9286575.html