最近看了 zepto 设计和源码分析 ,感觉有耳目一新的感觉,作者没有老套的去讲各个方法怎么实现的,能做些什么,而是讲了许多为什么源码要这样去设计,这样设计有什么好处,也分享了一些自己的看源码心得
一口气看完,收货良多,不光了解了 Zepto
的设计理念,同时也重温了 JavaScript
原型链的一些相关知识,在此记录一些笔记和涉及到的一些知识点,同时也感谢作者分享了加注版本的 zepto
源码,这样读起来也轻松不少,地址见 zepto-core-1.1.6 源码注视
大体结构 看了以后会发现,这一类库的最外层的设计都是大同小异的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 var Zepto = (function ( ) { var $, zepto = {} zepto.init = function (selector, context ) { return zepto.Z(dom, selector) } $ = function (selector, context ) { return zepto.init(selector, context) } zepto.Z.prototype = $.fn return $ })() window .Zepto = Zeptowindow .$ === undefined && (window .$ = Zepto)
首先,Zepto
是一个自执行的匿名函数(IIFE
),最后挂载在 window
对象身上,window.Zepto
和 window.$
都赋值给了 Zepto
这个变量,当我们平时在使用的时候,比如 $('div')
,依次会按照以下顺序来执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $ = function (selector, context ) { return zepto.init(selector, context) } ==> zepto.init = function (selector, context ) { return zepto.Z(dom, selector) } ==> zepto.Z = function (dom, selector ) { dom = dom || [] dom.__proto__ = $.fn dom.selector = selector || '' return dom }
接下来,我们分步来看
zepto.init 查看源码可以发现,其实 $
也是一个函数(同理也可得知 Zepto
也是一个函数)
1 2 3 $ = function (selector, context ) { return zepto.init(selector, context) }
它本身并没没有做什么具体的操作,而是直接返回了一个函数 zepto.init(selector, context)
,查看源码可知,init
函数中添加了一系列判断来梳理 selector
参数的各种可能,然后根据不同条件下对 DOM
变量进行赋值,最终,它将通过 selector
一起传递给 zepto.Z
函数并返回值,我们一条一条的来看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 zepto.init = function (selector, context ) { var dom if (!selector) return zepto.Z() else if (typeof selector == 'string' ) { selector = selector.trim() if (selector[0 ] == '<' && fragmentRE.test(selector)) dom = zepto.fragment(selector, RegExp .$1 , context), selector = null else if (context !== undefined ) return $(context).find(selector) else dom = zepto.qsa(document , selector) } else if (isFunction(selector)) return $(document ).ready(selector) else if (zepto.isZ(selector)) return selector else { if (isArray(selector)) dom = compact(selector) else if (isObject(selector)) dom = [selector], selector = null else if (fragmentRE.test(selector)) dom = zepto.fragment(selector.trim(), RegExp .$1 , context), selector = null else if (context !== undefined ) return $(context).find(selector) else dom = zepto.qsa(document , selector) } return zepto.Z(dom, selector) }
最后 init
函数的结构梳理如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 zepto.init = function (selector, context ) { var dom return zepto.Z(dom, selector) }
zepto.Z 先来看看之前的 zepto.Z
的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 zepto.Z = function (dom, selector ) { dom = dom || [] dom.__proto__ = $.fn dom.selector = selector || '' return dom }
首先需要明确,每一个函数,都有一个 prototype
的属性,同时又有一个 constructor
属性,指向该函数自身,如下图所示
所有通过函数 new
出来的对象,都有一个 __proto__
(隐式原型)指向这个函数的 prototype
(显式原型)
1 2 var arr = [] arr.__proto__ === Array .prototype
所以当你想要使用一个对象(或者一个数组)的某个功能时
如果该对象本身具有这个功能,则直接使用
如果该对象本身没有这个功能,则去 __proto__
中找(即构造函数的原型链上)
这就是为何数组会有 concat
、push
等方法,因为这些方法都存在于 Array.prototype
中,这也就解释了为什么 Zepto
取得的元素数组($('p')
返回的是一个元素数组)上会有类似 addClass
等方法了,原因就是 __proto__
肯定是被修改过的,而不仅仅是 Array.prototype
,这样再去看上面的 zepto.Z
函数就很明了了,新版的 zepto.Z
函数如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function Z (dom, selector ) { var i, len = dom ? dom.length : 0 for (i = 0 ; i < len; i++) this [i] = dom[i] this .length = len this .selector = selector || '' } zepto.Z = function (dom, selector ) { return new Z(dom, selector) } $.fn = { } zepto.Z.prototype = Z.prototype = $.fn
相较于老版,新版的 zepto.Z
函数直接将构造函数的原型修改了,即 Z.prototype = $.fn
,经过这样一改,构造函数再 new
出来的对象的隐式原型 __proto__
自然就指向了 $.fn
,而这时返回的是一个『对象数组』,相对于之前的单纯数组,本质上更灵活一些
区别如下 之前的 zepto.Z
函数
现在的 zepto.Z
函数
Zepto
整体的设计就是以上这些了,明白了 __proto__
(隐式原型)和 prototype
(显式原型)再来看的话就会清晰不少,其他各个函数,方法的实现,就慢慢去读源码来摸索认识了