一、前言
这篇研究zepto核心代码的实例方法。
二、源码
$.fn = {
constructor: zepto.Z,
length:
0,
forEach: emptyArray.forEach,
reduce: emptyArray.reduce,
push: emptyArray.push,
sort: emptyArray.sort,
splice: emptyArray.splice,
indexOf: emptyArray.indexOf,
concat:
function () {
var i, value, args = [];
for (i =
0; i <
arguments.length; i++) {
value =
arguments[i];
args[i] = zepto.isZ(value) ? value.toArray() : value
}
return concat.apply(zepto.isZ(
this) ?
this.toArray() :
this, args)
},
map:
function (fn) {
return $($.map(
this,
function (el, i) {
return fn.call(el, i, el)
}))
},
slice:
function () {
return $(slice.apply(
this,
arguments))
},
ready:
function (callback) {
if (readyRE.test(document.readyState) && document.body) callback($);
else document.addEventListener(
'DOMContentLoaded',
function () {
callback($)
},
false);
return this
},
get:
function (idx) {
return idx ===
undefined ? slice.call(
this) :
this[idx >=
0 ? idx : idx +
this.length]
},
toArray:
function () {
return this.get()
},
size:
function () {
return this.length
},
remove:
function () {
return this.each(
function () {
if (
this.parentNode !=
null)
this.parentNode.removeChild(
this)
})
},
each:
function (callback) {
emptyArray.every.call(
this,
function (el, idx) {
return callback.call(el, idx, el) !==
false
});
return this
},
filter:
function (selector) {
if (isFunction(selector))
return this.not(
this.not(selector));
return $(filter.call(
this,
function (element) {
return zepto.matches(element, selector)
}))
},
add:
function (selector, context) {
return $(uniq(
this.concat($(selector, context))))
},
is:
function (selector) {
return this.length >
0 && zepto.matches(
this[
0], selector)
},
not:
function (selector) {
var nodes = [];
if (isFunction(selector) && selector.call !==
undefined)
this.each(
function (idx) {
if (!selector.call(
this, idx)) nodes.push(
this)
});
else {
var excludes =
typeof selector ==
'string' ?
this.filter(selector) :
(likeArray(selector) && isFunction(selector.item)) ? slice.call(selector) : $(selector);
this.forEach(
function (el) {
if (excludes.indexOf(el) <
0) nodes.push(el)
})
}
return $(nodes)
},
has:
function (selector) {
return this.filter(
function () {
return isObject(selector) ?
$.contains(
this, selector) :
$(
this).find(selector).size()
})
},
eq:
function (idx) {
return idx === -
1 ?
this.slice(idx) :
this.slice(idx, +idx +
1)
},
first:
function () {
var el =
this[
0];
return el && !isObject(el) ? el : $(el)
},
last:
function () {
var el =
this[
this.length -
1];
return el && !isObject(el) ? el : $(el)
},
find:
function (selector) {
var result, $
this =
this;
if (!selector) result = $();
else if (
typeof selector ==
'object')
result = $(selector).filter(
function () {
var node =
this;
return emptyArray.some.call($
this,
function (parent) {
return $.contains(parent, node)
})
});
else if (
this.length ==
1) result = $(zepto.qsa(
this[
0], selector));
else result =
this.map(
function () {
return zepto.qsa(
this, selector)
});
return result;
},
closest:
function (selector, context) {
var nodes = [], collection =
typeof selector ==
'object' && $(selector);
this.each(
function (_, node) {
while (node && !(collection ? collection.indexOf(node) >=
0 : zepto.matches(node, selector)))
node = node !== context && !isDocument(node) && node.parentNode;
if (node && nodes.indexOf(node) <
0) nodes.push(node)
});
return $(nodes)
},
parents:
function (selector) {
var ancestors = [], nodes =
this;
while (nodes.length >
0)
nodes = $.map(nodes,
function (node) {
if ((node = node.parentNode) && !isDocument(node) && ancestors.indexOf(node) <
0) {
ancestors.push(node);
return node
}
});
return filtered(ancestors, selector)
},
parent:
function (selector) {
return filtered(uniq(
this.pluck(
'parentNode')), selector)
},
children:
function (selector) {
return filtered(
this.map(
function () {
return children(
this)
}), selector)
},
contents:
function () {
return this.map(
function () {
return this.contentDocument || slice.call(
this.childNodes)
})
},
siblings:
function (selector) {
return filtered(
this.map(
function (i, el) {
return filter.call(children(el.parentNode),
function (child) {
return child !== el
})
}), selector)
},
empty:
function () {
return this.each(
function () {
this.innerHTML =
''
})
},
pluck:
function (property) {
return $.map(
this,
function (el) {
return el[property]
})
},
show:
function () {
return this.each(
function () {
this.style.display ==
"none" && (
this.style.display =
'');
if (getComputedStyle(
this,
'').getPropertyValue(
"display") ==
"none")
this.style.display = defaultDisplay(
this.nodeName)
})
},
replaceWith:
function (newContent) {
return this.before(newContent).remove();
},
wrap:
function (structure) {
var func = isFunction(structure);
if (
this[
0] && !func)
var dom = $(structure).get(
0),
clone = dom.parentNode ||
this.length >
1;
return this.each(
function (index) {
$(
this).wrapAll(
func ? structure.call(
this, index) :
clone ? dom.cloneNode(
true) : dom
)
})
},
wrapAll:
function (structure) {
if (
this[
0]) {
$(
this[
0]).before(structure = $(structure));
var children;
while ((children = structure.children()).length) structure = children.first();
$(structure).append(
this)
}
return this
},
wrapInner:
function (structure) {
var func = isFunction(structure);
return this.each(
function (index) {
var self = $(
this), contents = self.contents(),
dom = func ? structure.call(
this, index) : structure;
contents.length ? contents.wrapAll(dom) : self.append(dom)
})
},
unwrap:
function () {
this.parent().each(
function () {
$(
this).replaceWith($(
this).children())
});
return this
},
clone:
function () {
return this.map(
function () {
return this.cloneNode(
true)
})
},
hide:
function () {
return this.css(
"display",
"none")
},
toggle:
function (setting) {
return this.each(
function () {
var el = $(
this);
(setting ===
undefined ? el.css(
"display") ==
"none" : setting) ? el.show() : el.hide()
})
},
prev:
function (selector) {
return $(
this.pluck(
'previousElementSibling')).filter(selector ||
'*')
},
next:
function (selector) {
return $(
this.pluck(
'nextElementSibling')).filter(selector ||
'*')
},
html:
function (html) {
return 0 in arguments ?
this.each(
function (idx) {
var originHtml =
this.innerHTML;
$(
this).empty().append(funcArg(
this, html, idx, originHtml))
}) :
(
0 in this ?
this[
0].innerHTML :
null)
},
text:
function (text) {
return 0 in arguments ?
this.each(
function (idx) {
var newText = funcArg(
this, text, idx,
this.textContent);
this.textContent = newText ==
null ?
'' :
'' + newText
}) :
(
0 in this ?
this.pluck(
'textContent').join(
"") :
null)
},
attr:
function (name, value) {
var result;
return (
typeof name ==
'string' && !(
1 in arguments)) ?
(
0 in this &&
this[
0].nodeType ==
1 && (result =
this[
0].getAttribute(name)) !=
null ? result :
undefined) :
this.each(
function (idx) {
if (
this.nodeType !==
1)
return;
if (isObject(name))
for (key
in name) setAttribute(
this, key, name[key])
else setAttribute(
this, name, funcArg(
this, value, idx,
this.getAttribute(name)))
})
},
removeAttr:
function (name) {
return this.each(
function () {
this.nodeType ===
1 && name.split(
' ').forEach(
function (attribute) {
setAttribute(
this, attribute)
},
this)
})
},
prop:
function (name, value) {
name = propMap[name] || name;
return (
1 in arguments) ?
this.each(
function (idx) {
this[name] = funcArg(
this, value, idx,
this[name])
}) :
(
this[
0] &&
this[
0][name])
},
removeProp:
function (name) {
name = propMap[name] || name;
return this.each(
function () {
delete this[name]
})
},
data:
function (name, value) {
var attrName =
'data-' + name.replace(capitalRE,
'-$1').toLowerCase();
var data = (
1 in arguments) ?
this.attr(attrName, value) :
this.attr(attrName);
return data !==
null ? deserializeValue(data) :
undefined
},
val:
function (value) {
if (
0 in arguments) {
if (value ==
null) value =
"";
return this.each(
function (idx) {
this.value = funcArg(
this, value, idx,
this.value)
})
}
else {
return this[
0] && (
this[
0].multiple ?
$(
this[
0]).find(
'option').filter(
function () {
return this.selected
}).pluck(
'value') :
this[
0].value)
}
},
offset:
function (coordinates) {
if (coordinates)
return this.each(
function (index) {
var $
this = $(
this),
coords = funcArg(
this, coordinates, index, $
this.offset()),
parentOffset = $
this.offsetParent().offset(),
props = {
top: coords.top - parentOffset.top,
left: coords.left - parentOffset.left
};
if ($
this.css(
'position') ==
'static') props[
'position'] =
'relative';
$
this.css(props)
});
if (!
this.length)
return null;
if (document.documentElement !==
this[
0] && !$.contains(document.documentElement,
this[
0]))
return {top:
0, left:
0};
var obj =
this[
0].getBoundingClientRect();
return {
left: obj.left + window.pageXOffset,
top: obj.top + window.pageYOffset,
width:
Math.round(obj.width),
height:
Math.round(obj.height)
}
},
css:
function (property, value) {
if (
arguments.length <
2) {
var element =
this[
0];
if (
typeof property ==
'string') {
if (!element)
return;
return element.style[camelize(property)] || getComputedStyle(element,
'').getPropertyValue(property)
}
else if (isArray(property)) {
if (!element)
return;
var props = {};
var computedStyle = getComputedStyle(element,
'');
$.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
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;
})
},
index:
function (element) {
return element ?
this.indexOf($(element)[
0]) :
this.parent().children().indexOf(
this[
0])
},
hasClass:
function (name) {
if (!name)
return false;
return emptyArray.some.call(
this,
function (el) {
return this.test(className(el))
}, classRE(name))
},
addClass:
function (name) {
if (!name)
return this;
return this.each(
function (idx) {
if (!(
'className' in this))
return;
classList = [];
var cls = className(
this), newName = funcArg(
this, name, idx, cls);
newName.split(
/\s+/g).forEach(
function (klass) {
if (!$(
this).hasClass(klass)) classList.push(klass)
},
this);
classList.length && className(
this, cls + (cls ?
" " :
"") + classList.join(
" "))
})
},
removeClass:
function (name) {
return this.each(
function (idx) {
if (!(
'className' in this))
return;
if (name ===
undefined)
return className(
this,
'');
classList = className(
this);
funcArg(
this, name, idx, classList).split(
/\s+/g).forEach(
function (klass) {
classList = classList.replace(classRE(klass),
" ")
});
className(
this, classList.trim())
})
},
toggleClass:
function (name, when) {
if (!name)
return this;
return this.each(
function (idx) {
var $
this = $(
this), names = funcArg(
this, name, idx, className(
this));
names.split(
/\s+/g).forEach(
function (klass) {
(when ===
undefined ? !$
this.hasClass(klass) : when) ?
$
this.addClass(klass) : $
this.removeClass(klass)
})
})
},
scrollTop:
function (value) {
if (!
this.length)
return;
var hasScrollTop =
'scrollTop' in this[
0];
if (value ===
undefined)
return hasScrollTop ?
this[
0].scrollTop :
this[
0].pageYOffset;
return this.each(hasScrollTop ?
function () {
this.scrollTop = value
} :
function () {
this.scrollTo(
this.scrollX, value)
})
},
scrollLeft:
function (value) {
if (!
this.length)
return;
var hasScrollLeft =
'scrollLeft' in this[
0];
if (value ===
undefined)
return hasScrollLeft ?
this[
0].scrollLeft :
this[
0].pageXOffset;
return this.each(hasScrollLeft ?
function () {
this.scrollLeft = value
} :
function () {
this.scrollTo(value,
this.scrollY)
})
},
position:
function () {
if (!
this.length)
return;
var elem =
this[
0],
offsetParent =
this.offsetParent(),
offset =
this.offset(),
parentOffset = rootNodeRE.test(offsetParent[
0].nodeName) ? {top:
0, left:
0} : offsetParent.offset();
offset.top -=
parseFloat($(elem).css(
'margin-top')) ||
0;
offset.left -=
parseFloat($(elem).css(
'margin-left')) ||
0;
parentOffset.top +=
parseFloat($(offsetParent[
0]).css(
'border-top-width')) ||
0;
parentOffset.left +=
parseFloat($(offsetParent[
0]).css(
'border-left-width')) ||
0;
return {
top: offset.top - parentOffset.top,
left: offset.left - parentOffset.left
}
},
offsetParent:
function () {
return this.map(
function () {
var parent =
this.offsetParent || document.body;
while (parent && !rootNodeRE.test(parent.nodeName) && $(parent).css(
"position") ==
"static")
parent = parent.offsetParent;
return parent
})
}
};
$.fn.detach = $.fn.remove;
[
'width',
'height'].forEach(
function (dimension) {
var dimensionProperty =
dimension.replace(
/./,
function (m) {
return m[
0].toUpperCase()
});
$.fn[dimension] =
function (value) {
var offset, el =
this[
0];
if (value ===
undefined)
return isWindow(el) ? el[
'inner' + dimensionProperty] :
isDocument(el) ? el.documentElement[
'scroll' + dimensionProperty] :
(offset =
this.offset()) && offset[dimension];
else return this.each(
function (idx) {
el = $(
this);
el.css(dimension, funcArg(
this, value, idx, el[dimension]()))
})
}
});
function traverseNode(node, fun) {
fun(node);
for (
var i =
0, len = node.childNodes.length; i < len; i++)
traverseNode(node.childNodes[i], fun)
}
adjacencyOperators.forEach(
function (operator, operatorIndex) {
var inside = operatorIndex %
2;
$.fn[operator] =
function () {
var argType, nodes = $.map(
arguments,
function (arg) {
var arr = [];
argType = type(arg);
if (argType ==
"array") {
arg.forEach(
function (el) {
if (el.nodeType !==
undefined)
return arr.push(el);
else if ($.zepto.isZ(el))
return arr = arr.concat(el.get());
arr = arr.concat(zepto.fragment(el))
});
return arr
}
return argType ==
"object" || arg ==
null ?
arg : zepto.fragment(arg)
}),
parent, copyByClone =
this.length >
1;
if (nodes.length <
1)
return this;
return this.each(
function (_, target) {
parent = inside ? target : target.parentNode;
target = operatorIndex ==
0 ? target.nextSibling :
operatorIndex ==
1 ? target.firstChild :
operatorIndex ==
2 ? target :
null;
var parentInDocument = $.contains(document.documentElement, parent);
nodes.forEach(
function (node) {
if (copyByClone) node = node.cloneNode(
true);
else if (!parent)
return $(node).remove();
parent.insertBefore(node, target);
if (parentInDocument) traverseNode(node,
function (el) {
if (el.nodeName !=
null && el.nodeName.toUpperCase() ===
'SCRIPT' &&
(!el.type || el.type ===
'text/javascript') && !el.src) {
var target = el.ownerDocument ? el.ownerDocument.defaultView : window;
target[
'eval'].call(target, el.innerHTML)
}
})
})
})
};
$.fn[inside ? operator +
'To' :
'insert' + (operatorIndex ?
'Before' :
'After')] =
function (html) {
$(html)[operator](
this);
return this
}
});
三、源码分析
1、prop与attr
prop是直接读取获取设置dom元素上的属性值,删除使用delete; attr是调用setAttribute、getAttribute和removeAttribute方法。
2、css
读取dom.style上的属性值(行内样式)或者调用getComputedStyle(element, ”).getPropertyValue(property)方法(计算样式); 设置css值时设置的是dom.style.cssText,不用去重,就算重复了也没关系,后面的属性值会覆盖前面的。
3、after、prepend、before、append、insertAfter、insertBefore、appendTo、prependTo
after、prepend、before、append巧妙的用了parent.insertBefore(node, target)方法来统一处理; insertAfter、insertBefore、appendTo、prependTo巧妙的反向调用对应的方法
4、cloneNode(true)
拷贝节点副本,为了避免同时添加一个元素多次,后面的操作会覆盖前面的(设置true为深拷贝,同时拷贝其子节点)
5、wrap、wrapAll、wrapInner
这几个方法有点绕,看个实例就知道了:
原文
<ul>
<li title='苹果'>苹果
</li>
<li title='橘子'>橘子
</li>
<li title='菠萝'>菠萝
</li>
</ul>
1、$("li").wrap("
<div></div>");
每一个选择器都添加
<ul>
<div><li title="苹果">苹果
</li></div>
<div><li title="橘子">橘子
</li></div>
<div><li title="菠萝">菠萝
</li></div>
</ul>
2、$("li").wrapAll("
<div></div>");
在所有选中的选择器最外面添加
<ul>
<div>
<li title="苹果">苹果
</li>
<li title="橘子">橘子
</li>
<li title="菠萝">菠萝
</li>
</div>
</ul>
3、$("li").wrapInner("
<div></div>");
为选择器的内容添加
<ul>
<li title='苹果'><div>苹果
</div></li>
<li title='橘子'><div>橘子
</div></li>
<li title='菠萝'><div>菠萝
</div></li>
</ul>