这篇依然是跟 dom 相关的方法,侧重点是操作样式的方法。

读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto

源码版本

本文阅读的源码为 zepto1.2.0

内部方法

classRE

classCache = {}

function classRE(name) {
return name in classCache ?
classCache[name] : (classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)'))
}

这个函数是用来返回一个正则表达式,这个正则表达式是用来匹配元素的 class 名的,匹配的是如 className1 className2 className3 这样的字符串。

calssCache 初始化时是一个空对象,用 name 用为 key ,如果正则已经生成过,则直接从 classCache 中取出对应的正则表达式。

否则,生成一个正则表达式,存储到 classCache 中,并返回。

来看一下这个生成的正则,'(^|\\s)' 匹配的是开头或者空白(包括空格、换行、tab缩进等),然后连接指定的 name ,再紧跟着空白或者结束。

maybeAddPx

cssNumber = { 'column-count': 1, 'columns': 1, 'font-weight': 1, 'line-height': 1, 'opacity': 1, 'z-index': 1, 'zoom': 1 }

function maybeAddPx(name, value) {
return (typeof value == "number" && !cssNumber[dasherize(name)]) ? value + "px" : value
}

在给属性设置值时,猜测所设置的属性可能需要带 px 单位时,自动给值拼接上单位。

cssNumber 是不需要设置 px 的属性值,所以这个函数里首先判断设置的值是否为 number 类型,如果是,并且需要设置的属性不在 cssNumber 中时,给值拼接上 px 单位。

defaultDisplay

elementDisplay = {}

function defaultDisplay(nodeName) {
var element, display
if (!elementDisplay[nodeName]) {
element = document.createElement(nodeName)
document.body.appendChild(element)
display = getComputedStyle(element, '').getPropertyValue("display")
element.parentNode.removeChild(element)
display == "none" && (display = "block")
elementDisplay[nodeName] = display
}
return elementDisplay[nodeName]
}

先透露一下,这个方法是给 .show() 用的,show 方法需要将元素显示出来,但是要显示的时候能不能直接将 display 设置成 block 呢?显然是不行的,来看一下 display 的可能会有那些值:

display: none

display: inline
display: block
display: contents
display: list-item
display: inline-block
display: inline-table
display: table
display: table-cell
display: table-column
display: table-column-group
display: table-footer-group
display: table-header-group
display: table-row
display: table-row-group
display: flex
display: inline-flex
display: grid
display: inline-grid
display: ruby
display: ruby-base
display: ruby-text
display: ruby-base-container
display: ruby-text-container
display: run-in display: inherit
display: initial
display: unset

如果元素原来的 display 值为 table ,调用 show 后变成 block 了,那页面的结构可能就乱了。

这个方法就是将元素显示时默认的 display 值缓存到 elementDisplay,并返回。

函数用节点名 nodeNamekey ,如果该节点显示时的 display 值已经存在,则直接返回。

element = document.createElement(nodeName)
document.body.appendChild(element)

否则,使用节点名创建一个空元素,并且将元素插入到页面中

display = getComputedStyle(element, '').getPropertyValue("display")
element.parentNode.removeChild(element)

调用 getComputedStyle 方法,获取到元素显示时的 display 值。获取到值后将所创建的元素删除。

display == "none" && (display = "block")
elementDisplay[nodeName] = display

如果获取到的 display 值为 none ,则将显示时元素的 display 值默认为 block。然后将结果缓存起来。display 的默认值为 none? Are you kiding me ? 真的有这种元素吗?还真的有,像 styleheadtitle 等元素的默认值都是 none 。将 styleheaddisplay 设置为 block ,并且将 stylecontenteditable 属性设置为 truestyle 就显示出来了,直接在页面上一边敲样式,一边看效果,爽!!!

关于元素的 display 默认值,可以看看这篇文章 Default CSS Display Values for Different HTML Elements

funcArg

function funcArg(context, arg, idx, payload) {
return isFunction(arg) ? arg.call(context, idx, payload) : arg
}

这个函数要注意,本篇和下一篇介绍的绝大多数方法都会用到这个函数。

例如本篇将要说到的 addClassremoveClass 等方法的参数可以为固定值或者函数,这些方法的参数即为形参 arg

当参数 arg 为函数时,调用 argcall 方法,将上下文 context ,当前元素的索引 idx 和原始值 payload 作为参数传递进去,将调用结果返回。

如果为固定值,直接返回 arg

className

function className(node, value) {
var klass = node.className || '',
svg = klass && klass.baseVal !== undefined if (value === undefined) return svg ? klass.baseVal : klass
svg ? (klass.baseVal = value) : (node.className = value)
}

className 包含两个参数,为元素节点 node 和需要设置的样式名 value

如果 value 不为 undefined(可以为空,注意判断条件为 value === undefined,用了全等判断),则将元素的 className 设置为给定的值,否则将元素的 className 值返回。

这个函数对 svg 的元素做了兼容,如果元素的 className 属性存在,并且 className 属性存在 baseVal 时,为 svg 元素,如果是 svg 元素,取值和赋值都是通过 baseVal 。对 svg 不是很熟,具体见文档: SVGAnimatedString.baseVal

.css()

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 })
}

css 方法有两个参数,property 是的 css 样式名,value 是需要设置的值,如果不传递 value 值则为取值操作,否则为赋值操作。

来看看调用方式:

css(property)   ⇒ value  // 获取值
css([property1, property2, ...]) ⇒ object // 获取值
css(property, value) ⇒ self // 设置值
css({ property: value, property2: value2, ... }) ⇒ self // 设置值

下面这段便是处理获取值情况的代码:

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
}
}

当为获取值时,css 方法必定只传递了一个参数,所以用 arguments.length < 2 来判断,用 css 方法来获取值,获取的是集合中第一个元素对应的样式值。

if (!element) return
return element.style[camelize(property)] || getComputedStyle(element, '').getPropertyValue(property)

propertystring 时,如果元素不存在,直接 return 掉。

如果 style 中存在对应的样式值,则优先获取 style 中的样式值,否则用 getComputedStyle 获取计算后的样式值。

为什么不直接获取计算后的样式值呢?因为用 style 获取的样式值是原始的字符串,而 getComputedStyle 顾名思义获取到的是计算后的样式值,如 style = "transform: translate(10px, 10px)"style.transform 获取到的值为 translate(10px, 10px),而用 getComputedStyle 获取到的是 matrix(1, 0, 0, 1, 10, 10)。这里用到的 camelize 方法是将属性 property 转换成驼峰式的写法,该方法在《读Zepto源码之内部方法》有过分析。

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
}

如果参数 property 为数组时,表示要获取一组属性的值。isArray 方法也在《读Zepto源码之内部方法》有过分析。

获取的方法也很简单,遍历 property ,获取 style 上对应的样式值,如果 style 上的值不存在,则通过 getComputedStyle 来获取,返回的是以样式名为 keyvalue 为对应的样式值的对象。

接下来是给所有元素设置值的情况:

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 })

这里定义了个变量 css 来接收需要新值的样式字符串。

if (type(property) == 'string') {
if (!value && value !== 0)
this.each(function() { this.style.removeProperty(dasherize(property)) })
else
css = dasherize(property) + ":" + maybeAddPx(property, value)
}

当参数 property 为字符串时

如果 value 不存在并且值不为 0 时(注意,valueundefined 时,已经在上面处理过了,也即是获取样式值),遍历集合,将对应的样式值从 style 中删除。

否则,拼接样式字符串,拼接成如 width:100px 形式的字符串。这里调用了 maybeAddPx 的方法,自动给需要加 px 的属性值拼接上了 px 单位。this.css('width', 100)this.css('width', '100px') 会得到一样的结果。

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]) + ';'

propertykey 是样式名,value 为样式值的对象时,用 for...in 遍历对象,接下来的处理逻辑跟 propertystring 时差不多,在做 css 拼接时,在末尾加了 ;,避免遍历时,将样式名和值连接在了一起。

.hide()

hide: function() {
return this.css("display", "none")
},

将集合中所有元素的 display 样式属性设置为 node,就达到了隐藏元素的目的。注意,css 方法中已经包含了 each 循环。

.show()

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)
})
},

hide 方法是直接将 display 设置为 none 即可,show 可不可以直接将需要显示的元素的 display 设置为 block 呢?

这样在大多数情况下是可以的,但是碰到像 tableli 等显示时 display 默认值不是 block 的元素,强硬将它们的 display 属性设置为 block ,可能会更改他们的默认行为。

show 要让元素真正显示,要经过两步检测:

this.style.display == "none" && (this.style.display = '')

如果 style 中的 display 属性为 none ,先将 style 中的 display 置为 ``。

if (getComputedStyle(this, '').getPropertyValue("display") == "none")
this.style.display = defaultDisplay(this.nodeName)
})

这样还未完,内联样式的 display 属性是置为空了,但是如果嵌入样式或者外部样式表中设置了 displaynone 的样式,或者本身的 display 默认值就是 none 的元素依然显示不了。所以还需要用获取元素的计算样式,如果为 none ,则将 display 的属性设置为元素显示时的默认值。如 table 元素的 style 中的 display 属性值会被设置为 table

.toggle()

toggle: function(setting) {
return this.each(function() {
var el = $(this);
(setting === undefined ? el.css("display") == "none" : setting) ? el.show(): el.hide()
})
},

切换元素的显示和隐藏状态,如果元素隐藏,则显示元素,如果元素显示,则隐藏元素。可以用参数 setting 指定 toggle 的行为,如果指定为 true ,则显示,如果为 falsesetting 不一定为 Boolean),则隐藏。

注意,判断条件是 setting === undefined ,用了全等,只有在不传参,或者传参为 undefined 的时候,条件才会成立。

.hasClass()

hasClass: function(name) {
if (!name) return false
return emptyArray.some.call(this, function(el) {
return this.test(className(el))
}, classRE(name))
},

判断集合中的元素是否存在指定 nameclass 名。

如果没有指定 name 参数,则直接返回 false

否则,调用 classRE 方法,生成检测样式名的正则,传入数组方法 some,要注意, some 里面的 this 值并不是遍历的当前元素,而是传进去的 classRE(name) 正则,回调函数中的 el 才是当前元素。具体参考文档 Array.prototype.some()

调用 className 方法,获取当前元素的 className 值,如果有一个元素匹配了正则,则返回 true

.addClass()

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(" "))
})
},

为集合中的所有元素增加指定类名 namename 可以为固定值或者函数。

如果 name 没有传递,则返回当前集合 this ,以进行链式操作。

如果 name 存在,遍历集合,判断当前元素是否存在 className 属性,如果不存在,立即退出循环。要注意,在 each 遍历中,this 指向的是当前元素。

classList = []
var cls = className(this),
newName = funcArg(this, name, idx, cls)

classList 用来接收需要增加的样式类数组。不太明白为什么要用全局变量 classList 来接收,用局部变量不是更好点吗?

cls 保存当前类的字符串,使用函数 className 获得。

newName 是需要新增的样式类字符串,因为 name 可以是函数或固定值,统一交由 funcArg 来处理。

newName.split(/\s+/g).forEach(function(klass) {
if (!$(this).hasClass(klass)) classList.push(klass)
}, this)
classList.length && className(this, cls + (cls ? " " : "") + classList.join(" "))

newName.split(/\s+/g) 是将 newName 字符串,用空白分割成数组。

再对数组遍历,得到单个类名,调用 hasClass 判断类名是否已经存在于元素的 className 中,如果不存在,将类名 push 进数组 classList 中。

如果 classList 不为空,则调用 className 方法给元素设置值。classList.join(" ") 是将类名转换成用空格分隔的字符串,如果 cls 即元素原来就存在有其他类名,拼接时也使用空格分隔开。

.removeClass()

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())
})
},

删除元素中指定的类 name 。如果不传递参数,则将 className 属性置为空,也即删除所有样式类。

classList = className(this)
funcArg(this, name, idx, classList).split(/\s+/g).forEach(function(klass) {
classList = classList.replace(classRE(klass), " ")
})
className(this, classList.trim())

这是的 classList 依然是全局变量,但是接收的是当前元素的当前样式类字符串(为什么不用局部变量呢?)。

参数 name 依然可以为函数或者固定值,因此用 funcArg 来处理,然后用空白分割成数组,再遍历得到单个样式类,调用 replace 方法,如果 classList 中能匹配到这个类,则将匹配的字符串替换成空格,这样就达到了删除的目的。

最后,用 trimclassList 的头尾空格去掉,调用 className 方法,重新给当前元素的 className 赋值。

.toggleClass()

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)
})
})
},

切换样式类,如果样式类不存在,则增加样式类,如果存在,则删除样式类。

toggleClass 接收两个参数,name 是需要切换的类名, when 是指定切换的方法,如果 whentrue ,则增加样式类,为 false ,则删除样式类。when 不一定要为 Boolean 类型。

这个方法跟 toggle 方法的逻辑参不多,只不过调用的方法变成 addClassremoveClass ,可以参考 toggle 的实现,不用过多分析。

系列文章

  1. 读Zepto源码之代码结构
  2. 读 Zepto 源码之内部方法
  3. 读Zepto源码之工具函数
  4. 读Zepto源码之神奇的$
  5. 读Zepto源码之集合操作
  6. 读Zepto源码之集合元素查找
  7. 读Zepto源码之操作DOM

参考

License

作者:对角另一面

读Zepto源码之样式操作的更多相关文章

  1. 读Zepto源码之属性操作

    这篇依然是跟 dom 相关的方法,侧重点是操作属性的方法. 读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto 源码版本 本文阅读的源码为 zepto1.2. ...

  2. 读Zepto源码之集合操作

    接下来几个篇章,都会解读 zepto 中的跟 dom 相关的方法,也即源码 $.fn 对象中的方法. 读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto 源码 ...

  3. 读Zepto源码之Event模块

    Event 模块是 Zepto 必备的模块之一,由于对 Event Api 不太熟,Event 对象也比较复杂,所以乍一看 Event 模块的源码,有点懵,细看下去,其实也不太复杂. 读Zepto源码 ...

  4. 读Zepto源码之Callbacks模块

    Callbacks 模块并不是必备的模块,其作用是管理回调函数,为 Defferred 模块提供支持,Defferred 模块又为 Ajax 模块的 promise 风格提供支持,接下来很快就会分析到 ...

  5. 读Zepto源码之Deferred模块

    Deferred 模块也不是必备的模块,但是 ajax 模块中,要用到 promise 风格,必需引入 Deferred 模块.Deferred 也用到了上一篇文章<读Zepto源码之Callb ...

  6. 读Zepto源码之Ajax模块

    Ajax 模块也是经常会用到的模块,Ajax 模块中包含了 jsonp 的现实,和 XMLHttpRequest 的封装. 读 Zepto 源码系列文章已经放到了github上,欢迎star: rea ...

  7. 读Zepto源码之Selector模块

    Selector 模块是对 Zepto 选择器的扩展,使得 Zepto 选择器也可以支持部分 CSS3 选择器和 eq 等 Zepto 定义的选择器. 在阅读本篇文章之前,最好先阅读<读Zept ...

  8. 读Zepto源码之Touch模块

    大家都知道,因为历史原因,移动端上的点击事件会有 300ms 左右的延迟,Zepto 的 touch 模块解决的就是移动端点击延迟的问题,同时也提供了滑动的 swipe 事件. 读 Zepto 源码系 ...

  9. 读Zepto源码之Gesture模块

    Gesture 模块基于 IOS 上的 Gesture 事件的封装,利用 scale 属性,封装出 pinch 系列事件. 读 Zepto 源码系列文章已经放到了github上,欢迎star: rea ...

随机推荐

  1. 2017年4月 TIOBE 编程语言排名

    2017年4月 TIOBE 编程语言排名 Hack是Facebook 在三年推出的PHP方言,在2017年4月首次进入TIOBE编程语言排行榜前50位. Hack原是Facebook的内部项目,与20 ...

  2. WPF 杂谈——入门介绍

    对于WPF的技术笔者是又爱又恨.现在WPF的市场并不是很锦气.如果以WPF来吃饭的话,只怕会饿死在街头.同时现在向面WEB开发更是如火冲天.所以如果是新生的话,最好不要以WPF为主.做为选择性来学习一 ...

  3. JavaScript中screen对象的两个属性

    Screen 对象 Screen 对象包含有关客户端显示屏幕的信息. 这里说一下今天用到的两个属性:availHeigth,availWidth avaiHeigth返回显示屏幕的高度 (除 Wind ...

  4. LeetCode 84. Largest Rectangle in Histogram 直方图里的最大长方形

    原题 Given n non-negative integers representing the histogram's bar height where the width of each bar ...

  5. 做一个常规的banner图——负边距的使用、banner图的拼法

    在这之前,首先要了解如何设置块级元素在块级元素水平居中 方法: 设置子容器为定位元素 水平居中 left:50%:margin-left:-width/2: 垂直居中 top:50%:margin-t ...

  6. final的用法

    先来看一段代码 class Car extends Vehicle {     public static void main (String[] args)     {         new  C ...

  7. poj3020二分图匹配

    The Global Aerial Research Centre has been allotted the task of building the fifth generation of mob ...

  8. 浅谈OA办公软件市场行情

    3.原文:http://www.jiusi.net/detail/472__776__3999__1.html 关键词:oa系统,OA办公软件 浅谈OA办公软件市场行情 中国的OA办公软件市场历经20 ...

  9. Spring Session实现分布式session的简单示例

    前面有用 tomcat-redis-session-manager来实现分布式session管理,但是它有一定的局限性,主要是跟tomcat绑定太紧了,这里改成用Spring Session来管理分布 ...

  10. C# Redis学习系列三:Redis配置主从

    Redis配置主从 主IP :端口      192.168.0.103 6666 从IP:端口       192.168.0.108 3333 配置从库 (1)安装服务: redis-server ...