今天我们读源码, Let’s go.
相信打开这篇文章准备学习的你 , 一定知道阅读源码 , 学习其中设计思想的重要性 , 今天来和我一起阅读一款经典JS库的源码 , Zepto.js.
学习前提
- JS基础良好
- 原型和原型链基础知识
- 闭包基础知识
- 作用域和执行期上下文基础知识
- 单线程和异步
- 熟悉Zepto.js的常用API
学习目标
- 分析zepto的设计思想
- 看源码的实现方式
先来感受一下Zepto.js和原生JS在使用方面的一点区别
// html
<div>
<span>1</span>
<span>2</span>
<span>3</span>
</div>
// JS
var arr1 = [1, 2, 3];
arr1.push(4);
console.log( arr1 ); // [1, 2, 3, 4]
console.log( arr1 instanceod Array ); // true
console.log(arr1.addClass); // undefined
// Zepto.js
var arr2 = $('span');
arr2.push(4);
console.log(arr2); // [1, 2, 3, 4] // [span, span, span, 4, selector: "span"]
console.log( arr2 instanceod Array ); // false
console.log(arr2.addClass); // fn(t){...}
- 这里很明显,arr1和arr2都形同数组,但arr1是真正的数组,arr2却是个“伪数组”。
- 读到这里, 你肯定会好奇为什么arr1没有addClass方法,回想原型链的基础知识,既然arr1没有addClass方法,那么我们就给arr1加上addClass方法
arr1.__proto__.addClass = function(){console.log('addClass!')}
arr1.addClass(); // addClass!
- 你或许也会好奇为什么arr2是个"伪数组",却拥有数组的push方法,它的addClass方法又是哪里来的,这里我们模拟一下
var arr2 = $('span');
arr2.__proto__ = {
addClass : function(){console.log('addClass!')},
push : Array.prototype.push,
splice : Array.prototype.splice // 记得吗?类数组一旦拥有splice方法就会和数组长得很像!这个伪军!
}
arr2.addClass(); // addClass!
arr2.push(4); // [span, span, span, 4, selector: "span"]
接下来解开Zepto_1.1.6的面纱
-
分析核心模块代码 zepto-core-docs
-
首先看源码的最外层代码结构
// 立即执行函数私有化变量防止污染全局
var Zepto = (function(){})()
// Zepto挂载到window
window.Zepto = Zepto
// 先确保window.$为undefined再将window.$指向Zepto
window.$ === undefined && (window.$ = Zepto)
-
结果就是window.$和window.Zepto都指向立即执行函数的返回值
-
然后看看立即执行函数执行完到底返回了什么
var Zepto = (function(){
var $;
$ = function(selector, context){
return zepto.init(selector, context)
}
return $;
})()
- 发现返回的是一个函数,我们平时执行代码
$()
就是执行返回的该函数,最终的返回结果是init方法执行结果的返回值,接下来我们顺藤摸瓜,研究init方法
var Zepto = (function(){
var $,
zepto = {};
zepto.init = function(selector, context) {
var dom;
// 先不分析Z函数
if (!selector) return zepto.Z()
// 如果selector是字符串又分好几种情况
else if (typeof selector == 'string') {...}
// 如果selector是个方法, 保证DOM加载完成后再执行该方法
else if (isFunction(selector)) return $(document).ready(selector)
// 如果selector本身就是个Zepto对象就直接返回
else if (zepto.isZ(selector)) return selector
// else又分好几种情况(例如selector是数组对象等),结合API文档和源码注释就能看明白
else {...}
// 先不分析Z函数
return zepto.Z(dom, selector);
}
$ = function(selector, context){
return zepto.init(selector, context)
}
return $;
})()
- 发现返回的结果无外乎是zepto.Z方法的执行结果,接下来分析zepto.Z方法
var Zepto = (function(){
var $,
zepto = {};
zepto.Z = function(dom, selector) {
dom = dom || []
dom.__proto__ = $.fn
dom.selector = selector || ''
return dom // [span, span, span, 4, selector: "span"]
}
zepto.init = function(selector, context) {
var dom;
// 此处省略一大堆分支判断
return zepto.Z(dom, selector);
}
$ = function(selector, context){
return zepto.init(selector, context)
}
return $;
})()
- 回想平时调用Zepto方法返回的Zepto对象的样貌大致长这个样子 : [span, span, span, 4, selector: “span”],显然唯一有疑问的就是Z方法中dom.__proto__指向的$.fn是啥,接下来分析 $.fn 对象
$.fn = { 一堆Zepto对象的常用方法 } // 哈哈,这里面就都是Zepto对象的方法了
zepto.Z.prototype = $.fn // zepto.Z.prototype指向$.fn对象,这是由于$.fn写起来短嘛,而且Zepto中定义的函数$在JS中本质也是对象,就可以加属性
- Zepto.js_1.2.0对如上返回数组的方式进行了改进,将zepto.Z方法中的逻辑抽象出来成为一个构造函数Z,该构造函数返回值类型为类数组,zepto.Z方法返回该构造函数的实例化对象,然后将该构造函数的原型指向$.fn
zepto.Z.prototype = Z.prototype = $.fn