关于读源码,读jQuery自然是不错,但太过于庞大不易解读,对于小白,最好从Zepto,Lodash这样的小库入手。
这里使用的是zepto1.1.6版本为例。
自执行函数
在阅读之前,先弄清楚闭包和自执行函数
两种方式: (function() {})() 和 (function() {}())
1 (function() { 2 console.log('这里直接执行') 3 })()
1 (function () { 2 console.log('这里直接执行') 3 }())
自执行函数的优势在于,避免了全局变量的污染,即在自执行函数体内声明的变量,外部是无法访问到的。
1 (function () { 2 let val = '123' 3 })() 4 5 console.log(val) // val is not defined
如需要获取变量val的值,只能在函数体内部返回,然后将函数赋值给一个全局变量
1 let temp= (function () { 2 let val = '123'; 3 return val; // 必须return, 否则获取不到值 4 })() 5 6 window.temp = temp; 7 8 console.log(temp) // 123
而关于闭包,简单理解就是在自执行函数内部声明变量或方法,在自执行函数内就形成了一个闭合的上下文环境,外部是无法直接访问的。
源码结构
1 ;let Zepto = (function () { 2 let $, zepto = {}; 3 4 return $; // 最终返回值 5 })() 6 7 window.Zepto = Zepto; 8 window.$ === undefined && (window.$ = Zepto);
首先声明一个字支持函数并赋给变量Zepto, 再将变量Zepto赋给全局变量window.Zepto
如果$未被声明,则也将Zepto赋给window.$全局变量
此时,外部访问Zepto或者$都可以返回自执行函数内部的$(即 return $)
再看$到底是什么
$返回一个函数,传入的参数是selector选择器(即tagName,id,或className),context暂为空。
1 ;let Zepto = (function () { 2 let $, zepto = {}; 3 $ = function(selector, context) { 4 console.log('获取节点'); 5 return zepto.init(selector, context); 6 }; 7 return $; 8 })(); 9 10 window.Zepto = Zepto; 11 window.$ === undefined && (window.$ = Zepto);
此时,如果访问节点, 会输出'获取节点'
1 <div> 2 <span>测试</span> 3 <span>测试</span> 4 <span>测试</span> 5 </div>
1 let span = $('span'); 2 console.log(span)
这里报错是因为init方法还未定义;
继续创建 init方法
init函数是绑定在zepto对象上的方法(之前已经声明一个zepto空对象)
1 zepto.init = function(selector, context) { 2 let dom;
// 处理dom的代码 3 return zepto.Z(dom, selector); 4 };
init处理dom部分,分为几种情况
1 . 当不传参数 $(), 直接返回zepto.Z()不传参
1 if (!selector) return zepto.Z()
2 . 当传入字符串参数,又分为几种
1 else if (typeof selector == 'string') { 2 selector = selector.trim() 3 4 if (selector[0] == '<' && fragmentRE.test(selector)) 5 dom = zepto.fragment(selector, RegExp.$1, context), selector = null 6 7 else if (context !== undefined) return $(context).find(selector) 8 9 else dom = zepto.qsa(document, selector) //这里的qsa其实就是document.querySelectorAll() 10 }
(1) $(' span ')
首先将清除字符串前后空格 selector.trim()
(2) $('<div></div>')
如果第一个字符是<,并且正则fragmentRE成立,
则使用fragment方法处理dom节点
1 if (selector[0] == '<' && fragmentRE.test(selector)) 2 dom = zepto.fragment(selector, RegExp.$1, context), selector = null
(3) $('span', 'div')
如果传入第二个参数,即之前的context不等于undefined,则相当于执行$('div').find('span')
1 else if (context !== undefined) return $(context).find(selector)
若div中存在span则返回span,否则返回空数组。
(4)如以上三种不满足则执行最后一步,zepto.qsa()
3 . 如果传入函数,用document调用一个zepto对象,然后$.ready()方法, ready方法和上面的find方法一样在后面创建。
1 else if (isFunction(selector)) return $(document).ready(selector);
4 . 若已经是一个zepto对象,则直接返回该对象,不必再调用$(), 即 $($('span')) === $('span')
1 else if (zepto.isZ(selector)) return selector
1 zepto.isZ = function(object) { 2 return object instanceof zepto.Z 3 }
5 . 若是引用类型
1 else { 2 // 如参数是数组,则调用compact方法, 会将null或undefined剔除 3 if (isArray(selector)) dom = compact(selector) 4 // 如是DOM节点,则数组化 5 else if (isObject(selector)) 6 dom = [selector], selector = null 7 // 以下和上面字符串参数的重复,何意未知。 8 else if (fragmentRE.test(selector)) 9 dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null 10 // 11 // 12 else if (context !== undefined) return $(context).find(selector) 13 // 14 else dom = zepto.qsa(document, selector) 15 }
init是一个初始化方法,调用init时去执行zepto.Z()函数
1 zepto.Z = function(dom, selector) { 2 dom = dom || [] 3 dom.__proto__ = $.fn; 4 dom.selector = selector || ''; 5 console.log(dom) 6 console.log(selector) 7 return dom; 8 };
关于Z()
1 . 传入dom,如果未传则返回[]空数组
如果不传DOM节点 $(), 则返回一个空值。
2 . 如果有dom $('span'),则
3 . 同时在该数组的原型对象上创建$.fn对象
在未定义$.fn之前,dom.__proto__原型对象上的方法是Array构造函数的内置方法
而这里使用了dom.__proto__ = $.fn 重新定义了原型对象上的方法
1 $.fn = { 2 get: function () { 3 console.log('自定义get方法') 4 }, 5 on: function () { 6 console.log('自定义on方法') 7 } 8 }
可以看出,通过原型对象上创建新方法后,原来的内置方法都没有了。
如果只是想追加方法,则应该在原型对象上添加属性。
dom.__proto__.get = function() {}
dom.__proto__.on = function() {}
4 . 最后返回该DOM节点对象。此时$('span')对象就可以调用方法get和on了。