fx
模組為利用 CSS3
的過渡和動畫的屬性為 Zepto
提供了動畫的功能,在 fx
模組中,只做了事件和樣式瀏覽器字首的補全,沒有做太多的相容。對於不支援 CSS3
過渡和動畫的, Zepto
的處理也相對簡單,動畫立即完成,馬上執行回撥。
讀 Zepto 原始碼系列文章已經放到了github上,歡迎star: reading-zepto
原始碼版本
本文閱讀的原始碼為 zepto1.2.0
GitBook
內部方法
dasherize
function dasherize(str) { return str.replace(/([A-Z])/g, '-$1').toLowerCase() }複製程式碼
這個方法是將駝峰式( camleCase
)的寫法轉換成用 -
連線的連詞符的寫法( camle-case
)。轉換的目的是讓寫法符合 css
的樣式規範。
normalizeEvent
function normalizeEvent(name) { return eventPrefix ? eventPrefix + name : name.toLowerCase() }複製程式碼
為事件名增加瀏覽器字首。
為事件和樣式增加瀏覽器字首
變數
var prefix = '', eventPrefix,
vendors = { Webkit: 'webkit', Moz: '', O: 'o' },
testEl = document.createElement('div'),
supportedTransforms = /^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)$/i,
transform,
transitionProperty, transitionDuration, transitionTiming, transitionDelay,
animationName, animationDuration, animationTiming, animationDelay,
cssReset = {}複製程式碼
vendors
定義了瀏覽器的樣式字首( key
) 和事件字首 ( value
) 。
testEl
是為檢測瀏覽器字首所建立的臨時節點。
cssReset
用來儲存加完字首後的樣式規則,用來過渡或動畫完成後重置樣式。
瀏覽器字首檢測
if (testEl.style.transform === undefined) $.each(vendors, function(vendor, event){
if (testEl.style[vendor + 'TransitionProperty'] !== undefined) {
prefix = '-' + vendor.toLowerCase() + '-'
eventPrefix = event
return false
}
})複製程式碼
檢測到瀏覽器不支援標準的 transform
屬性,則依次檢測加了不同瀏覽器字首的 transitionProperty
屬性,直至找到合適的瀏覽器字首,樣式字首儲存在 prefix
中, 事件字首儲存在 eventPrefix
中。
初始化樣式
transform = prefix + 'transform'
cssReset[transitionProperty = prefix + 'transition-property'] =
cssReset[transitionDuration = prefix + 'transition-duration'] =
cssReset[transitionDelay = prefix + 'transition-delay'] =
cssReset[transitionTiming = prefix + 'transition-timing-function'] =
cssReset[animationName = prefix + 'animation-name'] =
cssReset[animationDuration = prefix + 'animation-duration'] =
cssReset[animationDelay = prefix + 'animation-delay'] =
cssReset[animationTiming = prefix + 'animation-timing-function'] = ''複製程式碼
獲取瀏覽器字首後,為所有的 transition
和 animation
屬性加上對應的字首,都初始化為 ''
,方便後面使用。
方法
$.fx
$.fx = {
off: (eventPrefix === undefined && testEl.style.transitionProperty === undefined),
speeds: { _default: 400, fast: 200, slow: 600 },
cssPrefix: prefix,
transitionEnd: normalizeEvent('TransitionEnd'),
animationEnd: normalizeEvent('AnimationEnd')
}複製程式碼
- off: 表示瀏覽器是否支援過渡或動畫,如果既沒有瀏覽器字首,也不支援標準的屬性,則判定該瀏覽器不支援動畫
- speeds: 定義了三種動畫持續的時間, 預設為
400ms
- cssPrefix: 樣式瀏覽器相容字首,即
prefix
- transitionEnd: 過渡完成時觸發的事件,呼叫
normalizeEvent
事件加了瀏覽器字首補全 - animationEnd: 動畫完成時觸發的事件,同樣加了瀏覽器字首補全
animate
$.fn.animate = function(properties, duration, ease, callback, delay){
if ($.isFunction(duration))
callback = duration, ease = undefined, duration = undefined
if ($.isFunction(ease))
callback = ease, ease = undefined
if ($.isPlainObject(duration))
ease = duration.easing, callback = duration.complete, delay = duration.delay, duration = duration.duration
if (duration) duration = (typeof duration == 'number' ? duration :
($.fx.speeds[duration] || $.fx.speeds._default)) / 1000
if (delay) delay = parseFloat(delay) / 1000
return this.anim(properties, duration, ease, callback, delay)
}複製程式碼
我們平時用得最多的是 animate
這個方法,但是這個方法最終呼叫的是 anim
這個方法,animate
這個方法相當靈活,因為它主要做的是引數修正的工作,做得引數適應 anim
的介面。
引數:
- properties:需要過渡的樣式物件,或者
animation
的名稱,只有這個引數是必傳的 - duration: 過渡時間
- ease: 緩動函式
- callback: 過渡或者動畫完成後的回撥函式
- delay: 過渡或動畫延遲執行的時間
修正引數
if ($.isFunction(duration))
callback = duration, ease = undefined, duration = undefined複製程式碼
這是處理傳參為 animate(properties, callback)
的情況。
if ($.isFunction(ease))
callback = ease, ease = undefined複製程式碼
這是處理 animate(properties, duration, callback)
的情況,此時 callback
在引數 ease
的位置
if ($.isPlainObject(duration))
ease = duration.easing, callback = duration.complete, delay = duration.delay, duration = duration.duration複製程式碼
這是處理 animate(properties, { duration: msec, easing: type, complete: fn })
的情況。除了 properties
,後面的引數還可以寫在一個物件中傳入。
如果檢測到為物件的傳參方式,則將對應的值從物件中取出。
if (duration) duration = (typeof duration == 'number' ? duration :
($.fx.speeds[duration] || $.fx.speeds._default)) / 1000複製程式碼
如果過渡時間為數字,則直接採用,如果是 speeds
中指定的 key
,即 slow
、fast
甚至 _default
,則從 speeds
中取值,否則用 speends
的 _default
值。
因為在樣式中是用 s
取值,所以要將毫秒數除 1000
。
if (delay) delay = parseFloat(delay) / 1000複製程式碼
也將延遲時間轉換為秒。
anim
$.fn.anim = function(properties, duration, ease, callback, delay){
var key, cssValues = {}, cssProperties, transforms = '',
that = this, wrappedCallback, endEvent = $.fx.transitionEnd,
fired = false
if (duration === undefined) duration = $.fx.speeds._default / 1000
if (delay === undefined) delay = 0
if ($.fx.off) duration = 0
if (typeof properties == 'string') {
// keyframe animation
cssValues[animationName] = properties
cssValues[animationDuration] = duration + 's'
cssValues[animationDelay] = delay + 's'
cssValues[animationTiming] = (ease || 'linear')
endEvent = $.fx.animationEnd
} else {
cssProperties = []
// CSS transitions
for (key in properties)
if (supportedTransforms.test(key)) transforms += key + '(' + properties[key] + ') '
else cssValues[key] = properties[key], cssProperties.push(dasherize(key))
if (transforms) cssValues[transform] = transforms, cssProperties.push(transform)
if (duration > 0 && typeof properties === 'object') {
cssValues[transitionProperty] = cssProperties.join(', ')
cssValues[transitionDuration] = duration + 's'
cssValues[transitionDelay] = delay + 's'
cssValues[transitionTiming] = (ease || 'linear')
}
}
wrappedCallback = function(event){
if (typeof event !== 'undefined') {
if (event.target !== event.currentTarget) return // makes sure the event didn't bubble from "below"
$(event.target).unbind(endEvent, wrappedCallback)
} else
$(this).unbind(endEvent, wrappedCallback) // triggered by setTimeout
fired = true
$(this).css(cssReset)
callback && callback.call(this)
}
if (duration > 0){
this.bind(endEvent, wrappedCallback)
// transitionEnd is not always firing on older Android phones
// so make sure it gets fired
setTimeout(function(){
if (fired) return
wrappedCallback.call(that)
}, ((duration + delay) * 1000) + 25)
}
// trigger page reflow so new elements can animate
this.size() && this.get(0).clientLeft
this.css(cssValues)
if (duration <= 0) setTimeout(function() {
that.each(function(){ wrappedCallback.call(this) })
}, 0)
return this
}複製程式碼
animation
最終呼叫的是 anim
方法,Zepto
也將這個方法暴露了出去,其實我覺得只提供 animation
方法就可以了,這個方法完全可以作為私有的方法呼叫。
引數預設值
if (duration === undefined) duration = $.fx.speeds._default / 1000
if (delay === undefined) delay = 0
if ($.fx.off) duration = 0複製程式碼
如果沒有傳遞持續時間 duration
,則預設為 $.fx.speends._default
的定義值 400ms
,這裡需要轉換成 s
。
如果沒有傳遞 delay
,則預設不延遲,即 0
。
如果瀏覽器不支援過渡和動畫,則 duration
設定為 0
,即沒有動畫,立即執行回撥。
處理animation動畫引數
if (typeof properties == 'string') {
// keyframe animation
cssValues[animationName] = properties
cssValues[animationDuration] = duration + 's'
cssValues[animationDelay] = delay + 's'
cssValues[animationTiming] = (ease || 'linear')
endEvent = $.fx.animationEnd
}複製程式碼
如果 properties
為 string
, 即 properties
為動畫名,則設定動畫對應的 css
,duration
和 delay
都加上了 s
的單位,預設的緩動函式為 linear
。
處理transition引數
else {
cssProperties = []
// CSS transitions
for (key in properties)
if (supportedTransforms.test(key)) transforms += key + '(' + properties[key] + ') '
else cssValues[key] = properties[key], cssProperties.push(dasherize(key))
if (transforms) cssValues[transform] = transforms, cssProperties.push(transform)
if (duration > 0 && typeof properties === 'object') {
cssValues[transitionProperty] = cssProperties.join(', ')
cssValues[transitionDuration] = duration + 's'
cssValues[transitionDelay] = delay + 's'
cssValues[transitionTiming] = (ease || 'linear')
}
}複製程式碼
supportedTransforms
是用來檢測是否為 transform
的正則,如果是 transform
,則拼接成符合 transform
規則的字串。
否則,直接將值存入 cssValues
中,將 css
的樣式名存入 cssProperties
中,並且呼叫了 dasherize
方法,使得 properties
的 css
樣式名( key
)支援駝峰式的寫法。
if (transforms) cssValues[transform] = transforms, cssProperties.push(transform)複製程式碼
這段是檢測是否有 transform
,如果有,也將 transform
存入 cssValues
和 cssProperties
中。
接下來判斷動畫是否開啟,並且是否有過渡屬性,如果有,則設定對應的值。
回撥函式的處理
wrappedCallback = function(event){
if (typeof event !== 'undefined') {
if (event.target !== event.currentTarget) return // makes sure the event didn't bubble from "below"
$(event.target).unbind(endEvent, wrappedCallback)
} else
$(this).unbind(endEvent, wrappedCallback) // triggered by setTimeout
fired = true
$(this).css(cssReset)
callback && callback.call(this)
}複製程式碼
如果瀏覽器支援過渡或者動畫事件,則在動畫結束的時候,取消事件監聽,注意在 unbind
時,有個 event.target !== event.currentTarget
的判定,這是排除冒泡事件。
如果事件不存在時,直接取消對應元素上的事件監聽。
並且將狀態控制 fired
設定為 true
,表示回撥已經執行。
動畫完成後,再將涉及過渡或動畫的樣式設定為空。
最後,呼叫傳遞進來的回撥函式,整個動畫完成。
繫結過渡或動畫的結束事件
if (duration > 0){
this.bind(endEvent, wrappedCallback)
setTimeout(function(){
if (fired) return
wrappedCallback.call(that)
}, ((duration + delay) * 1000) + 25)
}複製程式碼
繫結過渡或動畫的結束事件,在動畫結束時,執行處理過的回撥函式。
注意這裡有個 setTimeout
,是避免瀏覽器不支援過渡或動畫事件時,可以通過 setTimeout
執行回撥。setTimeout
的回撥執行比動畫時間長 25ms
,目的是讓事件響應在 setTimeout
之前,如果瀏覽器支援過渡或動畫事件, fired
會在回撥執行時設定成 true
, setTimeout
的回撥函式不會再重複執行。
觸發頁面迴流
// trigger page reflow so new elements can animate
this.size() && this.get(0).clientLeft
this.css(cssValues)複製程式碼
這裡用了點黑科技,讀取 clientLeft
屬性,觸發頁面的迴流,使得動畫的樣式設定上去時可以立即執行。
具體可以這篇文章中的解釋:2014-02-07-hidden-documentation.md
過渡時間不大於零的回撥處理
if (duration <= 0) setTimeout(function() {
that.each(function(){ wrappedCallback.call(this) })
}, 0)複製程式碼
duration
不大於零時,可以是引數設定錯誤,也可能是瀏覽器不支援過渡或動畫,就立即執行回撥函式。
系列文章
- 讀Zepto原始碼之程式碼結構
- 讀Zepto原始碼之內部方法
- 讀Zepto原始碼之工具函式
- 讀Zepto原始碼之神奇的$
- 讀Zepto原始碼之集合操作
- 讀Zepto原始碼之集合元素查詢
- 讀Zepto原始碼之操作DOM
- 讀Zepto原始碼之樣式操作
- 讀Zepto原始碼之屬性操作
- 讀Zepto原始碼之Event模組
- 讀Zepto原始碼之IE模組
- 讀Zepto原始碼之Callbacks模組
- 讀Zepto原始碼之Deferred模組
- 讀Zepto原始碼之Ajax模組
- 讀Zepto原始碼之Assets模組
- 讀Zepto原始碼之Selector模組
- 讀Zepto原始碼之Touch模組
- 讀Zepto原始碼之Gesture模組
- 讀Zepto原始碼之IOS3模組
附文
參考
- 一步一步DIY zepto庫,研究zepto原始碼7--動畫模組(fx fx_method)/)
- How (not) to trigger a layout in WebKit
- 2014-02-07-hidden-documentation.md
License
署名-非商業性使用-禁止演繹 4.0 國際 (CC BY-NC-ND 4.0)
最後,所有文章都會同步傳送到微信公眾號上,歡迎關注,歡迎提意見:
作者:對角另一面