博客 / 詳情

返回

讀Zepto源碼之Fx模塊

fx 模塊為利用 CSS3 的過渡和動畫的屬性為 Zepto 提供了動畫的功能,在 fx 模塊中,只做了事件和樣式瀏覽器前綴的補全,沒有做太多的兼容。對於不支持 CSS3 過渡和動畫的, Zepto 的處理也相對簡單,動畫立即完成,馬上執行回調。

讀 Zepto 源碼系列文章已經放到了github上,歡迎star: reading-zepto

源碼版本

本文閲讀的源碼為 zepto1.2.0

GitBook

《reading-zepto》

內部方法

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'] = ''

獲取瀏覽器前綴後,為所有的 transitionanimation 屬性加上對應的前綴,都初始化為 '',方便後面使用。

方法

$.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 ,即 slowfast 甚至 _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
} 

如果 propertiesstring, 即 properties 為動畫名,則設置動畫對應的 cssdurationdelay 都加上了 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 方法,使得 propertiescss 樣式名( key )支持駝峯式的寫法。

if (transforms) cssValues[transform] = transforms, cssProperties.push(transform)

這段是檢測是否有 transform ,如果有,也將 transform 存入 cssValuescssProperties 中。

接下來判斷動畫是否開啓,並且是否有過渡屬性,如果有,則設置對應的值。

回調函數的處理

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 會在回調執行時設置成 truesetTimeout 的回調函數不會再重複執行。

觸發頁面迴流

 // 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 不大於零時,可以是參數設置錯誤,也可能是瀏覽器不支持過渡或動畫,就立即執行回調函數。

系列文章

  1. 讀Zepto源碼之代碼結構
  2. 讀Zepto源碼之內部方法
  3. 讀Zepto源碼之工具函數
  4. 讀Zepto源碼之神奇的$
  5. 讀Zepto源碼之集合操作
  6. 讀Zepto源碼之集合元素查找
  7. 讀Zepto源碼之操作DOM
  8. 讀Zepto源碼之樣式操作
  9. 讀Zepto源碼之屬性操作
  10. 讀Zepto源碼之Event模塊
  11. 讀Zepto源碼之IE模塊
  12. 讀Zepto源碼之Callbacks模塊
  13. 讀Zepto源碼之Deferred模塊
  14. 讀Zepto源碼之Ajax模塊
  15. 讀Zepto源碼之Assets模塊
  16. 讀Zepto源碼之Selector模塊
  17. 讀Zepto源碼之Touch模塊
  18. 讀Zepto源碼之Gesture模塊
  19. 讀Zepto源碼之IOS3模塊

附文

  • 譯:怎樣處理 Safari 移動端對圖片資源的限制

參考

  • 一步一步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)

最後,所有文章都會同步發送到微信公眾號上,歡迎關注,歡迎提意見:

作者:對角另一面

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.