zepto源码分析·core模块

时间:2023-03-10 06:50:18
zepto源码分析·core模块

准备说明

  • 该模块定义了库的原型链结构,生成了Zepto变量,并将其以'Zepto'和'$'的名字注册到了window,然后开始了其它模块的拓展实现。
  • 模块内部除了对选择器和zepto对象的实现,就是一些工具方法和原型方法的定义。
  • 值得一提的是,内部很多实现都利用了原生数组的方法,很多api也是基于内部或公开的方法进一步拓展实现的。
  • 虽然该模块涉及的api非常多,但内部实现上比较统一,因此只会针对性地挑一些方法进行分析。

实现内容

var Zepto = (function () {
// 1.基础变量定义
// 2.内部方法实现
// 3.zepto对象实现——$('选择器')
// 4.$.extend方法实现
// 5.zepto.qsa内部方法和其它5个内部方法
// 6.全局方法实现——$.方法
// 7.原型方法实现——$().方法
// 8.原型链结构设置
}();
  • 上面是主体实现内容,代码定义顺序大致如上,某些地方会穿插定义

重点分析

选择器

选择器的实现逻辑放在了zepto.init方法中,由$()内部调用zepto.init方法,先处理选择器生成dom集合,然后将集合传入能生成zepto对象的内部方法,主体逻辑如下:

zepto.init = function(selector, context) {
// 1.如果selector为空
// 2.如果selector为字符串
a.标签字符串形式
b.context存在的处理
c.css选择器
// 3.如果selector为方法
// 4.如果selector为zepto对象
// 5.如果selector为其它情况
// 6.执行zepto对象生成方法
}

源码如下

zepto.init = function(selector, context) {
var dom
// 如果selector不存在(返回一个空的zepto对象)
if (!selector) return zepto.Z()
// 如果selector为字符串
else if (typeof selector == 'string') {
selector = selector.trim()
// 如果是标签字符串形式(在Chrome 21 and Firefox 15中,如果选择器不以'<'开头,会引发dom错误)
if (selector[0] == '<' && fragmentRE.test(selector))
dom = zepto.fragment(selector, RegExp.$1, context), selector = null
// 如果存在context属性,则应该以它为基础往下查找
else if (context !== undefined) return $(context).find(selector)
// 如果是css选择器
else dom = zepto.qsa(document, selector)
}
// 如果selector是方法
else if (isFunction(selector)) return $(document).ready(selector)
// 如果是zepto对象(直接返回)
else if (zepto.isZ(selector)) return selector
else {
// 如果是本身数组,则去掉不存在的
if (isArray(selector)) dom = compact(selector)
// 如果是对象,则存为数组形式
else if (isObject(selector))
dom = [selector], selector = null
// 这里重复了typeof selector == 'string'时的判断
// 因为typeof无法让new String()包装类型进入条件,因此通过最后的else再进行一次判断
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)
}
// 创建zepto集合
return zepto.Z(dom, selector)
}

zepto.qsa方法

// 处理css选择器的情况
zepto.qsa = function(element, selector){
var found,
maybeID = selector[0] == '#',
maybeClass = !maybeID && selector[0] == '.',
nameOnly = maybeID || maybeClass ? selector.slice(1) : selector,
// 是否为单一形式的选择器而非复杂形式
isSimple = simpleSelectorRE.test(nameOnly)
return (element.getElementById && isSimple && maybeID) ? // Safari游览器的DocumentFrament没有getElementById方法
// id选择器处理
( (found = element.getElementById(nameOnly)) ? [found] : [] ) :
// 非id选择器处理
(element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] :
slice.call(
isSimple && !maybeID && element.getElementsByClassName ? // DocumentFragment没有getElementsByClassName/TagName方法
maybeClass ? element.getElementsByClassName(nameOnly) : // 如果是class
element.getElementsByTagName(selector) : // 如果是标签
element.querySelectorAll(selector) // 其它都用querySelectorAll处理
)
}

zepto.fragment方法

// 处理标签字符串的情况
zepto.fragment = function(html, name, properties) {
var dom, nodes, container
// 如果是无标签内容的标签,则直接创建并赋值给dom变量
if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1))
// 如果是有内容的标签,则走其他的判断流程
if (!dom) {
// 保证标签字符串为双标签形式
if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>")
// 如果没有从html字符串中获取到标签名,则再次获取
if (name === undefined) name = fragmentRE.test(html) && RegExp.$1
// 如果标签名为非常规字符串,则name置为'*',容器container设置为div
if (!(name in containers)) name = '*'
// 通过容器标签将html字符串dom化
container = containers[name]
container.innerHTML = '' + html
// 返回dom集合,清空container
dom = $.each(slice.call(container.childNodes), function(){
container.removeChild(this)
})
}
// 如果是纯粹的object对象
if (isPlainObject(properties)) {
nodes = $(dom)
$.each(properties, function(key, value) {
// 如果是库的特殊属性名,则应该通过方法调用
if (methodAttributes.indexOf(key) > -1) nodes[key](value)
// 如果不是,则设置集合的节点属性
else nodes.attr(key, value)
})
}
return dom
}

zepto对象

Zepto对象的生成依赖于一连串相关的内部方法,源码实现如下

// zepto对象工厂————第四步
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.fragment = function(html, name, properties) {
// 源码不再赘述...
} // 生成zepto对象————第三步
zepto.Z = function(dom, selector) {
// 通过模块的原型链结构,可获得各种操作方法
return new Z(dom, selector)
} // 判断是否为zepto对象
zepto.isZ = function(object) {
return object instanceof zepto.Z
} // 生成dom集合,然后进行zepto集合创建————第二步
zepto.init = function(selector, context) {
// 生成dom集合
// 执行zepto.Z方法
// 源码不再赘述...
return zepto.Z(dom, selector)
} // 获取zepto对象————第一步
$ = function(selector, context){
return zepto.init(selector, context)
} // 处理css选择器
zepto.qsa = function(element, selector){
// 源码不再赘述...
} // 设置原型方法
$.fn = { } // 设置原型链结构
zepto.Z.prototype = Z.prototype = $.fn // 返回给外部Zepto变量,然后通过window注册
return $

部分api方法

  • 工具方法和原型方法太多,只能挑几个典型了
  • extend和ready的实现都比较简单
  • css方法的实现主要还是对原生element.style和getComputedStyle的熟悉,对属性名转换和样式单位的自动添加的考虑
  • 插入操作的几个方法实现比较精巧,可以认真研究下

$.extend

function extend(target, source, deep) {
for (key in source)
// 如果是深拷贝
// isArray判断是否为数组类型
// isPlainObject判断是否为纯粹的object类型
if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
// 如果拷贝对象的属性值为Object,而目标对象的属性值不为Object,则目标对象的属性值重置为Object
if (isPlainObject(source[key]) && !isPlainObject(target[key]))
target[key] = {}
// 如果拷贝对象的属性值为Array,而目标对象的属性值不为Array,则目标对象的属性值重置为Array
if (isArray(source[key]) && !isArray(target[key]))
target[key] = []
// 进行深拷贝
extend(target[key], source[key], deep)
}
// 如果是浅拷贝
else if (source[key] !== undefined) target[key] = source[key]
} $.extend = function(target){
var deep, args = slice.call(arguments, 1)
// 如果存在深/浅拷贝设置
if (typeof target == 'boolean') {
// 存储深浅拷贝判断值
deep = target
// 获得目标对象
target = args.shift()
}
// 遍历拷贝对象
args.forEach(function(arg){ extend(target, arg, deep) })
return target
}

$.fn.ready

$.fn = {
ready: function(callback){
// 需要检查IE是否存在document.body,因为浏览器在尚未创建body元素时会报告文档就绪
if (readyRE.test(document.readyState) && document.body) callback($)
else document.addEventListener('DOMContentLoaded', function(){ callback($) }, false)
return this
}
}

$.fn.css

$.fn = {
css: function(property, value){
// 如果是获取属性(参数小于2)
if (arguments.length < 2) {
// 取第一个元素
var element = this[0]
// 如果是字符串类型
if (typeof property == 'string') {
if (!element) return
// element.style用于访问直接样式,getComputedStyle用于访问层叠后的样式
// camelize方法用于将a-b,转换aB,驼峰转换
return element.style[camelize(property)] || getComputedStyle(element, '').getPropertyValue(property)
// 如果是数组类型
} else if (isArray(property)) {
if (!element) return
var props = {}
// 存储层叠样式集
var computedStyle = getComputedStyle(element, '')
// 遍历数组样式名,生成样式对象props返回
$.each(property, function(_, prop){
props[prop] = (element.style[camelize(prop)] || computedStyle.getPropertyValue(prop))
})
return props
}
} var css = ''
if (type(property) == 'string') {
// 如果属性值不存在,遍历删除
if (!value && value !== 0)
this.each(function(){ this.style.removeProperty(dasherize(property)) })
else
// dasherize用于将property规范化为'a-b'形式
// maybeAddPx用于自动增加px
css = dasherize(property) + ":" + maybeAddPx(property, value)
} else {
for (key in property)
// 如果属性值不存在,遍历删除
if (!property[key] && property[key] !== 0)
this.each(function(){ this.style.removeProperty(dasherize(key)) })
else
css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';'
}
// 遍历设置样式字符串
return this.each(function(){ this.style.cssText += ';' + css })
}
}

$.fn.插入

$.fn = {
// 递归执行节点处理函数
function traverseNode(node, fun) {
fun(node)
for (var i = 0, len = node.childNodes.length; i < len; i++)
traverseNode(node.childNodes[i], fun)
} // after|before|append|prepend与insertAfter|insertBefore|appendTo|prepend实现
// adjacencyOperators = [ 'after', 'prepend', 'before', 'append' ]
adjacencyOperators.forEach(function(operator, operatorIndex) {
// inside为0和2, after|before(标签外插入)
// inside为1和3, append|prepend(标签内插入)
var inside = operatorIndex % 2 // after|before|append|prepend实现
$.fn[operator] = function(){
// 获得dom数组
var argType, nodes = $.map(arguments, function(arg) {
var arr = []
// 判断参数项类型
argType = type(arg)
// 如果是array类型,处理成dom数组返回
if (argType == "array") {
arg.forEach(function(el) {
// 如果el是dom对象
if (el.nodeType !== undefined) return arr.push(el)
// 如果el是zepto对象
else if ($.zepto.isZ(el)) return arr = arr.concat(el.get())
// 如果el是标签字符串
arr = arr.concat(zepto.fragment(el))
})
return arr
}
// 如果为object类型或不为null,就返回自身;否则作为标签字符串处理,返回dom
return argType == "object" || arg == null ?
arg : zepto.fragment(arg)
}),
parent, copyByClone = this.length > 1 // 如果不存在,则返回自身
if (nodes.length < 1) return this // 处理dom数组
return this.each(function(_, target){
// inside为0和2, 处理的是after|before, 用父级做容器
// inside为1和3, 处理的是append|prepend, 用自身做容器
parent = inside ? target : target.parentNode
// target在insertBefore方法中用作参照点
// operatorIndex == 0, 操作的是after方法, 取下一个节点
// operatorIndex == 1, 操作的是prepend方法, 取第一个子节点
// operatorIndex == 2, 操作的是before方法, 取自身
// operatorIndex == 3, 操作的是append方法, 取null
target = operatorIndex == 0 ? target.nextSibling :
operatorIndex == 1 ? target.firstChild :
operatorIndex == 2 ? target :
null
// 是否在html内
var parentInDocument = $.contains(document.documentElement, parent)
// 遍历处理插入的节点
nodes.forEach(function(node){
// 因为操作对象可能有多个, 而被插入node只有1个
// 如果要让所有操作对象都被插入内容,需要对插入node进行深克隆
if (copyByClone) node = node.cloneNode(true)
// 如果不存在parent,则删除插入的节点
else if (!parent) return $(node).remove()
// 插入节点
parent.insertBefore(node, target)
// 如果在html内,就递归执行,遇到有js代码的script标签,就执行它
if (parentInDocument) traverseNode(node, function(el){
if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' &&
(!el.type || el.type === 'text/javascript') && !el.src){
// 这里用到ownerDocument判断而不直接用window,是考虑到页面有可能是iframe引入的
var target = el.ownerDocument ? el.ownerDocument.defaultView : window
target['eval'].call(target, el.innerHTML)
}
})
})
})
} // insertAfter|insertBefore|appendTo|prepend实现
$.fn[inside ? operator+'To' : 'insert'+(operatorIndex ? 'Before' : 'After')] = function(html){
$(html)[operator](this)
return this
}
})
}